Belts, filters, screw conveyors, shafts and pipe pieces never fill a rectangle; a diagonal belt drag stays sloped with Shift held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
14 KiB
JavaScript
180 lines
14 KiB
JavaScript
// Everything the player can build (PIECES): blocks, machines and molds, with costs and
|
|
// descriptions; filter settings; wall materials and their cost.
|
|
// ---------- Building pieces ----------
|
|
// Everything you build snaps to a grid of 4x4-grain blocks. The world itself
|
|
// stays grain-level; only built pieces snap.
|
|
const BS = 4;
|
|
const BP_WALL = 1, BP_BELT = 2, BP_SCREW = 3, BP_SHAFT = 4, BP_GRATE = 5, BP_GATE = 6, BP_FILTER = 7, BP_PIPE = 8, BP_PUMP = 9, BP_OUTLET = 10, BP_GRIND = 11, BP_SAW = 12, BP_PRESS = 13, BP_RACK = 14;
|
|
const BP_SLOPE_BL = 15, BP_SLOPE_BR = 16, BP_SLOPE_TL = 17, BP_SLOPE_TR = 18;
|
|
// The shape is independent of the direction: reversing a belt never flips its face.
|
|
const BP_BELT_UP = 19, BP_BELT_DOWN = 20, BP_FILTER_UP = 21, BP_FILTER_DOWN = 22;
|
|
function isSlopeBelt(c) { return c >= BP_BELT_UP && c <= BP_FILTER_DOWN; }
|
|
function isFilter(c) { return c === BP_FILTER || c === BP_FILTER_UP || c === BP_FILTER_DOWN; }
|
|
function isBelt(c) { return c === BP_BELT || c === BP_BELT_UP || c === BP_BELT_DOWN; }
|
|
function beltGrade(c) { return c === BP_BELT_UP || c === BP_FILTER_UP ? -1 : c === BP_BELT_DOWN || c === BP_FILTER_DOWN ? 1 : 0; }
|
|
function slopeShape(c) { return isSlopeBelt(c) ? (beltGrade(c) < 0 ? BP_SLOPE_BR : BP_SLOPE_BL) : c; }
|
|
function beltFaceY(c, bx, by, x) { return by * BS + (beltGrade(c) < 0 ? 3 - (x - bx * BS) : beltGrade(c) > 0 ? x - bx * BS : 0); }
|
|
function filterVariant(c, v) { return beltGrade(c) ? (beltGrade(c) > 0 ? 128 : 0) : v & 128; }
|
|
function beltDirection(c, v) {
|
|
const d = isFilter(c) ? (filterVariant(c, v) & 128 ? 1 : -1) : (v ? 1 : -1), g = beltGrade(c);
|
|
return g ? (g * d < 0 ? 'up-' : 'down-') + (d > 0 ? 'right' : 'left') : d > 0 ? 'right' : 'left';
|
|
}
|
|
function isSlope(c) { return c >= BP_SLOPE_BL && c <= BP_SLOPE_TR; }
|
|
function isWall(c) { return c === BP_WALL || isSlope(c); }
|
|
function slopeCell(c, x, y) {
|
|
c = slopeShape(c);
|
|
if (c === BP_SLOPE_BR || c === BP_SLOPE_TR) x = 3 - x;
|
|
if (c === BP_SLOPE_TL || c === BP_SLOPE_TR) y = 3 - y;
|
|
return y >= x;
|
|
}
|
|
function slopeCode(dx, dy, orientation) {
|
|
if (!diagonalDrag(dx, dy)) return BP_SLOPE_BL + orientation;
|
|
return dx * dy > 0 ? (orientation >= 2 ? BP_SLOPE_TR : BP_SLOPE_BL) : (orientation >= 2 ? BP_SLOPE_TL : BP_SLOPE_BR);
|
|
}
|
|
function isPipeCode(c) { return c === BP_PIPE || c === BP_PUMP || c === BP_OUTLET; }
|
|
// pieces that carry something along a run: a Shift-drag still draws a line, never a filled rectangle
|
|
function lineOnly(c) { return isBelt(c) || isFilter(c) || c === BP_SCREW || c === BP_SHAFT || isPipeCode(c); }
|
|
// Filters: each filter block has a set of materials and a mode. In drop mode (inv 0)
|
|
// the chosen materials fall through and everything else rides along on top; in carry
|
|
// mode (inv 1) the chosen ones ride along and everything else falls through. The block
|
|
// variant's top bit is which way it carries (1 right, 0 left).
|
|
let filterCfg = new Map(); // block index -> { m: [materials], inv: 0|1, pass: Uint8Array(NMAT) }
|
|
function filterable(m) { return m > 0 && m < NMAT && (PH[m] === PH_POWDER || PH[m] === PH_ITEM); }
|
|
function cleanFilterCfg(c) { return { m: [...new Set(((c && c.m) || []).filter(filterable))].sort((a, b) => a - b), inv: c && c.inv ? 1 : 0 }; }
|
|
function filterRec(c) {
|
|
const k = cleanFilterCfg(c), pass = new Uint8Array(NMAT);
|
|
for (let m = 1; m < NMAT; m++) if (filterable(m)) pass[m] = (k.m.includes(m) ? 1 : 0) ^ k.inv;
|
|
return { m: k.m, inv: k.inv, pass };
|
|
}
|
|
function filterPassAt(m, j) {
|
|
let b = (((j / W) | 0) >> 2) * BW + ((j % W) >> 2);
|
|
if (!isFilter(blockP[b])) { const owner = slopeSealOwner(j); if (owner >= 0) b = owner; }
|
|
const c = filterCfg.get(b);
|
|
return c ? c.pass[m] : 0;
|
|
}
|
|
function filterInfo(b) { const c = filterCfg.get(b); return c ? { m: c.m.slice(), inv: c.inv } : { m: [], inv: 0 }; }
|
|
function sameFilter(a, b) { return a.inv === b.inv && a.m.length === b.m.length && a.m.every((m, k) => m === b.m[k]); }
|
|
// loose material -> the solid it becomes in a wall block
|
|
const WALL_OF = new Uint8Array(NMAT);
|
|
WALL_OF[LDIRT] = DIRT; WALL_OF[RSTONE] = STONE; WALL_OF[RGRAN] = GRANITE; WALL_OF[RBAS] = BASALT; WALL_OF[LCLAY] = CLAY;
|
|
WALL_OF[LWOOD] = WOOD; WALL_OF[RBRICK] = BRICK; WALL_OF[CULLET] = GLASS; WALL_OF[BARCU] = COPPER; WALL_OF[BARFE] = IRON; WALL_OF[BARST] = STEEL;
|
|
WALL_OF[BARSB] = CUTST; WALL_OF[PLANK] = WOOD; WALL_OF[BARBR] = BRICK; WALL_OF[BARGL] = GLASS;
|
|
const WALL_LIST = [RSTONE, BARSB, PLANK, BARBR, LCLAY, RBRICK, LDIRT, LWOOD, CULLET, BARGL, RGRAN, RBAS, BARCU, BARFE, BARST];
|
|
const BAR_OF = new Uint8Array(NMAT); BAR_OF[COPPER] = BARCU; BAR_OF[IRON] = BARFE; BAR_OF[STEEL] = BARST;
|
|
BAR_OF[GLASS] = BARGL; BAR_OF[CUTST] = BARSB; BAR_OF[WOOD] = PLANK; BAR_OF[TIN] = BARSN; BAR_OF[ZINC] = BARZN; BAR_OF[SILVER] = BARAG; BAR_OF[GOLD] = BARAU; BAR_OF[BRONZE] = BARBZ; BAR_OF[BRASS] = BARBS;
|
|
const GRAINS_PER_BAR = 8;
|
|
function wallCost(v, cells) { return [[v, PH[v] === PH_ITEM ? Math.ceil(cells / GRAINS_PER_BAR) : cells]]; }
|
|
|
|
// For now every machine simply runs: no power to supply. The power pieces (shaft,
|
|
// water wheel, steam engine) are hidden from the build menu but old saves keep them.
|
|
const POWER_FREE = true;
|
|
const CASH = 255; // stands for money in cost lists
|
|
const PIECES = [
|
|
{ id: 'wall', code: BP_WALL, name: 'Wall block', kind: 'block', variants: 'wall',
|
|
desc: 'A solid block. Diagonal drags make a sloped face; T flips it. For one block, T cycles square and four slope corners. R picks the material: a square costs 16 grains or 2 bars; a slope costs 8 grains or 1 bar.' },
|
|
{ id: 'belt', code: BP_BELT, name: 'Belt', kind: 'block', cost: [[CASH, 12]], dirs: ['left', 'right'],
|
|
desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it. Drag diagonally for a sidewall belt. Moving cleats lift pockets of grains; extra grains roll back.' },
|
|
{ id: 'screw', code: BP_SCREW, name: 'Screw conveyor', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left', 'up-right', 'down-right', 'down-left', 'up-left'],
|
|
desc: 'A turning screw inside a tube that draws in loose grains and whole bars and pushes them along, four grains wide. Water and other liquids run straight through it; the screw never carries them. Drag a line to set its direction, including diagonals. R turns a single screw conveyor. Join either end to screw conveyors or belts. Copper softens around 1000°C.' },
|
|
{ id: 'filter', code: BP_FILTER, name: 'Filter', kind: 'block', cost: [[CASH, 40]], dirs: ['left', 'right'], filter: true,
|
|
desc: 'A shaking mesh that joins belts. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through. Drag diagonally for a downhill screen. Grains slide down while it shakes; R keeps a picked slope downhill.' },
|
|
{ id: 'shaft', code: BP_SHAFT, name: 'Shaft', kind: 'block', cost: [[LWOOD, 8]], hidden: POWER_FREE,
|
|
desc: 'Carries power between machines. Wooden, so keep it away from fire.' },
|
|
{ id: 'grate', code: BP_GRATE, name: 'Grate', kind: 'block', cost: [[LCLAY, 8]],
|
|
desc: 'Ceramic grille. Liquids and gases pass; powders and bars do not.' },
|
|
{ id: 'gate', code: BP_GATE, name: 'Gate', kind: 'block', cost: [[LCLAY, 8]],
|
|
desc: 'Ceramic slide gate. Press F on it to open or close every gate block touching it.' },
|
|
{ id: 'pipe', code: BP_PIPE, name: 'Pipe', kind: 'block', cost: [[CASH, 8]],
|
|
desc: 'Thin copper pipe for liquids. Sand, water and anything else loose fall past it, so a run of pipe never dams anything. Every pipe, pump and outlet touching another joins it in one line, and the line holds 16 grains of liquid per block. Drag to lay a run. Taking a pipe back spills what was in it.' },
|
|
{ id: 'pump', code: BP_PUMP, name: 'Pump', kind: 'block', cost: [[CASH, 70]], dirs: ['up', 'right', 'down', 'left'],
|
|
desc: 'Draws liquid in at the tail of its arrow (keep that side under water) and pushes it into the pipes on its other sides, about 60 grains a second while the line has room. It takes water and anything else cooler than 500°C, one liquid at a time per line. Press F to switch it on or off.' },
|
|
{ id: 'outlet', code: BP_OUTLET, name: 'Outlet', kind: 'block', cost: [[CASH, 15]], dirs: ['down', 'left', 'up', 'right'],
|
|
desc: 'Lets a pipe line\'s liquid out of the side its arrow points to, wherever there is room for it. Press F to open or close it.' },
|
|
{ id: 'grinder', code: BP_GRIND, name: 'Grinder', kind: 'block', cost: [[CASH, 60]],
|
|
desc: 'Grinds rubble, gravel, sand and dirt resting on top into sand and the odd grain of ore. Results drop out below; keep that space clear. Wider rows grind faster. Stack under a Saw to cut stone and grind the rest.' },
|
|
{ id: 'press', code: BP_PRESS, name: 'Brick press', kind: 'block', cost: [[CASH, 60]],
|
|
desc: 'Presses clay lumps resting on top into a green brick for every 8. They drop out underneath; put a belt or an open drop under it. Fire green bricks in a fire hotter than about 900°C to make Bricks. Wider rows press faster.' },
|
|
{ id: 'rack', name: 'Bar rack', kind: 'block', code: BP_RACK, cost: [[CASH, 40]],
|
|
desc: 'Stores bars of any kind, 50 per block. Stack blocks into a column for more room. Bars that land on top go in; F opens the gate at the bottom and lets them out one at a time, oldest first, onto a belt below.' },
|
|
{ id: 'saw', code: BP_SAW, name: 'Saw', kind: 'block', cost: [[CASH, 90]],
|
|
desc: 'Cuts stone rubble into stone blocks and wood chunks into planks, one from every 8 grains resting on top. They drop out underneath and pile up there: put a belt or an open drop under it to carry them away. Wider rows cut faster. Stack over a Grinder to grind everything else.' },
|
|
{ id: 'wheel', name: 'Water wheel', kind: 'machine', bw: 3, bh: 3, cost: [[LWOOD, 48]], hidden: POWER_FREE,
|
|
desc: 'Makes power when liquid falls through it. More flow, more power.' },
|
|
{ id: 'fan', name: 'Fan', kind: 'machine', bw: 1, bh: 1, cost: [[CASH, 60]], dirs: ['right', 'down', 'left', 'up'],
|
|
desc: 'Blows gas and forces air into fires, making them far hotter: its stream reaches 28 grains and passes through grates but not walls. Its copper body melts at 1085°C, so keep it outside the furnace, a block clear of the wall, blowing in through a grate. Press F to switch it on or off.' },
|
|
{ id: 'silo', name: 'Silo', kind: 'machine', bw: 3, bh: 6, cost: [[CASH, 180]],
|
|
desc: 'A steel bin on legs for one loose material: up to 4,000 grains go in through the hatch in its roof (drop them in from a belt or screw conveyor). Its gauge shows how full it is. Press F to open the gate in its cone and let it out underneath, about 60 grains a second; F again shuts it. The first material in decides what it holds until it is empty.' },
|
|
{ id: 'engine', name: 'Steam engine', kind: 'machine', bw: 3, bh: 2, cost: [[BARCU, 10]], dirs: ['exhaust right', 'exhaust left'], hidden: POWER_FREE,
|
|
desc: 'Steam rising into the intake slots on its underside drives it. Condensate drips out the exhaust side.' },
|
|
{ id: 'dock', name: 'Trade dock', kind: 'machine', bw: 6, bh: 4, cost: [], hidden: true, fixed: true,
|
|
desc: 'Owned by the claim office. Anything that drops through the grate in its floor is sold on the spot. Build walls around it to funnel more in.' },
|
|
{ id: 'ingotmold', name: 'Ingot mold', kind: 'mold', mold: 'ingot', variants: 'wall',
|
|
desc: 'A long trough. Pour molten metal in: bars of 8 grains drop out of the bottom when set, up to 3. Leave one open block below it; a belt under that carries them off. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap.' },
|
|
{ id: 'bitmold', name: 'Bit mold', kind: 'mold', mold: 'bit', variants: 'wall',
|
|
desc: 'A drill-bit-shaped cavity. Cast a harder metal in it to upgrade your drill.' },
|
|
{ id: 'tankmold', name: 'Tank mold', kind: 'mold', mold: 'tank', variants: 'wall',
|
|
desc: 'A tank-shaped cavity. Cast metal in it to carry more.' },
|
|
];
|
|
const PIECE_BY_ID = {}, PIECE_BY_CODE = [];
|
|
PIECES.forEach((p, i) => { p.index = i; PIECE_BY_ID[p.id] = p; if (p.code) PIECE_BY_CODE[p.code] = p; if (p.kind === 'machine') { p.w = p.bw * BS; p.h = p.bh * BS; } });
|
|
for (let c = BP_SLOPE_BL; c <= BP_SLOPE_TR; c++) PIECE_BY_CODE[c] = PIECE_BY_ID.wall;
|
|
for (const c of [BP_BELT_UP, BP_BELT_DOWN]) PIECE_BY_CODE[c] = PIECE_BY_ID.belt;
|
|
for (const c of [BP_FILTER_UP, BP_FILTER_DOWN]) PIECE_BY_CODE[c] = PIECE_BY_ID.filter;
|
|
// power per grain of belt / screw (a block is 16 grains)
|
|
const BELT_POWER = 0.03, SCREW_POWER = 0.125, FAN_POWER = 6;
|
|
const WHEEL_K = 0.32, ENGINE_K = 40;
|
|
|
|
const MOLDS = {
|
|
ingot: {
|
|
// one long trough: metal sets level and drops bars of 8 grains below
|
|
name: 'Ingot mold', rows: [
|
|
'................',
|
|
'#oooooooooooooo#',
|
|
'#oooooooooooooo#',
|
|
'################',
|
|
]
|
|
},
|
|
bit: {
|
|
name: 'Bit mold', rows: [
|
|
'#..............#',
|
|
'##............##',
|
|
'###oooooooooo###',
|
|
'####oooooooo####',
|
|
'#####oooooo#####',
|
|
'######oooo######',
|
|
'#######oo#######',
|
|
'################',
|
|
]
|
|
},
|
|
tank: {
|
|
name: 'Tank mold', rows: [
|
|
'#..............#',
|
|
'#oo##########oo#',
|
|
'#oo##########oo#',
|
|
'#oo##########oo#',
|
|
'#oo##########oo#',
|
|
'#oooooooooooooo#',
|
|
'#oooooooooooooo#',
|
|
'################',
|
|
]
|
|
},
|
|
};
|
|
for (const k in MOLDS) {
|
|
const md = MOLDS[k]; md.w = md.rows[0].length; md.h = md.rows.length; md.bw = md.w / BS; md.bh = md.h / BS; md.walls = 0; md.cav = 0;
|
|
for (const r of md.rows) for (const ch of r) { if (ch === '#') md.walls++; else if (ch === 'o') md.cav++; }
|
|
// split the cavity into separate pieces (the ingot mold has three)
|
|
const seenC = new Set(); md.cavs = [];
|
|
for (let r = 0; r < md.h; r++) for (let c = 0; c < md.w; c++) {
|
|
if (md.rows[r][c] !== 'o' || seenC.has(r * 64 + c)) continue;
|
|
const comp = [], st = [[c, r]]; seenC.add(r * 64 + c);
|
|
while (st.length) {
|
|
const [cx, cy] = st.pop(); comp.push([cx, cy]);
|
|
for (const [nx, ny] of [[cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]]) {
|
|
if (nx < 0 || ny < 0 || nx >= md.w || ny >= md.h || md.rows[ny][nx] !== 'o' || seenC.has(ny * 64 + nx)) continue;
|
|
seenC.add(ny * 64 + nx); st.push([nx, ny]);
|
|
}
|
|
}
|
|
md.cavs.push(comp);
|
|
}
|
|
}
|