/**
* Settlement renderer — detailed top-down village / city-block tactical maps.
* Rich, colourful baked-in scenery (cars, trees, lamps, crosswalks, market
* stalls, wells, flower beds…). Buildings are rooftop blocks; streets/grass are
* the open token-scaled play space. Pure + deterministic.
*/
import { Rng } from "./rng.mjs";
const CITY = {
asphalt: "#33373d", asphaltAlt: "#2c3035", sidewalk: "#b3bac2",
curb: "#7f868e", laneYellow: "#e8c64a", laneWhite: "#d7dde3",
grid: "#3c4954", roofTrim: "#23262b", park: "#5d7c3c", parkPath: "#b9a988", label: "#eef3f7"
};
const VILLAGE = {
grass: "#5f7e3e", grassAlt: "#6e8d48", tuft: "#47602c", road: "#9c814f", roadEdge: "#6f5a34",
cobble: "#8c7448", water: "#3f6f86", waterEdge: "#2c5468", square: "#b7a079",
garden: ["#6b8a3a", "#8a7a3a", "#5a7b34"], gardenRow: "#42501f", fence: "#6a4a2c",
grid: "#42562a", roofTrim: "#2a1d12", label: "#f3f7e6"
};
const TREE_GREENS = [["#2f5526", "#3f6e30"], ["#355e2a", "#4a7a38"], ["#2a4a22", "#386026"]];
const PX = (v, gs) => +(v * gs).toFixed(1);
export function renderSettlementSVG(args) {
const m = args.model;
const gs = args.gridSize ?? 100;
const showGrid = args.showGrid !== false;
const showLabels = args.showLabels !== false;
const rng = new Rng(`${args.seed ?? "town"}::settle`);
const W = m.cols * gs, H = m.rows * gs;
const out = [``);
return out.join("");
}
// ============================ CITY ========================================
function renderCity(out, m, gs, W, H, rng) {
out.push(``);
out.push(speckle(W, H, gs, "#000", 0.07, 4, rng));
// crosswalks (under sidewalks/markings)
out.push(``);
for (const cw of m.crosswalks) out.push(crosswalk(cw, gs));
out.push(``);
// lane markings: solid white edge + dashed yellow centre
out.push(``);
for (const ln of m.roadLines) out.push(laneLine(ln, gs, W, H, 0.55));
for (const ln of m.roadLines) out.push(laneLine(ln, gs, W, H, -0.55));
out.push(``);
out.push(``);
for (const ln of m.roadLines) out.push(laneLine(ln, gs, W, H, 0));
out.push(``);
// sidewalks
for (const blk of m.blocks) {
const x = PX(blk.x, gs), y = PX(blk.y, gs), w = PX(blk.w, gs), h = PX(blk.h, gs);
out.push(``);
}
// sidewalk paving grid (subtle)
out.push(``);
for (const blk of m.blocks) for (let i = 1; i < blk.w; i += 1) out.push(``);
out.push(``);
// parks
for (const p of m.parks) out.push(park(p, gs, rng));
// cars (under buildings so buildings cast over them slightly; fine on road)
out.push(``);
for (const c of m.cars) out.push(car(c, gs));
out.push(``);
// road props that sit on asphalt (manhole/patch)
for (const p of m.props) if (p.kind === "manhole" || p.kind === "patch") out.push(prop(p, gs, rng));
// buildings
for (const b of m.buildings) cityRoof(out, b, gs, rng);
// sidewalk props (above buildings' shadow, on sidewalk)
for (const p of m.props) if (!(p.kind === "manhole" || p.kind === "patch")) out.push(prop(p, gs, rng));
}
function crosswalk(cw, gs) {
let s = "";
if (cw.vertical) {
for (let i = 0; i < cw.w; i += 1) if (i % 2 === 0) s += ``;
} else {
for (let i = 0; i < cw.h; i += 1) if (i % 2 === 0) s += ``;
}
return s;
}
function laneLine(ln, gs, W, H, off) {
if (ln.vertical) { const x = PX(ln.at + off, gs); return ``; }
const y = PX(ln.at + off, gs); return ``;
}
function cityRoof(out, b, gs, rng) {
const x = PX(b.x, gs), y = PX(b.y, gs), w = PX(b.w, gs), h = PX(b.h, gs), off = gs * 0.1;
out.push(``);
out.push(``);
// inner trim
out.push(``);
const cx = b.x + b.w / 2, cy = b.y + b.h / 2;
if (b.roofStyle === 1 && b.w >= 3 && b.h >= 3) { // rooftop garden
out.push(``);
} else if (b.roofStyle === 2) { // water tank
out.push(``);
} else { // AC units / skylights
const units = Math.min(4, Math.max(1, Math.floor(b.w * b.h / 5)));
for (let i = 0; i < units; i += 1) {
const ux = PX(b.x + 0.3 + rng.float() * (b.w - 0.8), gs), uy = PX(b.y + 0.3 + rng.float() * (b.h - 0.8), gs), us = gs * (0.22 + rng.float() * 0.16);
out.push(``);
}
}
}
function car(c, gs) {
const long = gs * 1.7, wide = gs * 0.8;
const w = c.horizontal ? long : wide, h = c.horizontal ? wide : long;
const x = PX(c.x, gs) - w / 2, y = PX(c.y, gs) - h / 2;
const wx = c.horizontal ? w * 0.32 : w, wy = c.horizontal ? h : h * 0.32;
return `` +
`` +
`` +
`` +
``;
}
function park(p, gs, rng) {
const x = PX(p.x, gs), y = PX(p.y, gs), w = PX(p.w, gs), h = PX(p.h, gs);
let s = ``;
// diagonal path
s += ``;
s += ``;
const n = Math.max(2, Math.floor(p.w * p.h / 4));
for (let i = 0; i < n; i += 1) s += treeSVG(p.x + 0.5 + rng.float() * (p.w - 1), p.y + 0.5 + rng.float() * (p.h - 1), 0.5 + rng.float() * 0.3, 0, gs);
return s;
}
function prop(p, gs, rng) {
const cx = PX(p.x, gs), cy = PX(p.y, gs);
switch (p.kind) {
case "tree": return treeSVG(p.x, p.y, 0.42 + (p.x % 1) * 0.001 + 0.16, 1, gs);
case "lamp": return ``;
case "bench": return ``;
case "hydrant": return ``;
case "planter": return ``;
case "manhole": return ``;
case "patch": return ``;
// village props
case "well": return ``;
case "fountain": return ``;
case "stall": return ``;
case "cart": return ``;
case "barrel": return ``;
case "haybale": return ``;
case "bush": return ``;
case "flowers": return ``;
default: return "";
}
}
// ============================ VILLAGE =====================================
function renderVillage(out, m, gs, W, H, rng) {
out.push(``);
out.push(grassPatches(m, gs, rng));
out.push(grassTufts(m, gs, rng));
if (m.road?.length) {
const d = "M" + m.road.map((p) => `${PX(p.x, gs)},${PX(p.y, gs)}`).join(" L");
const wpx = (m.roadHalf * 2 * gs);
out.push(``);
out.push(``);
out.push(``);
}
if (m.square) out.push(``);
if (m.water) {
const d = "M" + m.water.loop.map((p) => `${PX(p.x, gs)},${PX(p.y, gs)}`).join(" L") + "Z";
out.push(``);
}
for (const p of m.plots) {
const x = PX(p.x, gs), y = PX(p.y, gs), w = PX(p.w, gs), h = PX(p.h, gs);
out.push(``);
for (let r = 0; r < p.h * 2; r += 1) out.push(``);
}
// ground props that sit under buildings (well/fountain/stall/cart on square/grass)
for (const p of m.props) if (p.kind !== "bush" && p.kind !== "flowers") out.push(prop(p, gs, rng));
for (const b of m.buildings) villageRoof(out, b, gs);
// trees + small props above buildings
const trees = m.trees.slice().sort((a, b) => a.y - b.y);
out.push(``);
for (const t of trees) out.push(treeSVG(t.x, t.y, t.r, t.green ?? 0, gs));
out.push(``);
for (const p of m.props) if (p.kind === "bush" || p.kind === "flowers") out.push(prop(p, gs, rng));
}
function villageRoof(out, b, gs) {
const x = PX(b.x, gs), y = PX(b.y, gs), w = PX(b.w, gs), h = PX(b.h, gs), off = gs * 0.1;
out.push(``);
out.push(``);
// ridge line + thatch/tile rows
if (b.w >= b.h) {
const my = y + h / 2;
out.push(``);
for (let i = 1; i < b.w; i += 1) out.push(``);
} else {
const mx = x + w / 2;
out.push(``);
for (let i = 1; i < b.h; i += 1) out.push(``);
}
if (b.chimney) out.push(``);
}
// ============================ shared ======================================
function treeSVG(x, y, r, greenIdx, gs) {
const cx = PX(x, gs), cy = PX(y, gs), rr = r * gs, g = TREE_GREENS[greenIdx % TREE_GREENS.length];
return `` +
`` +
``;
}
function speckle(W, H, gs, color, opacity, density, rng) {
const out = [``];
const n = Math.round((W * H) / (gs * gs) / density);
for (let i = 0; i < n; i += 1) { const x = rng.float() * W, y = rng.float() * H, l = gs * (0.2 + rng.float() * 0.4), a = rng.float() * Math.PI; out.push(``); }
return out.join("") + ``;
}
function grassPatches(m, gs, rng) {
const out = [``];
const n = Math.round(m.cols * m.rows / 14);
for (let i = 0; i < n; i += 1) { const x = rng.float() * m.cols, y = rng.float() * m.rows, rx = (0.8 + rng.float() * 2) * gs; out.push(``); }
return out.join("") + ``;
}
function grassTufts(m, gs, rng) {
const out = [``];
const n = Math.round(m.cols * m.rows * 0.8);
for (let i = 0; i < n; i += 1) { const x = PX(rng.float() * m.cols, gs), y = PX(rng.float() * m.rows, gs), hh = gs * (0.05 + rng.float() * 0.07); out.push(``); }
return out.join("") + ``;
}
function grid(m, gs, color) {
const out = [``];
for (let x = 0; x <= m.cols; x += 1) out.push(``);
for (let y = 0; y <= m.rows; y += 1) out.push(``);
return out.join("") + ``;
}
function esc(s) { return String(s).replace(/[<>&'"]/g, (c) => ({ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ }[c])); }