// core/multiblock.js; defineGridMultiblock(): the shared place/break machinery. import { BlockPermutation, getDir, safeGetBlock } from "./mc.js"; import { grid, fcCanReplace } from "./grid.js"; import { undoPlace, breakAir } from "./fx.js"; import { onPlace, onBreak } from "./events.js"; // cfg: // prefix block-id prefix, e.g. "rzb_dcs:fc_" (color canopies: "rzb_dcs:um_") // anchorKey the catalog/menu cell, e.g. "3_0_0" // cells { cellKey: [dx,dy,dz] } south-frame offsets // name human label for the "not enough room" undo message // rotating=true grid rotates with facing (false = symmetric canopy: raw offsets, placed with no facing state) // color=false ids carry a trailing "_" (um/bu/bt); parse splits it off // canReplace predicate for a cell being buildable (default fcCanReplace; lg passes air|water) // undefinedBlocks unreadable cell (chunk edge/ceiling) blocks placement (at/tent/lg); default: it's allowed // actionBar optional (placed,total,dir) => string shown after a successful build (fc/ic) // returns { prefix, anchorKey, cells, color, rotating, parse, cellOf, cellWorld, anchorFrom, idFor }. export function defineGridMultiblock(cfg) { const { prefix, anchorKey, cells, name, rotating = true, color = false, canReplace = fcCanReplace, undefinedBlocks = false, actionBar = null } = cfg; const g = grid(cells, rotating); const total = Object.keys(cells).length - 1; const parse = color ? (id) => { if (!id || !id.startsWith(prefix)) return null; const m = id.slice(prefix.length).match(/^(\d+_\d+_\d+)_(.+)$/); return (m && cells[m[1]]) ? { cell: m[1], color: m[2] } : null; } : (id) => { if (!id || !id.startsWith(prefix)) return null; const c = id.slice(prefix.length); return cells[c] ? c : null; }; const cellOf = color ? (id) => parse(id)?.cell ?? null : parse; const idFor = (cell, col) => color ? prefix + cell + "_" + col : prefix + cell; onPlace((ev) => { const b = ev.block, pr = parse(b.typeId); if (color ? !(pr && pr.cell === anchorKey) : pr !== anchorKey) return; const dir = rotating ? getDir(b.permutation) : "south", col = color ? pr.color : null; for (const c in cells) { // validate every non-anchor cell first -> undo if any is blocked (never half-place) if (c === anchorKey) continue; const nb = safeGetBlock(b.dimension, g.cellWorld(c, b.x, b.y, b.z, dir)); const blocked = undefinedBlocks ? !canReplace(nb) : (nb !== undefined && !canReplace(nb)); if (blocked) { undoPlace(b, ev.player, idFor(anchorKey, col), name); return; } } let placed = 0; for (const c in cells) { if (c === anchorKey) continue; const nb = b.dimension.getBlock(g.cellWorld(c, b.x, b.y, b.z, dir)); if (canReplace(nb)) { try { nb.setPermutation(BlockPermutation.resolve(idFor(c, col), rotating ? { "minecraft:cardinal_direction": dir } : {})); placed++; } catch {} } } if (actionBar) { try { ev.player.onScreenDisplay.setActionBar(actionBar(placed, total, dir)); } catch {} } }); onBreak((ev) => { const cell = cellOf(ev.brokenBlockPermutation?.type?.id || ""); if (!cell) return; const dir = rotating ? getDir(ev.brokenBlockPermutation) : "south"; const b = ev.block, a = g.anchorFrom(cell, b.x, b.y, b.z, dir); for (const c in cells) { if (c === cell) continue; const nb = safeGetBlock(b.dimension, g.cellWorld(c, a.x, a.y, a.z, dir)); if (nb && cellOf(nb.typeId) === c) { try { breakAir(nb); } catch {} } } }); return { prefix, anchorKey, cells, color, rotating, parse, cellOf, cellWorld: g.cellWorld, anchorFrom: g.anchorFrom, idFor }; }