Lane C: round-12 pub venue — archetype + watchPoints + audio.gigKey (?gigs=1)
v3.0-alpha band room. New pub recipe (venue:true): stage + PA + amps at the back wall (new stage/paSpeaker/ampStack fittings, GLB slots stage/pa_speaker/amp for E), bar counter (keeper), sticky carpet, blocked back door = green room.
buildInterior returns for venues: room.watchPoints[] (6-8 seeded audience poses {x,z,ry,dance} facing the stage, dance ~1/3, counter.stand ry convention); room.stage {x,z,w,d,deckY,frontZ,bandPoses[3]} for Lane D's band; room.audio.gigKey ('gig-'+genreKey) set when Lane F passes opts.gig.
Flags-off byte-identical: venue code runs only for recipe.venue on its own seed sub-streams; non-pub rooms have watchPoints:[], stage:null, unchanged placement/audio.
Verified: 40-seed pub sweep (0 throws/carves/path-fails, 8 watchPoints, dance 0.38) + 60-room mixed soak (0 det/path fails, leak geo0/tex0, worst 19ms). Non-pub unchanged. Interface for D/F: docs/LANES/LANE_C_PUB.md. scaffold + manifest green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
acf734a1e5
commit
05e020e3b8
@ -3,7 +3,30 @@
|
|||||||
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
||||||
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
|
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
|
||||||
|
|
||||||
Last updated: 2026-07-15 (round 11) · owner: PROCITY-C · reviewer: Fable
|
Last updated: 2026-07-15 (round 12) · owner: PROCITY-C · reviewer: Fable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Update 2026-07-15 (round 12, v3.0-alpha) — the pub venue: archetype + watchPoints
|
||||||
|
|
||||||
|
R12 §Lane C (venues & gigs, behind `?gigs=1`). Interface published in
|
||||||
|
[docs/LANES/LANE_C_PUB.md](docs/LANES/LANE_C_PUB.md) — the "publish interface first" deliverable D/F build on.
|
||||||
|
|
||||||
|
- **`pub` archetype** (`theme.js`, `venue:true`): low **stage** + PA cabinets + amps at the back wall (new
|
||||||
|
`stage`/`paSpeaker`/`ampStack` fittings, GLB slots `stage`/`pa_speaker`/`amp` for E), bar counter (`corner`
|
||||||
|
— the barkeep is just a keeper), sticky-carpet floor, the blocked back doorway = the green room.
|
||||||
|
- **`room.watchPoints[]`** — 6–8 seeded audience floor poses `{x,z,ry,dance,slotIndex}` facing the stage
|
||||||
|
centre (`counter.stand` ry convention). `dance` seeded **~⅓ true** (John: "a bit of both"). Walkable,
|
||||||
|
reachable, clear of keeper/browse/stage. **`room.stage`** = `{x,z,w,d,deckY,frontZ,bandPoses[3]}` so D
|
||||||
|
drops the band on the deck.
|
||||||
|
- **`room.audio.gigKey`** — F passes `opts.gig` (gig state); a gig-on pub returns `gigKey='gig-'+genreKey`
|
||||||
|
(`gig-pubrock` for the alpha), else a quiet-night interior. F prefers gigKey over musicKey. (→ E: name the
|
||||||
|
live bed `gig-pubrock`.)
|
||||||
|
- **Flags-off byte-identical**: venue code runs only for `recipe.venue`, on its own `stage`/`watch` sub-
|
||||||
|
streams — non-pub rooms have `watchPoints:[]`, `stage:null`, unchanged audio + placement.
|
||||||
|
- Verified: 40-seed pub sweep — 0 throws · **0 carves** (PA `areaFree`-guarded off the counter strip) · 0
|
||||||
|
path-fails · 8 watchPoints each · dance 0.38. 60-room mixed soak (incl. pub) — 0 throws/det/path fails,
|
||||||
|
**leak geo0/tex0**, worst 19 ms. Non-pub rooms confirmed unchanged.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
55
docs/LANES/LANE_C_PUB.md
Normal file
55
docs/LANES/LANE_C_PUB.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# LANE C — pub venue interface (round 12, v3.0-alpha) → for Lane D + Lane F
|
||||||
|
|
||||||
|
*PROCITY-C, 2026-07-15. R12 §Lane C. The band room ships behind `?gigs=1` (A only emits `pub` shops
|
||||||
|
when the flag is on). Interface published here + in this commit's code; non-pub rooms are byte-identical
|
||||||
|
(the venue code only runs for `recipe.venue`, on its own seed sub-streams). Verified: 40-seed pub sweep +
|
||||||
|
60-room mixed soak — 0 throws · 0 carves · 0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst 19 ms.*
|
||||||
|
|
||||||
|
## What `buildInterior` returns for a pub (type `'pub'`)
|
||||||
|
Everything below is **pure data on the return** (room-local coords, `counter.stand` ry convention:
|
||||||
|
rig-front = local −Z), so Lane D drops rigs straight in and Lane F reads state — no new lifecycle.
|
||||||
|
|
||||||
|
### `room.stage` — the band stage (null for non-venues)
|
||||||
|
```js
|
||||||
|
room.stage = {
|
||||||
|
x, z, // deck centre (against the back wall)
|
||||||
|
w, d, // deck size (m)
|
||||||
|
deckY, // top surface height (m) — band rigs stand at y = deckY
|
||||||
|
frontZ, // z of the deck lip (the crowd stands in front of this, +Z)
|
||||||
|
bandPoses: [ { x, z, ry, role } ×3 ], // guitar / vocal / bass, across the deck front, facing the crowd (ry=π → +Z)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Lane D:** put the 3-piece band at `bandPoses` (lift to `y = deckY`); `role` hints instrument. Drummer
|
||||||
|
can sit up-stage on the riser (there's a raised riser at deck-centre-back) — your call, C just gives 3 front poses.
|
||||||
|
Instruments = E's GLBs when present, primitives otherwise (asset law).
|
||||||
|
|
||||||
|
### `room.watchPoints` — the audience (empty [] for non-venues)
|
||||||
|
```js
|
||||||
|
room.watchPoints = [ { x, z, ry, dance, slotIndex } ] // 6..8 seeded floor poses, all facing the stage centre
|
||||||
|
```
|
||||||
|
- `dance` is seeded **~⅓ true** ("a bit of both"). **Lane D:** `dance:false` → stand-and-watch idle
|
||||||
|
(subtle weight-shift); `dance:true` → bob/sway/step loop with a **seeded phase offset** so the crowd
|
||||||
|
doesn't metronome (use `slotIndex` or the pose coords to derive the phase).
|
||||||
|
- Every point is walkable, reachable from the door, and clear of the keeper stand, browse points, and the
|
||||||
|
stage/PA footprint. Deterministic per shop.seed. Crowd cap = `watchPoints.length` (F's smoke asserts ≤).
|
||||||
|
|
||||||
|
### `room.counter` / keeper — unchanged
|
||||||
|
The bar is a normal counter; the **barkeep is just a keeper** at `room.counter.stand` (existing R9 seam).
|
||||||
|
|
||||||
|
### `room.audio.gigKey` — the live bed
|
||||||
|
`room.audio = { musicKey, toneKey, gigKey? }`. **Lane F** passes gig state via `opts.gig` to `buildInterior`:
|
||||||
|
```js
|
||||||
|
buildInterior(pubShop, THREE, { gig: { on: true, genreKey: 'pubrock' } })
|
||||||
|
// → room.audio.gigKey === 'gig-pubrock' (gigKey = 'gig-' + genreKey)
|
||||||
|
```
|
||||||
|
- `gigKey` is present **only when a gig is on**. A quiet-night pub has no `opts.gig` → normal interior
|
||||||
|
(`toneKey` room-tone + maybe the seeded radio `musicKey`).
|
||||||
|
- **Prefer `gigKey` over `musicKey`** while the gig plays: `play(room.audio.gigKey || room.audio.musicKey)`.
|
||||||
|
- **Lane E:** please name the pub-rock live bed **`gig-pubrock`** in `manifest.audio` (music or a `gig`
|
||||||
|
section) so the key resolves; missing key → silence (audio law), band still visible.
|
||||||
|
|
||||||
|
## The archetype (C-owned, FYI)
|
||||||
|
`pub` recipe (`theme.js`, `venue:true`): stage + PA + amps at the back wall, bar counter (`corner`),
|
||||||
|
sticky-carpet floor, pub tables + bar-back shelf against the walls, the blocked back doorway doubles as
|
||||||
|
the **green room**. Primitive stage/PA/amp fittings today with **GLB slots for Lane E**: `stage`,
|
||||||
|
`pa_speaker`, `amp` (house GLB law; primitive fallback holds — a gig with no GLBs is still a gig).
|
||||||
@ -392,12 +392,51 @@ function artFrame(ctx, o = {}, r) {
|
|||||||
return { group: g, footprint: { w: sz, d: 0.08 }, height: sz, slots: [], places: [], wallMounted: true, mountY: 1.5 };
|
return { group: g, footprint: { w: sz, d: 0.08 }, height: sz, slots: [], places: [], wallMounted: true, mountY: 1.5 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── VENUE (pub, round 12 · behind ?gigs=1) ───────────────────────────────────────────
|
||||||
|
|
||||||
|
// Low band stage — a timber deck with a dark backdrop + a drum riser, against the back wall. The band
|
||||||
|
// (Lane D) stands on the deck (`deckY` = top surface). GLB slot: `stage` (E's when present, else this).
|
||||||
|
function stage(ctx, o = {}, r) {
|
||||||
|
const g = new ctx.THREE.Group();
|
||||||
|
const w = o.w || 3.4, d = o.d || 2.0, h = o.h || 0.32;
|
||||||
|
const deck = ctx.mat('#3a2a1e', 0.85), riser = ctx.mat('#2a2018', 0.85);
|
||||||
|
ctx.box(w, h, d, deck, 0, h / 2, 0, g); // deck
|
||||||
|
ctx.box(w + 0.06, 0.06, d + 0.06, ctx.mat('#1c130d', 0.9), 0, h, 0, g); // deck edge trim
|
||||||
|
ctx.box(w, 2.2, 0.06, ctx.mat('#161016', 0.95), 0, h + 1.1, -d / 2 + 0.05, g); // backdrop curtain
|
||||||
|
ctx.box(1.0, 0.16, 0.9, riser, 0, h + 0.08, -d / 4, g); // drum riser, up-stage centre
|
||||||
|
g.userData = { kind: 'stage' };
|
||||||
|
return { group: g, footprint: { w: w + 0.1, d: d + 0.1 }, height: h + 2.3, slots: [], places: [g], deckY: h };
|
||||||
|
}
|
||||||
|
|
||||||
|
// PA speaker stack — a tall black cabinet flanking the stage. GLB slot: `pa_speaker`.
|
||||||
|
function paSpeaker(ctx, o = {}, r) {
|
||||||
|
const g = new ctx.THREE.Group();
|
||||||
|
const w = 0.6, d = 0.55, h = o.h || 1.7;
|
||||||
|
ctx.box(w, h, d, ctx.mat('#1a1a1e', 0.7), 0, h / 2, 0, g); // cabinet
|
||||||
|
const grille = ctx.mat('#0c0c10', 0.6);
|
||||||
|
ctx.box(w - 0.1, 0.5, 0.02, grille, 0, h * 0.32, d / 2, g); // woofer face
|
||||||
|
ctx.box(w - 0.14, 0.24, 0.02, grille, 0, h - 0.32, d / 2, g); // horn face
|
||||||
|
g.userData = { kind: 'prop' };
|
||||||
|
return { group: g, footprint: { w, d }, height: h, slots: [], places: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guitar/bass combo amp — sits ON the stage deck behind the band. GLB slot: `amp`.
|
||||||
|
function ampStack(ctx, o = {}, r) {
|
||||||
|
const g = new ctx.THREE.Group();
|
||||||
|
const w = 0.62, d = 0.36, h = 0.5;
|
||||||
|
ctx.box(w, h, d, ctx.mat(r() < 0.5 ? '#2a2320' : '#20242a', 0.8), 0, h / 2, 0, g);
|
||||||
|
ctx.box(w - 0.06, 0.3, 0.02, ctx.mat('#100f10', 0.6), 0, h * 0.55, d / 2, g); // grille cloth
|
||||||
|
g.userData = { kind: 'prop' };
|
||||||
|
return { group: g, footprint: { w, d }, height: h, slots: [], places: [] };
|
||||||
|
}
|
||||||
|
|
||||||
export const FITTINGS = {
|
export const FITTINGS = {
|
||||||
recordBin, crate, crateStack, listeningCorner,
|
recordBin, crate, crateStack, listeningCorner,
|
||||||
wallShelf, metalShelf, cubeShelf, bookshelf, vhsAisle, spinnerRack, arcadeCabinet,
|
wallShelf, metalShelf, cubeShelf, bookshelf, vhsAisle, spinnerRack, arcadeCabinet,
|
||||||
clothesRack,
|
clothesRack,
|
||||||
trestleTable, counter, glassCase,
|
trestleTable, counter, glassCase,
|
||||||
pegboard, fridge, magazineRack, armchair, barredWindow, returnsSlot, escalator, artFrame,
|
pegboard, fridge, magazineRack, armchair, barredWindow, returnsSlot, escalator, artFrame,
|
||||||
|
stage, paSpeaker, ampStack,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build a fitting by kind. Unknown kinds fall back to a wall shelf (never crash).
|
// Build a fitting by kind. Unknown kinds fall back to a wall shelf (never crash).
|
||||||
|
|||||||
@ -19,8 +19,12 @@
|
|||||||
// // stands, in room-local space, facing the customer.
|
// // stands, in room-local space, facing the customer.
|
||||||
// browsePoints: [ {x,z,ry,atKind,slotIndex} ], // 0..3 seeded floor poses (R9) where Lane D stands
|
// browsePoints: [ {x,z,ry,atKind,slotIndex} ], // 0..3 seeded floor poses (R9) where Lane D stands
|
||||||
// // browser rigs, facing the goods (same frame + ry as counter.stand)
|
// // browser rigs, facing the goods (same frame + ry as counter.stand)
|
||||||
// audio: { musicKey, toneKey }, // R11 — keys into Lane E manifest.audio (interior music bed +
|
// audio: { musicKey, toneKey, gigKey? }, // R11/R12 — keys into Lane E manifest.audio (music bed +
|
||||||
// // room-tone), seeded per shop. musicKey may be null (no music).
|
// // room-tone), seeded per shop. musicKey may be null. gigKey set
|
||||||
|
// // only when opts.gig is on (venue) — F prefers gigKey over musicKey.
|
||||||
|
// watchPoints: [ {x,z,ry,dance} ], // VENUE (R12, pub only, else []) — 6..8 audience poses facing the
|
||||||
|
// // stage; dance ~1/3 true. Same frame + ry convention as counter.stand.
|
||||||
|
// stage: { x,z,w,d,deckY,frontZ,bandPoses }, // VENUE (R12, else null) — the band stage; bandPoses[3] on the deck.
|
||||||
// dims: { W, D, H, archetype, type }, // room metrics
|
// dims: { W, D, H, archetype, type }, // room metrics
|
||||||
// placement, // deterministic placement summary (deep-equal per seed)
|
// placement, // deterministic placement summary (deep-equal per seed)
|
||||||
// pathOK, // door→counter connectivity held (always true; carved if needed)
|
// pathOK, // door→counter connectivity held (always true; carved if needed)
|
||||||
@ -88,14 +92,19 @@ const MUSIC_BY_TYPE = { // manifest.audio.music dedi
|
|||||||
dept: 'arcade', arcade: 'arcade',
|
dept: 'arcade', arcade: 'arcade',
|
||||||
}; // default → null (no dedicated bed)
|
}; // default → null (no dedicated bed)
|
||||||
|
|
||||||
function audioFor(type, ctx) {
|
function audioFor(type, ctx, opts) {
|
||||||
const toneKey = ROOMTONE_BY_TYPE[type] || 'roomtone-retail';
|
const toneKey = ROOMTONE_BY_TYPE[type] || 'roomtone-retail';
|
||||||
let musicKey = MUSIC_BY_TYPE[type] || null;
|
let musicKey = MUSIC_BY_TYPE[type] || null;
|
||||||
// Shops with no dedicated bed: a seeded ~1-in-3 keeps a general radio on (the milk-bar bed carries the
|
// Shops with no dedicated bed: a seeded ~1-in-3 keeps a general radio on (the milk-bar bed carries the
|
||||||
// manifest "general" tag). Seeded on its own sub-stream so the same shop is silent-or-playing every
|
// manifest "general" tag). Seeded on its own sub-stream so the same shop is silent-or-playing every
|
||||||
// revisit and adding it never shifts another subsystem's seeded picks.
|
// revisit and adding it never shifts another subsystem's seeded picks.
|
||||||
if (!musicKey && ctx.stream('audio')() < 0.34) musicKey = 'milkbar';
|
if (!musicKey && ctx.stream('audio')() < 0.34) musicKey = 'milkbar';
|
||||||
return { musicKey, toneKey };
|
const audio = { musicKey, toneKey };
|
||||||
|
// Venue gig (R12): Lane F passes the live gig state via opts.gig; F should prefer gigKey over musicKey
|
||||||
|
// while the gig is on. A quiet-night pub has no opts.gig → normal interior (room-tone + maybe radio).
|
||||||
|
const gig = opts && opts.gig;
|
||||||
|
if (gig && (gig.on || gig.genreKey)) audio.gigKey = 'gig-' + (gig.genreKey || 'pubrock');
|
||||||
|
return audio;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildInterior(shop, THREE, opts) {
|
export function buildInterior(shop, THREE, opts) {
|
||||||
@ -150,7 +159,9 @@ export function buildInterior(shop, THREE, opts) {
|
|||||||
places: lay.places,
|
places: lay.places,
|
||||||
counter: lay.counter, // { mesh, pose:{x,z,ry}, stand:{x,z,ry} } — Lane D keeper spawn pose
|
counter: lay.counter, // { mesh, pose:{x,z,ry}, stand:{x,z,ry} } — Lane D keeper spawn pose
|
||||||
browsePoints: lay.browsePoints, // [ {x,z,ry,atKind,slotIndex} ] 0..3 — Lane D browser rig poses (R9)
|
browsePoints: lay.browsePoints, // [ {x,z,ry,atKind,slotIndex} ] 0..3 — Lane D browser rig poses (R9)
|
||||||
audio: audioFor(recipe.key, ctx), // { musicKey, toneKey } — Lane E manifest.audio keys (R11), seeded per shop
|
watchPoints: lay.watchPoints || [], // venue (R12): [ {x,z,ry,dance,slotIndex} ] 6..8 audience poses (else [])
|
||||||
|
stage: lay.stage || null, // venue (R12): { x,z,w,d,deckY,frontZ,bandPoses[3] } (else null)
|
||||||
|
audio: audioFor(recipe.key, ctx, opts), // { musicKey, toneKey, gigKey? } — Lane E manifest.audio keys (R11/R12)
|
||||||
dims: { ...dims, archetype, type: recipe.key },
|
dims: { ...dims, archetype, type: recipe.key },
|
||||||
recipe: { key: recipe.key, label: recipe.label, counterPos: recipe.counterPos, clutter: recipe.clutter },
|
recipe: { key: recipe.key, label: recipe.label, counterPos: recipe.counterPos, clutter: recipe.clutter },
|
||||||
placement: lay.placement,
|
placement: lay.placement,
|
||||||
|
|||||||
@ -209,6 +209,75 @@ function placeBrowsePoints(grid, placed, counterStand, spawnCell, r) {
|
|||||||
return pts;
|
return pts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── VENUE stage (pub, round 12 · behind ?gigs=1) ──────────────────────────────────────
|
||||||
|
// Places a low stage centred on the back wall, PA cabinets flanking it on the floor, and 1–2 amps on the
|
||||||
|
// deck. Stage + PA go into `placed` at priority 8 so the door→counter path loop never pulls them. Returns
|
||||||
|
// the stage frame the crowd faces + 3 seeded band poses on the deck (Lane D drops the band trio in).
|
||||||
|
function placeStage(ctx, grid, W, D, roomGroup, r) {
|
||||||
|
const sw = Math.min(W - 1.4, Math.max(3.0, W * 0.58)), sd = 2.0;
|
||||||
|
const cx = 0, cz = -D / 2 + sd / 2 + 0.2; // back wall, centred
|
||||||
|
const f = buildFitting('stage', ctx, { w: sw, d: sd }, r);
|
||||||
|
f.group.position.set(cx, 0, cz); roomGroup.add(f.group);
|
||||||
|
const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, 0);
|
||||||
|
const rect = rectCells(grid, cx, cz, hw, hd); stampRect(grid, rect);
|
||||||
|
const out = [{ fitting: f, x: cx, z: cz, ry: 0, hw, hd, rect, priority: 8, kind: 'stage' }];
|
||||||
|
|
||||||
|
for (const sgn of [-1, 1]) { // PA cabinets at the stage front corners, pulled
|
||||||
|
const px = sgn * (sw / 2 - 0.3), pz = cz + sd / 2 + 0.35; // in + just off the deck lip
|
||||||
|
const [phw, phd] = halfExtents(0.6, 0.55, 0); // paSpeaker footprint (fixed)
|
||||||
|
const prect = rectCells(grid, px, pz, phw, phd);
|
||||||
|
if (!areaFree(grid, prect)) continue; // never stamp over the counter's service strip / a wall
|
||||||
|
const pf = buildFitting('paSpeaker', ctx, {}, r);
|
||||||
|
pf.group.position.set(px, 0, pz); roomGroup.add(pf.group);
|
||||||
|
stampRect(grid, prect);
|
||||||
|
out.push({ fitting: pf, x: px, z: pz, ry: 0, hw: phw, hd: phd, rect: prect, priority: 8, kind: 'prop' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const deckY = f.deckY || 0.32; // 1–2 amps on the deck behind the band
|
||||||
|
for (let i = 0, n = 1 + (r() < 0.5 ? 1 : 0); i < n; i++) {
|
||||||
|
const af = buildFitting('ampStack', ctx, {}, r);
|
||||||
|
af.group.position.set(cx + (i === 0 ? -1 : 1) * sw * 0.28, deckY, cz - sd * 0.18);
|
||||||
|
roomGroup.add(af.group);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bandPoses = [-sw * 0.28, 0, sw * 0.28].map((bx, i) => ({ // 3 across the deck front, facing +Z (audience)
|
||||||
|
x: round(cx + bx), z: round(cz + sd * 0.12), ry: round(Math.PI), role: ['guitar', 'vocal', 'bass'][i],
|
||||||
|
}));
|
||||||
|
return { placed: out, stage: { x: cx, z: cz, w: sw, d: sd, deckY, frontZ: round(cz + sd / 2), bandPoses } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WATCH POINTS (venue, round 12) — 6–8 seeded audience floor poses facing the stage ──
|
||||||
|
// Same pure-data pattern as browse points: walkable free cells, reachable from spawn, clear of the keeper
|
||||||
|
// stand + browse points + stage, spaced for a crowd. `dance` seeded ~1/3 true (John: "a bit of both").
|
||||||
|
function placeWatchPoints(grid, stage, spawnCell, counterStand, browsePoints, r) {
|
||||||
|
let start = spawnCell;
|
||||||
|
if (grid.occ[start] !== 0) { const s = nearestFree(grid, start); if (s < 0) return []; start = s; }
|
||||||
|
const reach = floodFill(grid, start);
|
||||||
|
const avoid = [counterStand, ...(browsePoints || [])].filter(Boolean);
|
||||||
|
const near = (x, z, list, d) => list.some(q => Math.hypot(q.x - x, q.z - z) < d);
|
||||||
|
const cands = [];
|
||||||
|
for (let z = stage.frontZ + 0.6; z <= grid.D / 2 - 1.6; z += 0.7) // between stage lip and the door zone
|
||||||
|
for (let x = -grid.W / 2 + 0.8; x <= grid.W / 2 - 0.8; x += 0.8) cands.push({ x, z });
|
||||||
|
for (const c of cands) c.key = r(); // seed the visiting order up front (determinism)
|
||||||
|
cands.sort((a, b) => a.key - b.key);
|
||||||
|
const pts = [];
|
||||||
|
const valid = (x, z) => {
|
||||||
|
const [cx, cz] = grid.cellOf(x, z);
|
||||||
|
if (cx < 1 || cz < 1 || cx >= grid.cols - 1 || cz >= grid.rows - 1) return false;
|
||||||
|
const idx = cz * grid.cols + cx;
|
||||||
|
if (grid.occ[idx] !== 0 || grid.reserved[idx] || !reach.has(idx)) return false;
|
||||||
|
if (near(x, z, avoid, MIN_SEP) || near(x, z, pts, 0.7)) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
for (const c of cands) {
|
||||||
|
if (pts.length >= 8) break;
|
||||||
|
if (!valid(c.x, c.z)) continue;
|
||||||
|
const dx = stage.x - c.x, dz = stage.z - c.z; // face the stage centre (counter.stand ry convention)
|
||||||
|
pts.push({ x: round(c.x), z: round(c.z), ry: round(Math.atan2(-dx, -dz)), dance: r() < 0.34, slotIndex: pts.length });
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
// ── wall-slot system (shuffled pool → art + signs + wall fittings never overlap) ──────
|
// ── wall-slot system (shuffled pool → art + signs + wall fittings never overlap) ──────
|
||||||
function makeWallSlots(W, D, r) {
|
function makeWallSlots(W, D, r) {
|
||||||
const slots = [];
|
const slots = [];
|
||||||
@ -268,6 +337,15 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
|
|||||||
// reserve the strip in front of the till so nothing blocks service
|
// reserve the strip in front of the till so nothing blocks service
|
||||||
reserveRect(grid, counter.front.x - 0.5, counter.front.z - 0.5, counter.front.x + 0.5, counter.front.z + 0.5);
|
reserveRect(grid, counter.front.x - 0.5, counter.front.z - 0.5, counter.front.x + 0.5, counter.front.z + 0.5);
|
||||||
|
|
||||||
|
// 1b) VENUE STAGE (pub, round 12) — placed BEFORE the fitting mix so its footprint is reserved. Only
|
||||||
|
// for recipe.venue; other rooms never touch the 'stage'/'watch' streams → flags-off byte-identical.
|
||||||
|
let stageInfo = null, watchPoints = [];
|
||||||
|
if (recipe.venue) {
|
||||||
|
const st = placeStage(ctx, grid, W, D, roomGroup, ctx.stream('stage'));
|
||||||
|
stageInfo = st.stage;
|
||||||
|
for (const sp of st.placed) placed.push(sp);
|
||||||
|
}
|
||||||
|
|
||||||
// 2) FITTINGS from the recipe mix (skip the counter entry — already placed).
|
// 2) FITTINGS from the recipe mix (skip the counter entry — already placed).
|
||||||
const area = W * D;
|
const area = W * D;
|
||||||
const wallSlots = makeWallSlots(W, D, rWall);
|
const wallSlots = makeWallSlots(W, D, rWall);
|
||||||
@ -330,6 +408,10 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
|
|||||||
// every point sits on a survivor's open side and on a cell reachable from the door.
|
// every point sits on a survivor's open side and on a cell reachable from the door.
|
||||||
const browsePoints = placeBrowsePoints(grid, placed, counter.stand, spawnCell, ctx.stream('browse'));
|
const browsePoints = placeBrowsePoints(grid, placed, counter.stand, spawnCell, ctx.stream('browse'));
|
||||||
|
|
||||||
|
// 6b) WATCH POINTS (venue, round 12) — audience floor poses facing the stage, computed after the path
|
||||||
|
// guarantee + browse points (so they avoid both), on cells reachable from the door.
|
||||||
|
if (stageInfo) watchPoints = placeWatchPoints(grid, stageInfo, spawnCell, counter.stand, browsePoints, ctx.stream('watch'));
|
||||||
|
|
||||||
// placement summary for the determinism deep-equal test
|
// placement summary for the determinism deep-equal test
|
||||||
const placement = placed.filter(p => !p.removed).map(p => ({
|
const placement = placed.filter(p => !p.removed).map(p => ({
|
||||||
kind: p.kind || 'fitting', kindName: p.fitting.group.userData.kind,
|
kind: p.kind || 'fitting', kindName: p.fitting.group.userData.kind,
|
||||||
@ -340,6 +422,8 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
|
|||||||
placed: placed.filter(p => !p.removed),
|
placed: placed.filter(p => !p.removed),
|
||||||
places, decor: decor.meshes, grid, pathOK, carved,
|
places, decor: decor.meshes, grid, pathOK, carved,
|
||||||
spawnCell, targetCell, placement, browsePoints,
|
spawnCell, targetCell, placement, browsePoints,
|
||||||
|
watchPoints, // venue: 6–8 audience poses {x,z,ry,dance} facing the stage (else [])
|
||||||
|
stage: stageInfo, // venue: { x,z,w,d,deckY,frontZ,bandPoses } (else null)
|
||||||
counter: {
|
counter: {
|
||||||
mesh: counter.fitting.group,
|
mesh: counter.fitting.group,
|
||||||
pose: { x: round(counter.x), z: round(counter.z), ry: round(counter.ry) }, // the bench itself
|
pose: { x: round(counter.x), z: round(counter.z), ry: round(counter.ry) }, // the bench itself
|
||||||
|
|||||||
@ -200,6 +200,27 @@ const RECIPES = {
|
|||||||
stockKind: 'boxes',
|
stockKind: 'boxes',
|
||||||
signFlavour: ['$1 EACH', 'CASH ONLY', 'FIRST IN BEST DRESSED', 'MAKE ME AN OFFER', 'FRESH TODAY'],
|
signFlavour: ['$1 EACH', 'CASH ONLY', 'FIRST IN BEST DRESSED', 'MAKE ME AN OFFER', 'FRESH TODAY'],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── PUB (venue, v3.0-alpha · round 12, behind ?gigs=1) ───────────────────────────────
|
||||||
|
// The band room. `venue: true` makes layout.js place a stage (+ PA/amps) at the back wall and emit
|
||||||
|
// room.watchPoints[] for Lane D's crowd + room.stage for the band. Bar counter = a normal counter, so
|
||||||
|
// the barkeep is just a keeper. Sticky-carpet floor; the blocked back doorway doubles as the green room.
|
||||||
|
pub: {
|
||||||
|
label: 'Pub',
|
||||||
|
archetypeBias: [['wide', 3], ['hall', 2]], // big open room for a crowd
|
||||||
|
wallpaperBias: ['damask-mauve', 'woodchip-white', 'stripe-sage'],
|
||||||
|
floorBias: ['carpet-swirl', 'carpet-mustard'], // sticky carpet
|
||||||
|
clutter: 0.45, // keep the floor open — the crowd fills it
|
||||||
|
counterPos: 'corner', // the bar, along the east wall (keeper = barkeep)
|
||||||
|
venue: true, // → stage + PA + watchPoints (layout.js)
|
||||||
|
fittings: [
|
||||||
|
{ kind: 'trestleTable', zone: 'wall', min: 1, max: 3, priority: 2 }, // pub tables against the walls
|
||||||
|
{ kind: 'metalShelf', zone: 'wall', min: 0, max: 1, priority: 2 }, // bar back-shelf (bottles)
|
||||||
|
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||||||
|
],
|
||||||
|
stockKind: 'snacks',
|
||||||
|
signFlavour: ['LIVE MUSIC FRI', 'HAPPY HOUR 5–6', 'NO THONGS AFTER 7', 'COLD BEER ON TAP', 'BANDS OUT BACK'],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Aliases: accept thriftgod / Overpass-style type names and map to canonical recipes.
|
// Aliases: accept thriftgod / Overpass-style type names and map to canonical recipes.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user