PROCITY/web/js/world/fixture_plan.js
m3ultra 48033f48b1 Lane B R41 §41.5: the credits surface — and the game had never shown the OSM attribution it owes
THE CREDITS SURFACE. New web/assets/credits.json (schema procity-credits/1, the data file
every lane appends to — documented in LANE_B_NOTES §41 with a paste-ready row) + credits.js
(the panel, DOM only) + a HUD link. F2 or the link. Zero draws, zero boot fetches (0 before
open, exactly 1 after), zero fetches under ?noassets=1.

Seeding it found a LIVE OBLIGATION the game has never displayed: the 23 real-town caches are
OpenStreetMap data under ODbL 1.0, whose own source note says keep the credit visible. It now
rides the link and is context-aware — on an OSM boot the HUD reads '(c) OpenStreetMap
contributors · credits (F2)'; on the synthetic town, where no OSM data is on screen, it does
not claim it. three.js r175 MIT credited too. 14 entries seeded, all read off the repo.

Field law: licence 'unverified' is legal and a guess is not. Two gaps filed to E as amber
rather than invented: the 19-GLB base ped roster has NO per-asset licence record anywhere in
this repo (and CITY_SPEC's declared zone includes CC-BY and CGTrader RF — a credit may be
owed today), and the 18 house-library fittings still carry AUDIT.md's 38-round-old blanket
caveat despite shipping since R3.

A's D3 CLOSED. Re-measured over the shipped corpus at the golden seed: 27 towns, 1,838 shops,
81 venue lots, 81 of 81 taking the op-shop signage fallback — exactly 3 per town, in every
town. A's number reproduces to the digit. Bite drifted :344 -> :465 -> :483, hence a named
resolver: signStyleFor() tries B's nine palettes, then A's registry (gilt/stencil/club,
carried since R13), then op-shop as genuine last resort. Venue treatment is venue-gated by
construction. Deliberately NOT done: repainting the other nine types onto A's palettes
(record would go muted-teal -> hot-pink neon) — an art call for John, filed.

RULING 4 RESPECTED, NOTHING SPENT: frozen town, 136 station-yaw samples per arm, 544/544
identical on BOTH counters (draws and tris), 0 console errors. Pin method reproduces R40
exactly — default 282 noon / 291 NIGHT, classic 269 byte-exact. The nine-draw night margin
is intact. Classic purity: HUD DOM 2354:8 both arms, fetch surface identical, no
#pc-creditlink / overlay / F2 listener / PROCITY.credits.

Filed to F: three classic bookmarks are not byte-reproducible across boots even at HEAD
(2/5 HEAD-vs-HEAD) — a font race in the sign atlas. A screenshot golden on those cannot be
a byte-hash.

selfcheck ALL GREEN 157,647/157,647, fingerprint 0x5f76e76 unmoved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 18:10:39 +10:00

223 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 — THE FIXTURE'S TABLE, and Lane B's shipped sign palettes ─────────────────
// [Lane B R41 §41.5] The old header here said "Lane A owns web/js/core/registry.js; it hasn't landed
// yet" — registry.js landed in R13 and that line was still standing 28 rounds later, which is how
// `pub`/`band_room`/`rsl` stayed missing long enough to dress 81 venue lots corpus-wide in op-shop
// paint (A's D3; fixed in buildings.js `signStyleFor`, see its header for the measurement).
//
// What this table IS, honestly, today:
// • the FIXTURE's facade pools (bare skin keys — `pickFacade` below; the fixture has no venues), and
// • Lane B's nine SHIPPED sign palettes, which `buildings.js signStyleFor()` prefers over A's for
// exactly those nine so the town's signage bytes don't move under a bug fix.
// It is NOT the shop-type vocabulary. **`web/js/core/registry.js SHOP_TYPES` (12 types, each with a
// full `sign:{bg,fg,accent,style}`) is the single source of truth** — anything not in the nine below
// resolves there. Adding a type here is not how a new shop type enters the game.
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', 'Fosseys', '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 95-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;