/** * Outdoor biome renderer: grass/sand/snow base, water with ripples, rock * outcrops, layered tree canopies with shadows. Pure + deterministic. */ import { Rng } from "./rng.mjs"; const BIOME = { grassland: { ground: "#5b7a3a", groundAlt: "#6b8a44", tuft: "#46602c", patch: "#52722f", water: "#3f6f86", waterHiSafe: "#5a8caa", waterEdge: "#2c5468", rock: "#7d7a73", rockHi: "#9a968d", rockShadow: "#4a473f", tree: "#2f5526", treeHi: "#3f6e30", treeShadow: "#1d3318", grid: "#3c5226", label: "#f3f7e6" }, desert: { ground: "#c9a96a", groundAlt: "#d4b676", tuft: "#b3954f", patch: "#bf9f5d", water: "#4f8f9e", waterHiSafe: "#74b3bf", waterEdge: "#386b78", rock: "#a98f63", rockHi: "#c2a877", rockShadow: "#6e5a39", tree: "#5a6e3a", treeHi: "#6f8347", treeShadow: "#3a4825", grid: "#9b8048", label: "#3a2c12" }, snow: { ground: "#dfe6ec", groundAlt: "#eef3f7", tuft: "#c2ccd4", patch: "#d2dbe2", water: "#6f97ad", waterHiSafe: "#9bbdcd", waterEdge: "#557e94", rock: "#8b9099", rockHi: "#a7adb6", rockShadow: "#565b63", tree: "#3c5847", treeHi: "#4e6f59", treeShadow: "#26382d", grid: "#aebac4", label: "#2a323a" }, badlands: { ground: "#9c6a44", groundAlt: "#a9754d", tuft: "#824f30", patch: "#90613b", water: "#5a7d6e", waterHiSafe: "#7ea293", waterEdge: "#415d51", rock: "#8a5a3c", rockHi: "#a87149", rockShadow: "#5a3824", tree: "#6a6232", treeHi: "#7e763f", treeShadow: "#453f1f", grid: "#6e4628", label: "#f6e9da" } }; const PX = (v, gs) => +(v * gs).toFixed(1); function pathFor(pts, gs) { let d = `M${PX(pts[0].x, gs)},${PX(pts[0].y, gs)}`; for (let i = 1; i < pts.length; i += 1) d += `L${PX(pts[i].x, gs)},${PX(pts[i].y, gs)}`; return d + "Z"; } export function renderOutdoorSVG(args) { const m = args.model; const gs = args.gridSize ?? 70; const t = BIOME[m.biome] ?? BIOME.grassland; const showGrid = args.showGrid !== false; const showLabels = args.showLabels !== false; const rng = new Rng(`${args.seed ?? "field"}::outrender`); const W = m.cols * gs, H = m.rows * gs; const out = []; out.push(``); out.push(`` + `` + `` + ``); // Ground base + soft colour patches + tufts. out.push(``); out.push(groundPatches(m, gs, t, rng)); if (showGrid) out.push(grid(m, gs, t.grid)); out.push(tufts(m, gs, t, rng)); // Water. if (m.water) { out.push(``); out.push(ripples(m.water, gs, t, rng)); } // Rocks. for (const rk of m.rocks) { out.push(``); out.push(``); out.push(``); // a couple of highlight facets out.push(``); } // Trees (sorted by y for overlap depth). const trees = m.trees.slice().sort((a, b) => a.y - b.y); out.push(``); for (const tr of trees) { const cx = PX(tr.x, gs), cy = PX(tr.y, gs), r = tr.r * gs; out.push(``); out.push(``); } out.push(``); // Feature labels. if (showLabels && m.features?.length) { out.push(``); for (const f of m.features) { out.push(`${escapeXml(f.label)}`); } out.push(``); } out.push(``); return out.join(""); } function groundPatches(m, gs, t, 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; const rx = (0.8 + rng.float() * 2.2) * gs, ry = rx * (0.6 + rng.float() * 0.5); const col = rng.float() < 0.5 ? t.groundAlt : t.patch; out.push(``); } out.push(``); return out.join(""); } function tufts(m, gs, t, rng) { const out = [``]; const n = Math.round(m.cols * m.rows * 1.1); for (let i = 0; i < n; i += 1) { const x = PX(rng.float() * m.cols, gs), y = PX(rng.float() * m.rows, gs); const h = gs * (0.06 + rng.float() * 0.08); out.push(``); } out.push(``); return out.join(""); } function ripples(water, gs, t, rng) { const out = [``]; const n = 8; for (let i = 0; i < n; i += 1) { const x = water.cx + (rng.float() - 0.5) * water.r * 1.2; const y = water.cy + (rng.float() - 0.5) * water.r * 1.2; const w = gs * (0.3 + rng.float() * 0.5); out.push(``); } 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(``); out.push(``); return out.join(""); } function escapeXml(str) { return String(str).replace(/[<>&'"]/g, (c) => ({ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ }[c])); }