Files
mapwright/module/lib/furniture-layout.mjs
T
slaguru666andClaude Opus 4.8 100e514c4d Mapwright v0.5.0 — procedural battle map generator for Foundry VTT
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>
2026-06-17 15:05:25 +01:00

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;
}