/** * Organic cave renderer: smoothed contour loops -> SVG. * * The look: rocky floor inside flowing walls, thick rock walls with a cast * shadow + inner highlight, scattered floor debris, optional grid clipped to * floor. Pure + deterministic. */ import { Rng } from "./rng.mjs"; const THEMES = { cave: { outside: "#0f0d0b", floorBase: "#7d7164", floorSpeck: "#8d8173", floorEdge: "#5e5349", wall: "#1d1813", wallHi: "#5a5042", grid: "#3a342e", label: "#f3e9d4", speckLight: "#9a8e80" }, crypt: { outside: "#0c0e12", floorBase: "#6a6f7c", floorSpeck: "#7a8090", floorEdge: "#50545f", wall: "#171a20", wallHi: "#566173", grid: "#33363d", label: "#eef3fb", speckLight: "#8990a0" }, sewer: { outside: "#0c100e", floorBase: "#61705f", floorSpeck: "#6e7e69", floorEdge: "#4a564a", wall: "#161d18", wallHi: "#566355", grid: "#2f3733", label: "#e7f5ec", speckLight: "#7e8e78" } }; const PX = (v, gs) => +(v * gs).toFixed(1); export function renderCaveSVG(args) { const model = args.model; const gs = args.gridSize ?? 70; const theme = THEMES[args.theme] ?? THEMES.cave; const showGrid = args.showGrid !== false; const showLabels = args.showLabels !== false; const rng = new Rng(`${args.seed ?? "cave"}::caverender`); const W = model.cols * gs; const H = model.rows * gs; const out = []; const pathFor = (pts) => { 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"; }; const floorPath = model.loops.map((lp) => pathFor(lp.smooth)).join(" "); out.push(``); out.push(``); out.push( `` + `` ); out.push(``); out.push(``); // Rock surround. out.push(``); out.push(rockTexture(W, H, theme, gs, rng)); // Floor fill (evenodd handles pillars/holes). out.push(``); // Floor detail clipped to floor. out.push(``); out.push(floorDebris(model, gs, theme, rng)); if (showGrid) out.push(caveGrid(model, gs, theme.grid)); // inner shading ring just inside the walls out.push(``); out.push(``); // Walls: cast shadow, body, inner highlight. const wallW = (gs * 0.26).toFixed(1); out.push(``); out.push(``); out.push(``); out.push(``); // Feature labels. if (showLabels && model.features?.length) { out.push(``); for (const f of model.features) { const fs = Math.max(10, gs * 0.22); out.push(``); out.push(`${escapeXml(f.label)}`); } out.push(``); } out.push(``); return out.join(""); } function rockTexture(W, H, theme, gs, rng) { // Sparse rock cracks/blotches outside, very subtle. const out = [``]; const n = Math.round((W * H) / (gs * gs) / 6); for (let i = 0; i < n; i += 1) { const x = rng.float() * W, y = rng.float() * H; const len = gs * (0.3 + rng.float() * 0.5); const ang = rng.float() * Math.PI; out.push(``); } out.push(``); return out.join(""); } function floorDebris(model, gs, theme, rng) { const out = [``]; const n = Math.round(model.cols * model.rows * 0.6); for (let i = 0; i < n; i += 1) { const x = rng.float() * model.cols, y = rng.float() * model.rows; const r = gs * (0.02 + rng.float() * 0.06); const col = rng.float() < 0.5 ? theme.floorSpeck : theme.speckLight; out.push(``); } out.push(``); return out.join(""); } function caveGrid(model, gs, color) { const out = [``]; for (let x = 0; x <= model.cols; x += 1) out.push(``); for (let y = 0; y <= model.rows; y += 1) out.push(``); out.push(``); return out.join(""); } function escapeXml(str) { return String(str).replace(/[<>&'"]/g, (c) => ({ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ }[c])); }