Buildings (modern/fantasy, multi-floor, footprint shapes), caves, outdoor biomes, and town/village + city-block settlements. Auto-places Foundry walls, doors, windows, lighting; furniture as locked tiles; live preview. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
3.5 KiB
JavaScript
92 lines
3.5 KiB
JavaScript
/**
|
|
* Room-aware furniture placement.
|
|
*
|
|
* Produces grid-aligned, non-overlapping placements that sit inside rooms,
|
|
* prefer walls for wall-pieces, and keep doorways clear. The SAME placements
|
|
* feed both the live preview (vector) and the locked Tiles placed in the scene,
|
|
* so they always match. Pure / Node-testable. Output px uses the given gridSize.
|
|
*
|
|
* Placement shape:
|
|
* { kind, subtype, rotation(0|90), cx, cy (centre px), wpx, hpx (native px) }
|
|
*/
|
|
|
|
import { Rng } from "./rng.mjs";
|
|
import { PIECES, piecesFor } from "./furniture.mjs";
|
|
|
|
/** Cells adjacent to a door — kept clear so furniture never blocks a doorway. */
|
|
function buildKeepClear(walls) {
|
|
const set = new Set();
|
|
for (const s of walls.segments ?? []) {
|
|
if (s.kind !== "door") continue;
|
|
if (s.orient === "v") { set.add(`${s.x1 - 1},${s.y1}`); set.add(`${s.x1},${s.y1}`); }
|
|
else { set.add(`${s.x1},${s.y1 - 1}`); set.add(`${s.x1},${s.y1}`); }
|
|
}
|
|
return set;
|
|
}
|
|
|
|
export function computePlacements(layout, walls, gs, subtype, seed) {
|
|
const { region, cols, rows, rooms } = layout;
|
|
const rng = new Rng(`${seed}::furnlayout`);
|
|
const occupied = Array.from({ length: rows }, () => new Array(cols).fill(false));
|
|
const keepClear = buildKeepClear(walls);
|
|
const placements = [];
|
|
|
|
const inRoom = (id, x, y) => x >= 0 && y >= 0 && x < cols && y < rows && region[y][x] === id;
|
|
|
|
for (const room of rooms) {
|
|
if (!room.label || room.type === "stairs" || room.type === "elevator") continue;
|
|
const kinds = piecesFor(room.label);
|
|
|
|
for (const kind of kinds) {
|
|
const def = PIECES[kind];
|
|
if (!def) continue;
|
|
const [pw, ph] = def.footprint;
|
|
const orients = pw === ph ? [[pw, ph, 0]] : [[pw, ph, 0], [ph, pw, 90]];
|
|
const cands = [];
|
|
|
|
for (const [ow, oh, rot] of orients) {
|
|
for (let ty = room.y; ty <= room.y + room.h - oh; ty += 1) {
|
|
for (let tx = room.x; tx <= room.x + room.w - ow; tx += 1) {
|
|
let ok = true;
|
|
for (let dy = 0; dy < oh && ok; dy += 1) {
|
|
for (let dx = 0; dx < ow && ok; dx += 1) {
|
|
const cx = tx + dx, cy = ty + dy;
|
|
if (!inRoom(room.id, cx, cy) || occupied[cy][cx] || keepClear.has(`${cx},${cy}`)) ok = false;
|
|
}
|
|
}
|
|
if (!ok) continue;
|
|
let wallScore = 0;
|
|
for (let dx = 0; dx < ow; dx += 1) {
|
|
if (!inRoom(room.id, tx + dx, ty - 1)) wallScore += 1;
|
|
if (!inRoom(room.id, tx + dx, ty + oh)) wallScore += 1;
|
|
}
|
|
for (let dy = 0; dy < oh; dy += 1) {
|
|
if (!inRoom(room.id, tx - 1, ty + dy)) wallScore += 1;
|
|
if (!inRoom(room.id, tx + ow, ty + dy)) wallScore += 1;
|
|
}
|
|
cands.push({ tx, ty, ow, oh, rot, wallScore });
|
|
}
|
|
}
|
|
}
|
|
if (!cands.length) continue;
|
|
|
|
// Wall pieces want maximum wall contact; free pieces prefer open centre.
|
|
const scores = cands.map((c) => c.wallScore);
|
|
const target = def.wall ? Math.max(...scores) : Math.min(...scores);
|
|
const pool = cands.filter((c) => c.wallScore === target);
|
|
const pick = pool[Math.floor(rng.float() * pool.length)];
|
|
|
|
for (let dy = 0; dy < pick.oh; dy += 1) {
|
|
for (let dx = 0; dx < pick.ow; dx += 1) occupied[pick.ty + dy][pick.tx + dx] = true;
|
|
}
|
|
placements.push({
|
|
kind, subtype, rotation: pick.rot,
|
|
wpx: pw * gs, hpx: ph * gs,
|
|
cx: (pick.tx + pick.ow / 2) * gs,
|
|
cy: (pick.ty + pick.oh / 2) * gs
|
|
});
|
|
}
|
|
}
|
|
return placements;
|
|
}
|