153 lines
9.0 KiB
JavaScript
153 lines
9.0 KiB
JavaScript
// 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;
|
|
}
|