// Money, prices, market saturation, the trade dock, buyer and builder orders. // ---------- Economy ---------- // You sell by putting goods into the trade dock: they are paid for as they drop // through its grate. Money buys machines; wall blocks, molds and bits you still // make from your own materials. The claim on this land starts in debt. let money = 250, debt = 20000, earned = 0; const CLAIM_DEBT = 20000; // base price per grain (per bar for bars). 0 = not wanted: it drops through for nothing. const PRICE = new Float32Array(NMAT); for (const [m, v] of [ [SAND, 0.1], [LCLAY, 0.2], [LWOOD, 0.3], [ACORN, 0.5], [COAL, 0.5], [RGRAN, 0.3], [RBAS, 0.5], [CUORE, 1], [FEORE, 1.6], [SNORE, 2], [ZNORE, 2.2], [AGORE, 6], [AUORE, 14], [CHARCOAL, 1.2], [RBRICK, 0.8], [CULLET, 1.2], [BARGL, 14], [RSTONE, 0.02], [GRAVEL, 0.03], [BARSB, 0.6], [PLANK, 2.4], [GREENB, 2], [BARBR, 9], [SCRCU, 3], [SCRFE, 5], [SCRST, 11], [SCRSN, 3.5], [SCRZN, 3.5], [SCRAG, 18], [SCRAU, 40], [SCRBZ, 8], [SCRBS, 7], [BARCU, 40], [BARFE, 75], [BARST, 160], [BARSN, 50], [BARZN, 50], [BARAG, 220], [BARAU, 480], [BARBZ, 110], [BARBS, 95], ]) PRICE[m] = v; // what a cell is sold as: cast metal sitting in the hopper sells as scrap function saleAs(m) { return SOLIDMETAL[m] ? YLD[m] : CASTMETAL[m] ? YLD[FRZTO[m]] : m; } // The market dips when you sell a lot of one thing, and recovers over time. const SAT_K = 2500, SAT_FLOOR = 0.3, SAT_HALF = 28800; // half-life: 8 minutes of game time let mkRecent = new Float32Array(NMAT); function priceFactor(m) { return Math.max(SAT_FLOOR, 1 / (1 + mkRecent[m] / SAT_K)); } function priceNow(m) { return PRICE[m] * priceFactor(m); } function sellUnit(m) { const p = priceNow(m); mkRecent[m] += PRICE[m]; return p; } function marketTick() { const f = Math.pow(0.5, 60 / SAT_HALF); for (let m = 0; m < NMAT; m++) if (mkRecent[m] > 0.01) mkRecent[m] *= f; else mkRecent[m] = 0; } function resetEconomy() { money = 250; debt = CLAIM_DEBT; earned = 0; mkRecent = new Float32Array(NMAT); shipLog = []; saleAcc = { total: 0, items: {} }; floatAcc = 0; orders = []; ordersDone = 0; nextOrderT = 3600; orderSeq = 1; } function payClaim(amount) { const a = Math.max(0, Math.min(amount, money, debt)); money -= a; debt -= a; if (a > 0 && debt <= 0) emit({ t: 'claim_paid' }); return a; } // The trade dock: an open bin with a grated floor over a chute to the claim office. // Anything that reaches the grate (and is cool enough to handle) is sold on the spot. let shipLog = [], saleAcc = { total: 0, items: {} }, floatAcc = 0; function dock() { for (const mm of machines) if (mm && mm.type === 'dock') return mm; return null; } function inDock(d, x, y) { return x >= d.x + 2 && x < d.x + 22 && y >= d.y && y < d.y + 14; } function dockTick() { const d = dock(); if (!d) return; const fy = d.y + 13; for (let x = d.x + 2; x < d.x + 22; x++) { const i = fy * W + x, m = mat[i]; if (m === EMPTY || PH[m] === PH_GAS || temp[i] >= 150 || (flags[i] & F_BURN)) continue; setCell(i, EMPTY, AMB[fy], 0); sellToDock(saleAs(m)); } for (const e of ents.slice()) { if (e.iy + BAR_H - 1 >= fy && inDock(d, e.ix + 2, fy) && e.T < 150) { removeEnt(e); sellToDock(e.m); } } if (tick % 45 === 0 && floatAcc >= 0.5) { emit({ t: 'sold', v: floatAcc, x: d.x + 9 + rnd() * 6, y: d.y - 9 }); floatAcc = 0; } if (tick % 3600 === 0 && saleAcc.total > 0) { shipLog.unshift({ tick, total: Math.round(saleAcc.total), items: saleAcc.items }); if (shipLog.length > 12) shipLog.length = 12; saleAcc = { total: 0, items: {} }; } } // One unit reaches the grate: it goes to an open order for it first, else to the market. function sellToDock(m) { stats.sold = (stats.sold || 0) + 1; const o = orders.find(o => o.m === m && o.got < o.qty); if (o) { o.got++; if (o.got >= o.qty) completeOrder(o); return; } const p = sellUnit(m); money += p; earned += p; floatAcc += p; if (p > 0) { saleAcc.total += p; saleAcc.items[m] = (saleAcc.items[m] || 0) + 1; } } // Orders: buyers ask for an amount of one good by a deadline and pay well over market. // A missed deadline costs nothing: what you delivered is paid at market price. let orders = [], ordersDone = 0, nextOrderT = 3600, orderSeq = 1; const ORDER_SLOTS = 3; const STANDING = ['Local buyers', 'County buyers', 'Regional buyers', 'National buyers', 'Export houses']; function standing() { return Math.min(STANDING.length - 1, Math.floor(ordersDone / 4)); } // Builders buy the cheap bulk stuff (rubble, gravel, sand, stone blocks, planks) in big lots at a few // times the market price, so waste rock earns a little. [what, smallest lot, largest lot] const BUILD_GOODS = [[RSTONE, 400, 1500], [RGRAN, 300, 1200], [RBAS, 300, 1200], [GRAVEL, 300, 1000], [SAND, 300, 900], [BARSB, 20, 80], [PLANK, 20, 80], [BARBR, 20, 80]]; function makeBuildersOrder() { const cands = BUILD_GOODS.filter(([m]) => seen[m] && !orders.some(o => o.m === m)); if (!cands.length) return null; const [m, lo, hi] = cands[(rnd() * cands.length) | 0]; const qty = Math.round((lo + rnd() * (hi - lo)) * (1 + Math.min(1, ordersDone * 0.05)) / (PH[m] === PH_ITEM ? 1 : 10)) * (PH[m] === PH_ITEM ? 1 : 10); const mult = Math.round((PH[m] === PH_ITEM ? 2.5 : 4) + rnd() * 2) ; const minutes = Math.max(6, Math.min(20, Math.round(qty / (PH[m] === PH_ITEM ? 6 : 90)))); return { id: orderSeq++, m, qty, got: 0, mult, pay: Math.max(5, Math.round(qty * PRICE[m] * mult)), due: tick + minutes * 3600, mins: minutes, who: 'Builders' }; } function makeOrder() { if (rnd() < 0.3) { const b = makeBuildersOrder(); if (b) return b; } const cands = []; for (let m = 0; m < NMAT; m++) if (PRICE[m] > 0 && seen[m] && !orders.some(o => o.m === m) && !BUILD_GOODS.some(([g]) => g === m && g !== SAND)) cands.push(m); if (!cands.length) return null; const weight = m => (PH[m] === PH_ITEM ? 4 : 1) * (m === SCRCU || m === SCRFE || m === SCRST || m === SCRSN || m === SCRZN ? 0.4 : 1) * (PRICE[m] >= 5 ? 1.5 : 1); let sum = 0; for (const m of cands) sum += weight(m); let r = rnd() * sum, m = cands[0]; for (const c of cands) { r -= weight(c); if (r <= 0) { m = c; break; } } const target = (220 + ordersDone * 110) * (0.7 + rnd() * 0.7); // order size at base price let qty = Math.round(target / PRICE[m]); qty = PH[m] === PH_ITEM ? Math.max(2, Math.min(80, qty)) : Math.max(15, Math.min(900, Math.round(qty / 5) * 5)); const mult = Math.round((1.6 + rnd() * 0.7 + Math.min(0.6, ordersDone * 0.03)) * 10) / 10; const minutes = Math.max(5, Math.min(20, Math.round(4 + qty * PRICE[m] / 90))); return { id: orderSeq++, m, qty, got: 0, mult, pay: Math.round(qty * PRICE[m] * mult), due: tick + minutes * 3600, mins: minutes }; } function ordersTick() { for (let k = orders.length - 1; k >= 0; k--) { const o = orders[k]; if (tick < o.due) continue; orders.splice(k, 1); let paid = 0; for (let n = 0; n < o.got; n++) paid += sellUnit(o.m); money += paid; earned += paid; emit({ t: 'order_expired', m: o.m, got: o.got, qty: o.qty, paid: Math.round(paid) }); } if (orders.length < ORDER_SLOTS && tick >= nextOrderT) { const o = makeOrder(); nextOrderT = tick + 3600 + ((rnd() * 3600) | 0); if (o) { orders.push(o); emit({ t: 'order_new', o }); } } } function completeOrder(o) { orders = orders.filter(x => x !== o); money += o.pay; earned += o.pay; ordersDone++; emit({ t: 'order_done', m: o.m, qty: o.qty, pay: o.pay }); } // Put the dock on open ground near x: the nearest spot with nothing built in it. function placeDock(cx) { const BWD = 6, BHD = 4; for (let off = 0; off < W; off += 4) for (const sg of off ? [1, -1] : [1]) { const bx = Math.floor((cx + sg * off) / BS); if (bx < 2 || bx + BWD > BW - 2) continue; const x0 = bx * BS, x1 = x0 + BWD * BS; // the highest natural ground under it let g = H, ok = true; for (let x = x0; x < x1 && ok; x++) { let y = 0; while (y < H - 1 && !(PH[mat[y * W + x]] === PH_SOLID && (flags[y * W + x] & F_NAT))) y++; if (y < g) g = y; if (y > H - 40) ok = false; } if (!ok) continue; const by = Math.floor((g + 1) / BS) - BHD + 1; if (by < 2) continue; // nothing of yours may be in the way for (let y = by * BS; y < (by + BHD) * BS && ok; y++) for (let x = x0; x < x1; x++) { const i = y * W + x; if (mach[i] || part[i] || occ[i] || blockP[((y / BS) | 0) * BW + ((x / BS) | 0)] || (PH[mat[i]] === PH_SOLID && !(flags[i] & F_NAT)) || PH[mat[i]] === PH_LIQUID) { ok = false; break; } } if (!ok || molds.some(mo => mo.x < x1 && mo.x + mo.w > x0 && mo.y < (by + BHD) * BS && mo.y + mo.h > by * BS)) continue; for (let y = by * BS; y < (by + BHD) * BS; y++) for (let x = x0; x < x1; x++) { const i = y * W + x; bgm[i] = 0; if (mat[i] !== EMPTY) setCell(i, EMPTY, AMB[y], 0); } // fill under it down to the ground so it does not hang in the air for (let x = x0; x < x1; x++) for (let y = (by + BHD) * BS; y < H; y++) { const i = y * W + x; if (PH[mat[i]] === PH_SOLID) break; setCell(i, DIRT, AMB[y], F_NAT); bgm[i] = 0; } addMachine('dock', bx, by, 0); return true; } return false; }