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>
83 lines
3.6 KiB
JavaScript
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(),
|
|
};
|
|
}
|