PROCITY/web/js/world/minimap.js
m3ultra 5313402e43 Lane B (Streetscape): chunk-streamed walkable town shell
First-person shell: chunk streaming, instanced buildings + facade texture atlas
(22 facade materials -> 1; 5 skin materials city-wide), signs, awnings, furniture,
day/night lighting, minimap, pixel-accurate collision (incl. arbitrary-angle lots),
door raycast -> procity:enterShop, DBG harness, all-fallback mode. Renders Lane A's
full generated town (seed 20261990 'Boolarra Heads', 493 shops); worst continuous-
walk view ~261 draws (<=300 gate). 6 adversarial-review bugs fixed (collision sign,
house doors, awning skin, sign atlas, furniture seed hash, shot-mode restore).

index.html and skins.js include Lane F's inline integration seam edits (marked
'[Lane F integration]'): interior/keeper/citizen wiring + facade filename fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 14:28:47 +10:00

83 lines
3.6 KiB
JavaScript

// PROCITY Lane B — minimap.js
// The `map` mode: a 2D top-down town directory drawn from the CityPlan (streets + lots coloured by
// shop type + a live player dot/heading). This is the shell's own lightweight map; Lane A's
// map.html debug view grows into the richer directory later (Lane F reconciles).
const TYPE_COLOR = {
record: '#8a5aa8', opshop: '#8a7a5a', toy: '#c86aa0', book: '#7a6a3a', video: '#4a6ab0',
pawn: '#c4a028', milkbar: '#c4863a', dept: '#5a6a7a', stall: '#5a8a6a',
house: '#6b5a48', anchor: '#5a6a7a',
};
export function createMinimap(plan) {
const wrap = document.createElement('div');
wrap.id = 'pc-map';
wrap.style.cssText = 'position:fixed;inset:0;z-index:20;display:none;background:#141712;'
+ 'align-items:center;justify-content:center;flex-direction:column;font:13px -apple-system,sans-serif;color:#e8e0d0';
const canvas = document.createElement('canvas');
canvas.width = 720; canvas.height = 720;
canvas.style.cssText = 'background:#1b1e17;border-radius:10px;max-width:92vw;max-height:80vh;box-shadow:0 8px 40px #000';
const cap = document.createElement('div');
cap.style.cssText = 'margin-top:12px;opacity:.8';
cap.textContent = `${plan.name} — seed ${plan.citySeed} · press M to close`;
wrap.append(canvas, cap);
document.body.appendChild(wrap);
const ctx = canvas.getContext('2d');
// world→canvas transform (fit plan extent with margin)
let minX = -60, maxX = 60, minZ = -60, maxZ = 60;
for (const n of plan.streets.nodes) { minX = Math.min(minX, n.x); maxX = Math.max(maxX, n.x); minZ = Math.min(minZ, n.z); maxZ = Math.max(maxZ, n.z); }
for (const l of plan.lots) { minX = Math.min(minX, l.x); maxX = Math.max(maxX, l.x); minZ = Math.min(minZ, l.z); maxZ = Math.max(maxZ, l.z); }
const pad = 24, W = canvas.width, H = canvas.height;
const span = Math.max(maxX - minX, maxZ - minZ) + 40;
const cxw = (minX + maxX) / 2, czw = (minZ + maxZ) / 2;
const sc = (W - pad * 2) / span;
const tx = (x) => W / 2 + (x - cxw) * sc;
const tz = (z) => H / 2 + (z - czw) * sc;
const nodes = new Map(plan.streets.nodes.map((n) => [n.id, n]));
const shopByLot = new Map(plan.shops.map((s) => [s.lot, s]));
function drawStatic() {
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#1b1e17'; ctx.fillRect(0, 0, W, H);
// streets
for (const e of plan.streets.edges) {
const a = nodes.get(e.a), b = nodes.get(e.b);
ctx.strokeStyle = '#3a3f34'; ctx.lineWidth = Math.max(3, e.width * sc);
ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(tx(a.x), tz(a.z)); ctx.lineTo(tx(b.x), tz(b.z)); ctx.stroke();
}
// lots
for (const l of plan.lots) {
const s = shopByLot.get(l.id);
const col = TYPE_COLOR[s ? s.type : l.use] || '#666';
ctx.save();
ctx.translate(tx(l.x), tz(l.z)); ctx.rotate(-(l.ry || 0));
ctx.fillStyle = col;
ctx.fillRect(-l.w * sc / 2, -l.d * sc / 2, l.w * sc, l.d * sc);
// front-edge tick (toward the street)
ctx.fillStyle = 'rgba(255,255,255,.5)';
ctx.fillRect(-l.w * sc / 2, l.d * sc / 2 - 2, l.w * sc, 2);
ctx.restore();
}
}
function draw(playerPos, fwd) {
drawStatic();
const x = tx(playerPos.x), z = tz(playerPos.z);
// heading (world +X→right, +Z→down on the map)
ctx.strokeStyle = '#ffd75e'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(x, z);
ctx.lineTo(x + fwd.x * 16, z + fwd.z * 16); ctx.stroke();
ctx.fillStyle = '#ffd75e';
ctx.beginPath(); ctx.arc(x, z, 6, 0, Math.PI * 2); ctx.fill();
}
return {
draw,
setVisible: (v) => { wrap.style.display = v ? 'flex' : 'none'; },
dispose: () => wrap.remove(),
};
}