The roster board and the back office get real rooms

plate:corkboard, plate:backoffice and a TRELLIS'd COIN-U-SORT 400 — the two
screens seen every night were pure rectangles. Drawn versions stay as fallbacks.
RosterScene gets its own tiny preload because it is scene[0] and runs before the
prewarm it starts. Heading moved off the board: real art put it on the light.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-08-09 17:28:40 +10:00
parent 91fe06d023
commit d10b9a34fd
10 changed files with 219 additions and 18 deletions

View File

@ -2496,3 +2496,34 @@ had already split `barServeBreach` out of `barServe` so the RSA question can
actually fire. I only had to supply the missing words and the variety rule. actually fire. I only had to supply the missing words and the variety rule.
`tests/incidentReport.test.ts` now guards the whole class: every loaded kind `tests/incidentReport.test.ts` now guards the whole class: every loaded kind
must have a bespoke prompt, not the generic line. must have a bespoke prompt, not the generic line.
## SESSION — FABLE-SOLO-29 · 2026-08-09
**Branch:** main (solo, Fable) · **Gate:** lint ✓ build ✓ test ✓ (842 tests)
### The two between-shift screens stop being rectangles
The roster board and the back office are seen EVERY night and were both drawn
entirely from coloured primitives. Added to `tools/gen/assets.py` in the
existing idiom (new `BETWEEN` group):
- `plate:corkboard` — a lit cork noticeboard, flat route, keep_bg, no quantise
- `plate:backoffice` — a cramped panelled office, one bulb, empty desk
- `sorter` — MESH route (rake 58), the COIN-U-SORT itself. `quantise=False`
deliberately: it is a ~130px UI hero, not a 16px floor prop, and 16 colours
turns a grey machine to porridge.
Both scenes keep their drawn version as the fallback — a missing plate has
always meant placeholders, never a broken screen. The sorter image goes INSIDE
the existing container so every shake and bob still works untouched.
### RosterScene needs its own preload, uniquely
It is scene[0]: it runs before anything has loaded, so it cannot rely on the
manifest prewarm it goes on to START. `preload()` pulls the corkboard and only
the corkboard; the other ~130 sprites still warm in the background behind the
board.
### A legibility regression the plate caused, and the fix
Dropping real art behind the roster put the heading directly on the plate's
fluorescent tube — light type on the brightest thing in the image. The board
moved down to y=194 (258 tall) and the heading moved off it onto the dark wall
at y=16/38. Worth remembering: adding a background can break foreground text
that was fine over a flat colour.

View File

@ -70,7 +70,7 @@ furniture (bar, booths, stools) could come straight from its fit pipeline.
(794 MB, 26,910 files) — prior AI-gen pools; sweep for club/street/neon/night (794 MB, 26,910 files) — prior AI-gen pools; sweep for club/street/neon/night
material before generating fresh. material before generating fresh.
- m4pro SPACEY `MRP PHOTOSHOP FILES` (4.4 GB PSDs) — design-language source - m4pro SPACEY `MRP PHOTOSHOP FILES` (4.4 GB PSDs) — design-language source
(posters/flyers could become in-game wall dressing). m4pro is HANDS-OFF; access (posters/flyers could become in-game wall dressing). m4pro is reachable (HANDS-OFF label retired 2026-08-06 — never true); access
via the SMB mount on m3ultra if needed. via the SMB mount on m3ultra if needed.
### 4. Generation pipeline (MODELBEAST) ### 4. Generation pipeline (MODELBEAST)

118
docs/ASSETS.md.bak-20260806 Normal file
View File

@ -0,0 +1,118 @@
# NOT TONIGHT — Fleet Asset Notes
*What already exists on John's network that this game can reuse, borrow from, or
generate with. Sourced from the fleet-wide `ASSET_INVENTORY_2026-07-18.md` and
`GAME_ASSET_AUDIT_2026-07-18.md` (both on ultra `~/Documents/`). Compiled
2026-07-19.*
**Context for lanes:** v0.10.3 use PROCEDURAL placeholder art (coloured-pixel
paper-doll, synthesized audio) — nothing below is needed before the v0.4 art pass.
This doc exists so the art pass starts from the library, not from scratch. Nothing
here changes the `renderDoll` / layer contracts.
## Directly relevant (v0.4 art pass)
### 1. 90sDJsim paper-doll wardrobe — the big one
`ultra:~/Documents/90sDJsim/web/world/media/doll/` — **441 authored clothing
sprites** across tops/bottoms/shoes/hats/bags, brand-flavoured (coogi, dr_martens,
nike_air_max, moschino, quiksilver, kmart, disruptor_ii…), dynamically loaded by
`os.listdir` slot convention.
- **Reuse the taxonomy**: their slot/brand/type naming should inform
`data/outfits.ts` vocabulary (we already have shoes/legs/top/outer/hair/accessory
— cross-check their catalog for types we missed, e.g. bucket hats, bumbags).
- **Reuse the art via pixelation**: 90s wardrobe reads perfectly as 2026 Valley
fashion-in-quotes. A downscale/quantize pass (sprite → 32×48 palette-locked
pixel layer) could convert the catalog into real doll layers. Prototype on 10
sprites before committing — pixelated photo-sprites can look mushy; may need a
cleanup pass or MODELBEAST-assisted redraw per sprite.
### 2. RIGROOM — the character × animation factory (surveyed 2026-07-19)
**This supersedes the old "animation references" note. The fleet has a full
character/animation pipeline, live and organised:**
- **RIGROOM library** (`ultra:~/meshgod-backups/library`, 2.4 GB): **607
animation FBX** in themed packs + 59 character files. Web UI live at
`http://100.91.239.7:8010/rigroom` (characters × animations pairing, three.js
preview, instant Mixamo-ready FBX download). CLI:
`MESHGOD/scripts/rigroom.py classify|sort|scan`.
- Clips that read like THIS game's shot list: `Searching_Pockets` (pat-down),
`Standing_Arguing` + a full boxing/knockout pack (floor fights),
`Shaking_Head_No` (the deny), `Inspecting`/`Looking_Around` (ID check),
`Leaning_Idle02_Smoking` (smoking area), dozens of idles/sits, dance packs,
`Brutal To Happy Walking`, zombie stagger (maggot walk), 12 DJ motions.
Raw source: `m3ultra:~/Documents/Rokoko_Free_Mocap_FBX_263` (606 FBX).
- **Modular character kit** (`m3ultra:~/Documents/character_kit_modular`,
canonical `ultra:~/Documents/character_kit/modular/`): GODVERSE socket
standard — head/hands/feet/torso swap on one shared 65-bone `mixamorig:`
skeleton, 40 embedded actions, every library clip retargets for free.
- **repose** (`m3ultra:~/Documents/repose`): photo → T-pose views (SD1.5 +
ControlNet, all-local) → MeshGod multi-view → autorig. A face becomes a
rigged character without leaving the tailnet.
- **For NOT TONIGHT (v0.4 art pass), the pre-render path is now fully paved:**
modular kit character + RIGROOM clip → Blender orthographic top-down/front →
pixelate → sprite sheets behind the existing `renderDoll` poses. The outfit
variety could even come from kit part swaps. Same pipeline, one venue's
worth of NPC animation for free. Decide at v0.4; contracts don't move.
- Also present, unsurveyed depth: `MOCAPGOD` (empty queue — service scaffold),
`mixamo_upload` staging, `MOTIONLIB` (2,703 FBX, ultra) still the deep pool.
### 2b. procity (m3ultra) — the 90s-Aussie town sim
Walkable procedural shopping town (three.js, no build step), built by lanes
like ours. Pipeline: 186 GLB/FBX with fit/normalize/decimate tooling
(`procity_fit_*` props: arcade cabinet, clothes racks, shelving), 21 peds.
The **prop pipeline conventions** (batch manifests, decimation results JSON)
are the pattern to copy if NOT TONIGHT ever goes 3D-prop-assisted; the venue
furniture (bar, booths, stools) could come straight from its fit pipeline.
### 3. Venue / street reference art
- `ultra:~/Documents/90sDJsim/web/world/media/` named scene/venue art — 100 wired
widescreen club/venue images. Mood reference for our neon-dark interiors.
- `gdrive:ASSETS FROM AI` (2.47 GB, 3,239 files) + `gdrive:IMAGE-DATABASE`
(794 MB, 26,910 files) — prior AI-gen pools; sweep for club/street/neon/night
material before generating fresh.
- m4pro SPACEY `MRP PHOTOSHOP FILES` (4.4 GB PSDs) — design-language source
(posters/flyers could become in-game wall dressing). m4pro is HANDS-OFF; access
via the SMB mount on m3ultra if needed.
### 4. Generation pipeline (MODELBEAST)
m3ultra central queue :8777 (see fleet skill). For NOT TONIGHT this is an
**image generator, not a mesh generator** — we need no GLBs. Useful for: patron
face/outfit reference sheets to hand-pixel from, venue backdrop concepts, Dazza's
contact photo, joke ID photos. The thriftgod `gen_assets.py` + 640px `sips`
convention (m3ultra `~/Documents/thriftgod/`) is a proven prompt→asset-dir pattern
worth cloning for an `art_src/` pipeline here.
## Explicitly NOT for this game
- **All 3D mesh libraries** (`3D=models`, `3D-STORE`, FBX, femalemeshed,
NPCFACTORY, MODELBEAST GLB output) — no 3D in this game; only via the Option-B
prerender path above, never at runtime.
- **Audio archives** (bandcamp ~1 TB, YT-AUDIO, MONSTER MIXES). The design is
locked to a fully synthesized WebAudio techno engine (design §5 / LANE_JUICE) —
the low-pass location trick needs a synth we control, and shipping archive audio
is a licensing minefield. John's own MONSTER MIXES are legally his — park the
idea of a licensed-mix easter egg (jukebox in the kebab shop?) as a someday-note.
- **dealgod image pool** (2.4M product photos) + `recordgod_disc_images` — product
photography, wrong domain. (One cursed exception: pat-down contraband items
could be pixelated from real dealgod product shots. Someday-note.)
- **thriftgod 2D dressing** — authored 640px painterly JPGs, wrong style; its
*pipeline* is the reusable part (see §4).
## Ground rules for the eventual art pass
**Licensing: none needed.** This is a private tailnet sandbox game, never public —
John's call (2026-07-19). Anything on the fleet or gdrive is fair game, including
the audio archives and MONSTER MIXES. (If that ever changes and the game goes
public, this section is the checklist to revisit first.)
1. Everything funnels through the existing contracts (`renderDoll` poses, outfit
slots) — art swaps in behind interfaces, callers never change.
2. One palette, one pixel density (32×48 queue dolls at 640×360) decided ONCE
before any batch conversion; retrofitting mixed densities is the classic trap.
3. Log each imported batch (source → transform → dest) in this file — not for
licensing, just so we can regenerate/retrace later.
4. Check the MeshGod gallery + MODELBEAST `data/` (m3ultra) before generating
anything — the inventory's rule "catalog before regenerating" applies here too.
5. The synthesized techno engine stays the core soundtrack (the location-filter
trick needs a synth we control), but real archive audio is now allowed as
layers/easter eggs — e.g. MONSTER MIXES on a kebab-shop jukebox, real crowd
ambience beds under the synth.

View File

@ -68,6 +68,8 @@
"pillar", "pillar",
"plant", "plant",
"planter", "planter",
"plate_backoffice",
"plate_corkboard",
"plywoodBar", "plywoodBar",
"pokie", "pokie",
"poolTable", "poolTable",
@ -88,6 +90,7 @@
"sinkRow", "sinkRow",
"skylineCard", "skylineCard",
"smokeMachine", "smokeMachine",
"sorter",
"speaker", "speaker",
"stackF1", "stackF1",
"staffBartender", "staffBartender",

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

BIN
public/props/sorter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -66,11 +66,16 @@ export class HaulDeskScene extends Phaser.Scene {
const t = tallyHaul(this.args.haul, this.args.pocketedBaggie); const t = tallyHaul(this.args.haul, this.args.pocketedBaggie);
this.lines = t.lines; this.lines = t.lines;
this.add.rectangle(W / 2, H / 2, W, H, 0x14110c); // The room. The drawn version stays as the fallback: a missing plate has
// the desk: a slab of laminate under a single tired bulb // always meant placeholders rather than a broken screen.
this.add.ellipse(W / 2, 96, 460, 220, 0x2a2318, 0.55); if (this.textures.exists('gen:prop:plate_backoffice')) {
this.add.rectangle(W / 2, 250, W, 220, 0x3a2f1e); this.add.image(0, 0, 'gen:prop:plate_backoffice').setOrigin(0, 0).setDisplaySize(W, H);
this.add.rectangle(W / 2, 142, W, 3, 0x241c12); } else {
this.add.rectangle(W / 2, H / 2, W, H, 0x14110c);
this.add.ellipse(W / 2, 96, 460, 220, 0x2a2318, 0.55);
this.add.rectangle(W / 2, 250, W, 220, 0x3a2f1e);
this.add.rectangle(W / 2, 142, W, 3, 0x241c12);
}
this.add this.add
.text(W / 2, 20, HAUL_UI.title, { fontFamily: MONO, fontSize: '15px', color: '#f0e4d0' }) .text(W / 2, 20, HAUL_UI.title, { fontFamily: MONO, fontSize: '15px', color: '#f0e4d0' })
@ -85,7 +90,7 @@ export class HaulDeskScene extends Phaser.Scene {
.text(W / 2, 60, '', { fontFamily: MONO, fontSize: '9px', color: '#e8dcc4', align: 'center', wordWrap: { width: 420 } }) .text(W / 2, 60, '', { fontFamily: MONO, fontSize: '9px', color: '#e8dcc4', align: 'center', wordWrap: { width: 420 } })
.setOrigin(0.5, 0); .setOrigin(0.5, 0);
this.quipText = this.add this.quipText = this.add
.text(W / 2, 300, '', { fontFamily: MONO, fontSize: '8px', color: '#a09070', align: 'center', wordWrap: { width: 460 } }) .text(W / 2, 300, '', { fontFamily: MONO, fontSize: '8px', color: '#c0ac84', align: 'center', wordWrap: { width: 460 } })
.setOrigin(0.5, 0); .setOrigin(0.5, 0);
this.tallyText = this.add this.tallyText = this.add
.text(W - 18, 250, '$0', { fontFamily: MONO, fontSize: '26px', color: '#f0d060' }) .text(W - 18, 250, '$0', { fontFamily: MONO, fontSize: '26px', color: '#f0d060' })
@ -131,16 +136,22 @@ export class HaulDeskScene extends Phaser.Scene {
const y = 210; const y = 210;
const c = this.add.container(x, y); const c = this.add.container(x, y);
this.sorter = c; this.sorter = c;
c.add(this.add.rectangle(0, 0, 150, 92, 0x5a5348).setStrokeStyle(2, 0x3a352c)); // The machine goes INSIDE the container so every shake and bob below still
c.add(this.add.rectangle(0, -38, 116, 20, 0x241f18)); // hopper mouth // works whether it is real art or the drawn fallback.
c.add(this.add.rectangle(0, -46, 132, 8, 0x6a6355)); if (this.textures.exists('gen:prop:sorter')) {
for (let i = -2; i <= 2; i++) { c.add(this.add.image(0, 0, 'gen:prop:sorter').setDisplaySize(132, 100));
c.add(this.add.rectangle(i * 26, 22, 18, 26, 0x2a251c)); // coin channels } else {
c.add(this.add.rectangle(0, 0, 150, 92, 0x5a5348).setStrokeStyle(2, 0x3a352c));
c.add(this.add.rectangle(0, -38, 116, 20, 0x241f18)); // hopper mouth
c.add(this.add.rectangle(0, -46, 132, 8, 0x6a6355));
for (let i = -2; i <= 2; i++) {
c.add(this.add.rectangle(i * 26, 22, 18, 26, 0x2a251c)); // coin channels
}
c.add(this.add.rectangle(0, 50, 150, 10, 0x46403a)); // the tray
} }
c.add(this.add.rectangle(0, 50, 150, 10, 0x46403a)); // the tray
c.add( c.add(
this.add this.add
.text(0, -8, HAUL_UI.machineName, { fontFamily: MONO, fontSize: '7px', color: '#9a9080' }) .text(0, 62, HAUL_UI.machineName, { fontFamily: MONO, fontSize: '7px', color: '#8a8070' })
.setOrigin(0.5), .setOrigin(0.5),
); );
} }

View File

@ -39,6 +39,16 @@ export class RosterScene extends Phaser.Scene {
this.carried = data ?? {}; this.carried = data ?? {};
} }
/**
* The board's own plate, and only that. RosterScene is scene[0] it runs
* before anything has loaded, so unlike every other screen it cannot rely on
* the manifest prewarm it goes on to start. One small image is worth the
* wait; the rest of the art is warmed in the background by prewarmProps().
*/
preload(): void {
this.load.image('gen:prop:plate_corkboard', 'props/plate_corkboard.png');
}
/** /**
* Warm the prop textures while the player reads the board. * Warm the prop textures while the player reads the board.
* *
@ -89,15 +99,19 @@ export class RosterScene extends Phaser.Scene {
// the back office: unpainted, honest // the back office: unpainted, honest
this.add.rectangle(W / 2, H / 2, W, H, 0x14100c); this.add.rectangle(W / 2, H / 2, W, H, 0x14100c);
this.add.rectangle(W / 2, H / 2 - 8, 560, 280, 0x3a2a18).setStrokeStyle(3, 0x241a10); if (this.textures.exists('gen:prop:plate_corkboard')) {
this.add.image(W / 2, 194, 'gen:prop:plate_corkboard').setDisplaySize(560, 258);
} else {
this.add.rectangle(W / 2, 194, 560, 258, 0x3a2a18).setStrokeStyle(3, 0x241a10);
}
this.add this.add
.text(W / 2, 42, `${venue.name}${NIGHT_NAMES[nightIndex] ?? ''}`, { .text(W / 2, 16, `${venue.name}${NIGHT_NAMES[nightIndex] ?? ''}`, {
fontFamily: MONO, fontSize: '16px', color: '#f0e4d0', fontFamily: MONO, fontSize: '16px', color: '#f0e4d0',
}) })
.setOrigin(0.5); .setOrigin(0.5);
this.add this.add
.text(W / 2, 62, 'THE ROSTER · take a shift', { .text(W / 2, 38, 'THE ROSTER · take a shift', {
fontFamily: MONO, fontSize: '8px', color: '#a89878', fontFamily: MONO, fontSize: '8px', color: '#a89878',
}) })
.setOrigin(0.5); .setOrigin(0.5);

View File

@ -346,9 +346,33 @@ DOORSCENE = [
], ],
] ]
# ---- Between-shift screens ---------------------------------------------------
# The roster board and the back office are the two screens the player sees EVERY
# night that were still drawn entirely from coloured rectangles. Both are plates
# (keep_bg, no quantise): they fill their frame and are meant to be opaque.
BETWEEN = [
F("plate:corkboard", (560, 280), 4600,
"a battered cork noticeboard filling the whole frame, pinholes and old staples, "
"warm fluorescent light from above, a strip of timber frame at the edge, "
"NOTHING pinned to it, no paper, no notes, empty board",
gen=(1536, 768), quantise=False, keep_bg=True),
F("plate:backoffice", (640, 360), 4601,
"a cramped nightclub back office at night seen straight on, cheap laminate desk "
"surface across the lower half, dark panelled wall above, a wall calendar, "
"a taped-up roster, one tired ceiling bulb, NOTHING on the desk, empty desk",
gen=(1536, 864), quantise=False, keep_bg=True),
# The sorter is a UI hero at ~150px, not a 16px floor prop, so it keeps its
# full palette — quantised to 16 colours a grey machine turns to porridge.
Asset("sorter", "mesh", (128, 96),
"a battered desktop mechanical coin sorting machine, metal hopper on top, "
"five coin channels down the front, output tray at the base, worn enamel, "
f"{MESH_TAIL}", 4602, rake=58, quantise=False),
]
ALL: list[Asset] = [ ALL: list[Asset] = [
*ROYAL, *ELEVATE, *ROOM, *TEXTURES, *WALLS, *STAFF, *FILLER, *STREETS, *ROYAL, *ELEVATE, *ROOM, *TEXTURES, *WALLS, *STAFF, *FILLER, *STREETS,
*DOORSCENE, *DOORSCENE, *BETWEEN,
] ]
BY_KIND = {a.kind: a for a in ALL} BY_KIND = {a.kind: a for a in ALL}