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>
212 lines
10 KiB
JavaScript
212 lines
10 KiB
JavaScript
// PROCITY Lane B — fixture_plan.js
|
||
// A hand-authored CityPlan (CITY_SPEC schema v1) so the streetscape is buildable *before*
|
||
// Lane A ships generatePlan(). Everything here is deterministic layout math (no Math.random,
|
||
// no THREE) — pure JSON-serialisable data. When Lane A lands, index.html swaps in
|
||
// generatePlan(seed) and this file becomes the fallback + schema reference.
|
||
//
|
||
// LAYOUT (metres, +Y up, ground = XZ, origin = centre of the main square):
|
||
//
|
||
// north (backstreets: houses up the cross street)
|
||
// │ cross st (X=0)
|
||
// NW shops (Z=+11, face −Z) ──────┼────── NE shops
|
||
// ═══════════════ MAIN STREET (Z=0, east-west) ═══════════════
|
||
// SW shops (Z=−11, face +Z) ──────┼────── SE shops
|
||
//
|
||
// Orientation convention (documented once, obeyed by buildings.js):
|
||
// A lot is an oriented rectangle centred (x,z), footprint w (frontage) × d (depth).
|
||
// `ry` rotates the building about +Y. At ry=0 the shopfront outward-normal is +Z.
|
||
// Rotation by ry maps +Z → (sin ry, 0, cos ry), so:
|
||
// front faces +Z → ry = 0 (south side of an E-W street)
|
||
// front faces −Z → ry = π (north side of an E-W street)
|
||
// front faces +X → ry = +π/2 (west side of a N-S street)
|
||
// front faces −X → ry = −π/2 (east side of a N-S street)
|
||
|
||
import { rng, pick, irange } from '../core/prng.js';
|
||
|
||
// ── Shop-type registry (Lane A owns web/js/core/registry.js; it hasn't landed yet, so Lane B
|
||
// carries the slice it needs. facade pools are the CITY_SPEC table. Lane F reconciles.) ──
|
||
export const SHOP_REGISTRY = {
|
||
record: { facades: ['timber-teal', 'arcade-tile', 'djsim-record'], signBg: '#1f2e2b', signFg: '#e8d9a0' },
|
||
opshop: { facades: ['weatherboard', 'fibro-blue', 'djsim-opshop'], signBg: '#2c2820', signFg: '#f0e2c0' },
|
||
toy: { facades: ['stucco-pink', 'deco-pastel'], signBg: '#7a1f5a', signFg: '#ffe6f2' },
|
||
book: { facades: ['federation', 'sandstone'], signBg: '#2a231a', signFg: '#f3e6c4' },
|
||
video: { facades: ['stripmall', 'djsim-video'], signBg: '#141a2c', signFg: '#8fb7ff' },
|
||
pawn: { facades: ['besser', 'grimy', 'djsim-pawn'], signBg: '#f5c400', signFg: '#c1121f' },
|
||
milkbar: { facades: ['djsim-milkbar', 'brickveneer'], signBg: '#b21f1f', signFg: '#fff3d6' },
|
||
dept: { facades: ['terrazzo', 'djsim-dept'], signBg: '#20313a', signFg: '#ffd75e' },
|
||
stall: { facades: ['market'], signBg: '#2c2820', signFg: '#f0e2c0' },
|
||
};
|
||
export const SHOP_TYPES = Object.keys(SHOP_REGISTRY);
|
||
|
||
// Parody-name word banks (kept small; deterministic pick per shop). 90s-AU flavour.
|
||
const NAME_BANK = {
|
||
record: { a: ['Groove', 'Wax', 'Vinyl', 'Spin', 'Needle', 'Rotary'], b: ['Junction', 'Merchants', 'Traders', 'Exchange', 'Bin'] },
|
||
opshop: { a: ['Vinnies', 'Salvos', 'Second', 'Thrifty', 'Endeavour', 'Lifeline'], b: ['Chance', 'Op Shop', 'Finds', 'Bazaar', 'Nook'] },
|
||
toy: { a: ['Toyworld', 'Kiddie', 'Wonder', 'Bright', 'Jolly'], b: ['Toys', 'Playtime', 'Corner', 'Emporium'] },
|
||
book: { a: ['Dog-Eared', 'Second Story', 'Turning', 'Foxed', 'Marginalia'], b: ['Books', 'Book Barn', 'Reads', 'Pages'] },
|
||
video: { a: ['Blockheads', 'Video', 'Reel', 'Nightowl', 'Civic'], b: ['Video', 'Rental', 'Ezy Hire', 'Movies'] },
|
||
pawn: { a: ['Cash', 'Quick', 'Second Chance', 'City', 'Honest'], b: ['Converters', 'Pawn', 'Loans', 'Traders'] },
|
||
milkbar: { a: ['Corner', 'Lucky', 'Rio', 'Astoria', 'Sunny'], b: ['Milk Bar', 'Deli', 'Cafe', 'Store'] },
|
||
dept: { a: ['Coles', 'Waltons', 'Grace Bros', 'Fossey’s', 'Venture'], b: ['Variety', 'Department', 'Emporium', ''] },
|
||
stall: { a: ['Trash & Treasure', 'Sunday', 'Boot', 'Market'], b: ['Stall', 'Table', 'Trestle'] },
|
||
};
|
||
|
||
function makeName(seed, type) {
|
||
const r = rng(seed, 'name', 0);
|
||
const bank = NAME_BANK[type] || NAME_BANK.opshop;
|
||
const a = pick(r, bank.a), b = pick(r, bank.b);
|
||
return b ? `${a} ${b}`.trim() : a;
|
||
}
|
||
|
||
// Deterministic hours: most open 9–5-ish, some late, a few closed-feeling.
|
||
function makeHours(seed) {
|
||
const r = rng(seed, 'hours', 0);
|
||
const open = irange(r, 7, 10);
|
||
const close = irange(r, 16, 21);
|
||
return [open, close];
|
||
}
|
||
|
||
// Facade skin for a shop, seeded from its type pool.
|
||
function pickFacade(seed, type) {
|
||
const r = rng(seed, 'facade', 0);
|
||
return pick(r, (SHOP_REGISTRY[type] || SHOP_REGISTRY.opshop).facades);
|
||
}
|
||
|
||
// ── Geometry constants for the fixture town ──
|
||
const W = 8; // terraced shop frontage width (m)
|
||
const D = 8; // shop depth (m)
|
||
const N_FRONT = 11; // north-side frontage plane Z (fronts face −Z)
|
||
const S_FRONT = -11; // south-side frontage plane Z (fronts face +Z)
|
||
const CROSS_W = -11; // cross-street west frontage plane X (fronts face +X)
|
||
const CROSS_E = 11; // cross-street east frontage plane X (fronts face −X)
|
||
|
||
// The terraced blocks. Four along the north frontage + two on the south near the plaza, spread so
|
||
// the main street spans ~6 chunks (streaming visibly kicks in walking end-to-end). Every archetype
|
||
// shows up. 8m shops; the plaza gap sits around the origin.
|
||
const BLOCKS = [
|
||
{ key: 'NW2', side: 'N', xs: [-140, -132, -124, -116], types: ['record', 'opshop', 'book', 'milkbar'] },
|
||
{ key: 'NW1', side: 'N', xs: [-52, -44, -36, -28], types: ['video', 'toy', 'pawn', 'record'] },
|
||
{ key: 'NE1', side: 'N', xs: [28, 36, 44, 52], types: ['opshop', 'book', 'milkbar', 'video'] },
|
||
{ key: 'NE2', side: 'N', xs: [116, 124, 132, 140], types: ['dept', 'toy', 'pawn', 'opshop'] },
|
||
{ key: 'SW', side: 'S', xs: [-52, -44, -36, -28], types: ['milkbar', 'record', 'opshop', 'book'] },
|
||
{ key: 'SE', side: 'S', xs: [28, 36, 44, 52], types: ['toy', 'video', 'pawn', 'record'] },
|
||
];
|
||
|
||
let _uid = 0;
|
||
const uid = (p) => `${p}${_uid++}`;
|
||
|
||
function buildShopLots(citySeed, lots, shops, blocks) {
|
||
_uid = 0;
|
||
for (const bd of BLOCKS) {
|
||
const north = bd.side === 'N';
|
||
const z = north ? N_FRONT + D / 2 : S_FRONT - D / 2;
|
||
const ry = north ? Math.PI : 0;
|
||
const frontEdge = north ? 'S' : 'N';
|
||
const xs = bd.xs, types = bd.types;
|
||
const minX = Math.min(...xs) - W / 2, maxX = Math.max(...xs) + W / 2;
|
||
const zNear = north ? N_FRONT : S_FRONT;
|
||
const zFar = north ? N_FRONT + D : S_FRONT - D;
|
||
const blockId = `block-${bd.key}`;
|
||
blocks.push({
|
||
id: blockId, district: 'main',
|
||
poly: [[minX, Math.min(zNear, zFar)], [maxX, Math.min(zNear, zFar)],
|
||
[maxX, Math.max(zNear, zFar)], [minX, Math.max(zNear, zFar)]],
|
||
});
|
||
xs.forEach((x, i) => {
|
||
const type = types[i % types.length];
|
||
const lotId = uid('lot-');
|
||
const isAnchor = type === 'dept';
|
||
const storeys = isAnchor ? 3 : (i % 3 === 0 ? 2 : 1);
|
||
lots.push({
|
||
id: lotId, block: blockId, x, z, w: W, d: D, ry,
|
||
frontEdge, use: isAnchor ? 'anchor' : 'shop',
|
||
});
|
||
const seed = citySeed + _uid * 2654435761 % 0xffffffff;
|
||
const s = (seed >>> 0);
|
||
shops.push({
|
||
id: uid('shop-'), lot: lotId, type,
|
||
name: makeName(s, type), sign: makeName(s, type),
|
||
seed: s, facadeSkin: pickFacade(s, type), storeys, hours: makeHours(s),
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
function buildHouses(citySeed, lots) {
|
||
// Backstreets: houses lining the cross street north of the square.
|
||
const specs = [
|
||
{ x: CROSS_W - 5, z: 36, ry: Math.PI / 2, frontEdge: 'E' },
|
||
{ x: CROSS_W - 5, z: 60, ry: Math.PI / 2, frontEdge: 'E' },
|
||
{ x: CROSS_E + 5, z: 48, ry: -Math.PI / 2, frontEdge: 'W' },
|
||
{ x: CROSS_E + 5, z: 72, ry: -Math.PI / 2, frontEdge: 'W' },
|
||
];
|
||
specs.forEach((sp, i) => {
|
||
lots.push({
|
||
id: `lot-house-${i}`, block: 'block-back', x: sp.x, z: sp.z,
|
||
w: 9, d: 10, ry: sp.ry, frontEdge: sp.frontEdge, use: 'house',
|
||
});
|
||
});
|
||
}
|
||
|
||
function buildStalls(citySeed, lots, shops) {
|
||
// Two market stalls in the square (brickpave plaza around the origin).
|
||
const specs = [{ x: -9, z: 6, ry: 0 }, { x: 9, z: -6, ry: Math.PI }];
|
||
specs.forEach((sp, i) => {
|
||
const lotId = `lot-stall-${i}`;
|
||
lots.push({ id: lotId, block: 'block-square', x: sp.x, z: sp.z, w: 4, d: 3, ry: sp.ry, use: 'stall', frontEdge: 'S' });
|
||
const s = (citySeed + 7777 + i * 131) >>> 0;
|
||
shops.push({
|
||
id: `shop-stall-${i}`, lot: lotId, type: 'stall',
|
||
name: makeName(s, 'stall'), sign: makeName(s, 'stall'),
|
||
seed: s, facadeSkin: 'market', storeys: 1, hours: [8, 14],
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* fixturePlan(citySeed) → CityPlan (schema v1). Deterministic in citySeed.
|
||
*/
|
||
export function fixturePlan(citySeed = 20261990) {
|
||
citySeed = citySeed >>> 0;
|
||
const lots = [], shops = [], blocks = [];
|
||
buildShopLots(citySeed, lots, shops, blocks);
|
||
buildHouses(citySeed, lots);
|
||
buildStalls(citySeed, lots, shops);
|
||
|
||
const r = rng(citySeed, 'city', 0);
|
||
|
||
const plan = {
|
||
version: 1,
|
||
citySeed,
|
||
name: pick(r, ['Wollamby', 'Dunroamin', 'Barretta', 'Coolabah', 'Mungindi', 'Yarraville Junction']),
|
||
size: { w: 1024, d: 1024 },
|
||
districts: [
|
||
{ id: 'main', kind: 'mainstreet', cx: 0, cz: 0 },
|
||
{ id: 'square', kind: 'market', cx: 0, cz: 0 },
|
||
{ id: 'back', kind: 'backstreets', cx: 0, cz: 55 },
|
||
],
|
||
streets: {
|
||
nodes: [
|
||
{ id: 'n0', x: -155, z: 0 }, // west end, main
|
||
{ id: 'n1', x: 0, z: 0 }, // central intersection / square
|
||
{ id: 'n2', x: 155, z: 0 }, // east end, main
|
||
{ id: 'n3', x: 0, z: 95 }, // north end, cross street
|
||
],
|
||
edges: [
|
||
{ id: 'e0', a: 'n0', b: 'n1', width: 14, kind: 'main' },
|
||
{ id: 'e1', a: 'n1', b: 'n2', width: 14, kind: 'main' },
|
||
{ id: 'e2', a: 'n1', b: 'n3', width: 8, kind: 'side' },
|
||
],
|
||
},
|
||
blocks,
|
||
lots,
|
||
shops,
|
||
// Which sky this town gets (Lane B lighting reads this; seeded so a town's weather is stable).
|
||
sky: pick(r, ['golden-arvo', 'spring-puffy', 'endless-blue', 'high-cirrus', 'summer-storm', 'outback-dusk']),
|
||
};
|
||
return plan;
|
||
}
|
||
|
||
// Default fixture used by index.html when citygen is absent.
|
||
export default fixturePlan;
|