Compare commits

...

2 Commits

Author SHA1 Message Date
type-two
25cef5983e Fable (integrator): ROUND 35 CLOSED — the first five minutes land; every v7.0 SYSTEM is built
qa.sh --strict --matrix: 7 passed · 0 failed · 0 warn · 0 skipped, MATRIX 10 towns green. New
STRICT smoke_first_five: the fresh game hunts a roster artist and the splash names hunt + cash
· the beats walked the REAL paths (② at the crate, ③ on the buy, the send-off on the ⚖) · a
returning save gets no tutorial · a second fresh browser hunts the SAME artist (seeded, not
random) · ?game=0 shows nothing · 0 console errors. THE CROSS-ROUND CATCH: the seeded want
moved a fresh-boot invariant (wants 1, not 0) and the R33 gate went red on the FULL run while
green solo — amended to a baseline snapshot, lesson filed in §35 (a feature that moves a boot
invariant must sweep every gate that assumed it; full runs exist for exactly this). Money shot:
docs/shots/v7_beta/first_five_splash.png. Remaining for v7.0: the tour + docs freeze + THE TAG
— which waits for John's hands (the alpha precedent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 20:13:02 +10:00
type-two
55b984e0b0 Lane C+B R35 (v7.0 close #2): THE FIRST FIVE MINUTES — arrive hunting, three beats walk the hustle
A FRESH game (day 1 · empty crate · empty auction house) seeds ONE artist want off
rng(citySeed,'firstwant',0) × the parody roster (dig.js ARTISTS exported) — the splash names
the mission before the first step ('You're hunting HILLS HOIST — $191 in your pocket'), and
the keeper rumors point at the same hunt from shop one. Three toast-beats ride EXISTING events
(zero new systems): ① find a record shop (on the go-click) → ② riffle · mind the guide · BUY
(first digOpen) → ③ sell at a counter or ⚖ LIST and sleep (first find, via wallet.onChange) →
the send-off on the first sale-or-listing (the listener removes itself). A returning save gets
its town back, not a tutorial (fresh-detection = state, no save flag — the delta law stays
clean); ?game=0 / ?classic=1 never reach the block. #pc-toast body-mounted (the #pc-game rule):
beats fire INSIDE shops, and a SLEEP taken at a counter now shows its morning paper.
2026-07-20 20:13:02 +10:00
8 changed files with 212 additions and 12 deletions

View File

@ -4,6 +4,19 @@
--- ---
## Round 35 (v7.0 close, wave 2) — THE FIRST FIVE MINUTES (Fable, solo, JING5)
A fresh game arrives HUNTING: one seeded artist want (`rng(citySeed,'firstwant',0)` × the
parody roster) named on the splash with the pocket money ("You're hunting HILLS HOIST — $191
in your pocket"), pointing the rumors and the tour at the same target. Three toast-beats walk
the hustle off existing events (walk-in → digOpen → first find → first sale-or-listing, then
the listener removes itself). Returning saves get no tutorial; ?game=0/classic never reach it.
#pc-toast body-mounted so beats and morning papers surface inside shops too. STRICT
`smoke_first_five` green (roster want + splash · real-path beats · returning/?game=0 controls
· cross-browser seed determinism). Full record: LANE_F_NOTES §35.
---
## Round 34 (v7.0 close, wave 1) — GODBAY: the offline auction (Fable, solo, JING5) ## Round 34 (v7.0 close, wave 1) — GODBAY: the offline auction (Fable, solo, JING5)
⚖ LIST from the crate (mail-in consignment) → `listings` on procity-save/1 → resolves the day ⚖ LIST from the crate (mail-in consignment) → `listings` on procity-save/1 → resolves the day

View File

@ -1870,3 +1870,21 @@ m3ultra. If a third machine joins the rotation, sync that dir (49 M) or re-run E
"⚖ SOLD Cane Toad Blues — hammer $22, $19 after fees" · the ride to melbourne hammered a "⚖ SOLD Cane Toad Blues — hammer $22, $19 after fees" · the ride to melbourne hammered a
listing on arrival · ?game=0 has no crate to list from · 0 console errors. listing on arrival · ?game=0 has no crate to list from · 0 console errors.
- G's later LIVE tier must match these laws (the ladder rule: the offline tier is the contract). - G's later LIVE tier must match these laws (the ladder rule: the offline tier is the contract).
## §35 — R35: THE FIRST FIVE MINUTES (Fable, solo, JING5)
Fresh-game detection = day 1 ∧ empty collection ∧ empty listings (no flag in the save — the
delta law stays clean; a returning empty-crate player re-seeing beats is onboarding working,
not a bug). Seeded first want: `rng(citySeed,'firstwant',0)` × dig.js ARTISTS (exported R35);
guarded on empty wants so the persisted want isn't re-added. Splash gains #pc-hunt (fresh+game
only). Beats: ① on the go-click (900ms), ② on first `procity:digOpen`, ③ on first find via
wallet.onChange, send-off on first sale-or-listing (listener self-removes). #pc-toast body-
mounted (z 31) — visible in interiors/map; hud.dispose removes it. Gate `smoke_first_five`
STRICT (see ROUND35_INSTRUCTIONS). All v7.0 SYSTEMS are now built — tour + freeze + tag remain,
tag gated on John's playtest.
- **Cross-round amendment the suite caught (the reason full runs exist):** R35's seeded first
want changed the FRESH-BOOT CONTRACT (wants starts at 1, not 0) and the R33 gate's three
fresh-boot assumptions went red on the full run — green solo, red together. Amended to a
baseline snapshot (base = wants at boot; card adds base+1; the rumor may name ANY live want;
buying clears ITS want back to base). A feature that moves a boot invariant must sweep every
gate that assumed it — grep the harness for the invariant before calling a round closed.

View File

@ -0,0 +1,36 @@
# PROCITY — Round 35 instructions (from Fable, integrator)
Date: 2026-07-20 · **R34 closed all green** (GODBAY hammers). Round 35 is **THE FIRST FIVE
MINUTES** ([V7_THE_GAME.md](../V7_THE_GAME.md) system #7 — "smallest system, biggest
stranger-facing payoff"), the last unbuilt v7.0 system before the tour/freeze/tag.
## What ships
1. **You arrive with a want.** A FRESH game (day 1, empty crate, empty auction house) seeds ONE
artist want off `rng(citySeed,'firstwant',0)` × C's parody roster (ARTISTS now exported) —
so the splash, the keeper rumors, and the tour all point at the same hunt on a given seed.
The splash names it: "You're hunting HILLS HOIST — $191 in your pocket." A returning save
gets its town back, not a tutorial; ?game=0 / classic never reach the block.
2. **Three toast-beats walk the hustle** (fresh games only): ① find a record shop / E at a
crate (on walk-in) → ② riffle · mind the guide · BUY (first digOpen) → ③ sell at a counter
or ⚖ LIST and sleep (first find banked) → the send-off ("that's the whole hustle") on the
first sale-or-listing. Driven off existing events + wallet.onChange — zero new systems.
3. **Toasts surface in every mode**#pc-toast moved to document.body (the #pc-game body-mount
rule): beats fire INSIDE shops, and a SLEEP taken at a counter now shows its morning paper.
## Gate (F): `smoke_first_five` (STRICT)
Fresh game: 1 seeded want from the roster + the splash names hunt AND cash · beats ②/③/send-off
through the REAL paths (E at crate / dig buy / crate ⚖) · a returning save gets NO tutorial
(splash line hidden, no ② on dig) · a second fresh browser hunts the SAME artist (seeded, not
random) · ?game=0 shows nothing · 0 console errors.
## ROUND CLOSED (Fable) — ALL GREEN
`qa.sh --strict --matrix`: 7 passed · 0 failed · 0 warn · 0 skipped, MATRIX 10 towns green
(final tree). Gate run: hunts HILLS HOIST with $191, beats walked through the real dig/buy/list
paths, returning-save and ?game=0 controls clean, second fresh browser hunts the same artist,
0 console errors. Money shot: `docs/shots/v7_beta/first_five_splash.png`.
**v7.0 remaining:** the tour ("day one to first gem") · docs freeze · the tag — and the tag
waits for John's playtest (the alpha precedent: hands before tags).

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

View File

@ -2158,6 +2158,9 @@ def smoke_wantlist_travel(p):
c.dispatchEvent(new PointerEvent('pointerdown', { clientX: 640, clientY: 360, bubbles: true })); c.dispatchEvent(new PointerEvent('pointerdown', { clientX: 640, clientY: 360, bubbles: true }));
c.dispatchEvent(new PointerEvent('pointerup', { clientX: 640, clientY: 360, bubbles: true })); }""") c.dispatchEvent(new PointerEvent('pointerup', { clientX: 640, clientY: 360, bubbles: true })); }""")
pg.wait_for_timeout(300) pg.wait_for_timeout(300)
# [R35 amendment] a FRESH game already hunts ONE seeded want (the first-five system) — this
# gate's fresh-boot baseline is that want, and the card adds a SECOND on top of it.
base = pg.evaluate("() => window.PROCITY.game.wants.length")
rw = pg.evaluate("""() => { rw = pg.evaluate("""() => {
const w = document.querySelector('.pcdg-panel .want'); const w = document.querySelector('.pcdg-panel .want');
if (!w) return { err: 'no WANT line on the card' }; if (!w) return { err: 'no WANT line on the card' };
@ -2165,11 +2168,14 @@ def smoke_wantlist_travel(p):
const g = window.PROCITY.game, ex = JSON.parse(g.export()); const g = window.PROCITY.game, ex = JSON.parse(g.export());
return { before, after: w.textContent, wants: ex.wants || [], live: g.wants.length }; }""") return { before, after: w.textContent, wants: ex.wants || [], live: g.wants.length }; }""")
if rw.get('err'): FAIL(f"wantlist: {rw['err']}"); return if rw.get('err'): FAIL(f"wantlist: {rw['err']}"); return
if 'WANT IT' in rw['before'] and 'ON THE WANTLIST' in rw['after'] and len(rw['wants']) == 1 and rw['live'] == 1: if 'WANT IT' in rw['before'] and 'ON THE WANTLIST' in rw['after'] and len(rw['wants']) == base + 1 and rw['live'] == base + 1:
OK(f"wantlist: ☆→★ through the real card — the save carries {rw['wants'][0]}") OK(f"wantlist: ☆→★ through the real card — the save carries {len(rw['wants'])} want(s) "
f"(the seeded hunt + {rw['wants'][-1]})")
else: else:
FAIL(f"wantlist: want did not take — {rw}") FAIL(f"wantlist: want did not take — base {base}, {rw}")
artist = (rw['wants'][0].get('artist') or rw['wants'][0].get('title') or '').upper() # the rumor may pick ANY current want (seeded index over the live list)
artists = [str(w.get('artist') or w.get('title') or '').upper() for w in rw['wants']]
artist = artists[-1]
pg.evaluate("""() => { const p = document.querySelector('.pcdg-panel'); pg.evaluate("""() => { const p = document.querySelector('.pcdg-panel');
const x = p && p.querySelector('.x'); if (x) x.click(); const x = p && p.querySelector('.x'); if (x) x.click();
const o = document.querySelector('.pcdg-out'); if (o) o.click(); }""") const o = document.querySelector('.pcdg-out'); if (o) o.click(); }""")
@ -2190,20 +2196,21 @@ def smoke_wantlist_travel(p):
seen.push(s.id); seen.push(s.id);
} }
return { rumor: null, tried: seen.length }; }""") return { rumor: null, tried: seen.length }; }""")
if rr.get('rumor') and artist and artist in rr['rumor']: if rr.get('rumor') and any(a and a in rr['rumor'] for a in artists):
OK(f"rumor: a keeper muttered after {rr['tried']} shop(s) — {rr['rumor'][:80]}") OK(f"rumor: a keeper muttered after {rr['tried']} shop(s) — {rr['rumor'][:80]}")
else: else:
FAIL(f"rumor: no keeper named {artist!r} in {rr.get('tried')} open shops — {rr}") FAIL(f"rumor: no keeper named any of {artists} in {rr.get('tried')} open shops — {rr}")
# (c) buy the wanted sleeve → the want clears itself # (c) buy the wanted sleeve → the want clears itself
r1 = _enter_record_shop(pg) r1 = _enter_record_shop(pg)
if r1.get('err'): FAIL(f"wantlist/buy: {r1['err']}"); return if r1.get('err'): FAIL(f"wantlist/buy: {r1['err']}"); return
_at_bin(pg) _at_bin(pg)
rb = _dig_buy(pg) rb = _dig_buy(pg)
rc = pg.evaluate("() => ({ wants: window.PROCITY.game.wants.length, n: window.PROCITY.game.collection.length })") rc = pg.evaluate("() => ({ wants: window.PROCITY.game.wants.length, n: window.PROCITY.game.collection.length })")
if not rb.get('err') and not rb.get('disabled') and rc['wants'] == 0 and rc['n'] >= 1: if not rb.get('err') and not rb.get('disabled') and rc['wants'] == base and rc['n'] >= 1:
OK('wantlist: bought the hunted sleeve — the want cleared itself, the find is in the crate') OK('wantlist: bought the hunted sleeve — ITS want cleared itself (the seeded hunt stays), '
'the find is in the crate')
else: else:
FAIL(f"wantlist/buy: {rb} · after {rc}") FAIL(f"wantlist/buy: {rb} · after {rc} (want base {base})")
base = pg.evaluate("() => ({ day: window.PROCITY.game.day, cash: window.PROCITY.game.cash, n: window.PROCITY.game.collection.length })") base = pg.evaluate("() => ({ day: window.PROCITY.game.day, cash: window.PROCITY.game.cash, n: window.PROCITY.game.collection.length })")
# (d) travel: another town on the SAME save — the ride costs exactly one day # (d) travel: another town on the SAME save — the ride costs exactly one day
boot(pg, 'plansrc=osm&town=melbourne') boot(pg, 'plansrc=osm&town=melbourne')
@ -2357,6 +2364,89 @@ def smoke_godbay(p):
b.close() b.close()
def smoke_first_five(p):
"""R35 gate — THE FIRST FIVE MINUTES (charter #7). Legs: (a) a FRESH game arrives with a seeded
first want (parody roster) and the splash names the hunt + the pocket money; (b) the want is
the SAME artist in a second fresh browser (seeded, not random); (c) the beats walk the hustle:
E at a crate '', a buy '', a LIST the send-off; (d) a RETURNING save gets no
tutorial (splash line hidden, no '' on dig); (e) ?game=0 shows no hunt line at all."""
head('GATE R35: THE FIRST FIVE MINUTES — seeded want · splash · beats (+ returning/?game=0 controls)')
ROSTER = {'THE VERANDAHS', 'DEZ & THE UTES', 'MERINO', 'HILLS HOIST', 'THE ESKIES', 'GOON BAG',
'STOBIE POLES', 'THE FIBROS', 'WARATAH', 'THONGS', 'THE CLOTHESLINE', 'GALAH'}
b, pg, errs = new_page(p)
try:
boot(pg, '')
fa = pg.evaluate("""() => { const g = window.PROCITY.game, h = document.getElementById('pc-hunt');
return { wants: g.wants.map(w => w.artist), cash: g.cash,
hunt: h ? { shown: h.style.display !== 'none', text: h.textContent } : null }; }""")
if (len(fa['wants']) == 1 and fa['wants'][0] in ROSTER and fa['hunt'] and fa['hunt']['shown']
and fa['wants'][0] in fa['hunt']['text'] and f"${fa['cash']}" in fa['hunt']['text']):
OK(f"first-five: fresh game hunts {fa['wants'][0]} with ${fa['cash']} — the splash says so")
else:
FAIL(f"first-five: seeded want/splash wrong — {fa}")
# (c) the beats, through the real paths
r0 = _enter_record_shop(pg)
if r0.get('err'): FAIL(f"first-five: {r0['err']}"); return
pg.evaluate("""() => { const g = window.PROCITY.game;
const ex = JSON.parse(g.export()); ex.cash = 5000; ex.savedAt = Date.now(); g.import(JSON.stringify(ex)); }""")
pg.evaluate("() => window.DBG.setSegment(2)")
_at_bin(pg)
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
pg.wait_for_timeout(350)
t2 = pg.evaluate("() => (document.getElementById('pc-toast') || {}).textContent || ''")
pg.evaluate("() => { const o = document.querySelector('.pcdg-out'); if (o) o.click(); }")
pg.wait_for_timeout(200)
_at_bin(pg)
rb = _dig_buy(pg)
t3 = pg.evaluate("() => (document.getElementById('pc-toast') || {}).textContent || ''")
rl = pg.evaluate("""() => {
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
const lb = document.querySelector('#pc-crate .pc-crate-row div[title*="GodBay"]');
if (lb) lb.click();
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
return (document.getElementById('pc-toast') || {}).textContent || ''; }""")
if '' in t2 and '' in t3 and 'whole hustle' in rl:
OK('first-five: the beats walked the hustle — ② at the crate, ③ on the buy, the send-off on the ⚖')
else:
FAIL(f"first-five: beats wrong — ②:{('' in t2)} ③:{('' in t3)} send-off:{('whole hustle' in rl)}")
# (d) a returning save gets no tutorial
boot(pg, '')
rd = pg.evaluate("""() => { const h = document.getElementById('pc-hunt');
return { shown: h && h.style.display !== 'none', n: window.PROCITY.game.collection.length,
nl: window.PROCITY.game.listings.length }; }""")
r1 = _enter_record_shop(pg)
if r1.get('err'): FAIL(f"first-five/return: {r1['err']}")
else:
_at_bin(pg)
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
pg.wait_for_timeout(350)
tr = pg.evaluate("""() => { const t = document.getElementById('pc-toast');
return { text: t ? t.textContent : '', vis: t ? t.style.opacity : '0' }; }""")
if not rd['shown'] and not ('' in tr['text'] and tr['vis'] == '1'):
OK(f"first-five/return: a save with {rd['n']} find(s)/{rd['nl']} listing(s) gets no tutorial")
else:
FAIL(f"first-five/return: tutorial leaked to a returning save — {rd} {tr}")
# (b) determinism: a second FRESH browser hunts the same artist
b2, pg2, _ = new_page(p)
try:
boot(pg2, '')
w2 = pg2.evaluate("() => window.PROCITY.game.wants.map(w => w.artist)")
if w2 == fa['wants']: OK(f"first-five: a second fresh browser hunts the same {w2[0]} — seeded, not random")
else: FAIL(f"first-five: fresh want differs across browsers — {fa['wants']} vs {w2}")
finally:
b2.close()
# (e) ?game=0 — no hunt line
boot(pg, 'game=0')
g0 = pg.evaluate("""() => { const h = document.getElementById('pc-hunt');
return h ? h.style.display !== 'none' : false; }""")
if not g0: OK('first-five/control: ?game=0 — no hunt, no tutorial')
else: FAIL('first-five/control: game=0 shows the hunt line')
if errs: FAIL(f"first-five: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK('first-five: 0 console errors')
finally:
b.close()
def smoke_classic_purity(p): def smoke_classic_purity(p):
"""R30 gate (c) — ?classic=1 and ?game=0: ZERO Storage-prototype calls (instrumented before any """R30 gate (c) — ?classic=1 and ?game=0: ZERO Storage-prototype calls (instrumented before any
page script), zero game DOM (B's ids), game null. Default boot is the POSITIVE control (the page script), zero game DOM (B's ids), game null. Default boot is the POSITIVE control (the
@ -2513,6 +2603,7 @@ def main():
smoke_bible(p) # R32 — THE BIBLE (bands · gems · ledger) + per-day scarcity + ?game=0 control smoke_bible(p) # R32 — THE BIBLE (bands · gems · ledger) + per-day scarcity + ?game=0 control
smoke_wantlist_travel(p) # R33 — wantlist · keeper rumor · travel-costs-a-day (+ controls) smoke_wantlist_travel(p) # R33 — wantlist · keeper rumor · travel-costs-a-day (+ controls)
smoke_godbay(p) # R34 — GODBAY offline auction (one hammer · bounded · morning paper) smoke_godbay(p) # R34 — GODBAY offline auction (one hammer · bounded · morning paper)
smoke_first_five(p) # R35 — the first five minutes (seeded want · splash · beats + controls)
smoke_tier2(p) # R27 — the kill-the-server gate (charter law #1) smoke_tier2(p) # R27 — the kill-the-server gate (charter law #1)
smoke_real_crate(p) # R24 — THE REAL CRATE (per-shop atlas identity · soft fall · no-op off the path) smoke_real_crate(p) # R24 — THE REAL CRATE (per-shop atlas identity · soft fall · no-op off the path)
finally: finally:

View File

@ -32,6 +32,7 @@
<h1>PROCITY</h1> <h1>PROCITY</h1>
<div class="sub">A procedurally generated, walkable 90s-Australian shopping town. <div class="sub">A procedurally generated, walkable 90s-Australian shopping town.
Every door opens. Dig the crates, sell the flips, sleep — the town remembers.</div> Every door opens. Dig the crates, sell the flips, sleep — the town remembers.</div>
<div class="sub" id="pc-hunt" style="color:#ffd75e;display:none"></div>
<button id="pc-go">walk into town</button> <button id="pc-go">walk into town</button>
<div class="seed" id="pc-startseed"></div> <div class="seed" id="pc-startseed"></div>
<div class="sub" style="font-size:12px;opacity:.7">WASD move · shift run · mouse look · click a door · E riffle a bin / sell at the counter · C your crate · SLEEP to save · [ ] time · M map</div> <div class="sub" style="font-size:12px;opacity:.7">WASD move · shift run · mouse look · click a door · E riffle a bin / sell at the counter · C your crate · SLEEP to save · [ ] time · M map</div>
@ -63,6 +64,8 @@ import { loadPedFleet } from './js/citizens/rigs.js'; // [Lane F §3
import { VenueQueue } from './js/citizens/queue.js'; // [Lane F R13] Lane D outdoor gig queue (?gigs=1) import { VenueQueue } from './js/citizens/queue.js'; // [Lane F R13] Lane D outdoor gig queue (?gigs=1)
import { preloadManifest, preloadStockPack } from './js/interiors/interiors.js'; // [Lane F] Lane E manifest + Lane C/E stock pack import { preloadManifest, preloadStockPack } from './js/interiors/interiors.js'; // [Lane F] Lane E manifest + Lane C/E stock pack
import { createGame } from './js/world/save.js'; // [Lane F R30] v7 THE SAVE CORE (contract: LANE_F_NOTES §30) import { createGame } from './js/world/save.js'; // [Lane F R30] v7 THE SAVE CORE (contract: LANE_F_NOTES §30)
import { ARTISTS } from './js/interiors/dig.js'; // [R35] the parody roster — a fresh game's seeded first want
import { rng, pick } from './js/core/prng.js'; // [R35] seeded first-want draw (runtime stream, fresh key)
main().catch((err) => { main().catch((err) => {
console.error('[procity] init failed:', err); console.error('[procity] init failed:', err);
@ -292,6 +295,40 @@ if (game) {
+ (bootNews.length ? ' · ' + bootNews.join(' · ') : ''), bootNews.length ? 7 : 4); + (bootNews.length ? ' · ' + bootNews.join(' · ') : ''), bootNews.length ? 7 : 4);
else if (bootNews.length) hud.showToast(bootNews.join(' · '), 6); else if (bootNews.length) hud.showToast(bootNews.join(' · '), 6);
} }
// ── [R35 — THE FIRST FIVE MINUTES (charter #7)] ────────────────────────────────────────────────
// A stranger arrives with cash and A WANT, and three toast-beats walk the whole hustle: dig →
// buy → sell-or-list. FRESH games only (day 1, empty crate, empty auction house) — a returning
// save gets its town back, not a tutorial. ?game=0 / ?classic=1 never reach this block. The
// first want is SEEDED (citySeed → the parody roster), so the splash line, the keeper rumors,
// and the tour all point at the same hunt on a given seed — deterministic, gate-checkable.
if (game && game.day === 1 && !game.collection.length && !game.listings.length) {
if (!game.wants.length) game.recordWant({ artist: pick(rng(plan.citySeed, 'firstwant', 0), ARTISTS) });
const hunt = document.getElementById('pc-hunt');
if (hunt && game.wants.length) {
hunt.textContent = `You're hunting ${game.wants[0].artist} — $${game.cash} in your pocket.`;
hunt.style.display = 'block';
}
let stage = 1;
const go = document.getElementById('pc-go');
if (go) go.addEventListener('click', () =>
setTimeout(() => { if (stage === 1) hud.showToast('① find a record shop — walk in, press E at a crate', 7); }, 900),
{ once: true });
window.addEventListener('procity:digOpen', () => {
if (stage === 1) { stage = 2; hud.showToast('② riffle with the wheel · pull a sleeve · mind the guide — BUY', 7); }
});
let n3 = 0;
const offBeats = game.wallet.onChange(() => {
if (stage <= 2 && game.collection.length) {
stage = 3; n3 = game.collection.length;
hud.showToast('③ yours now. Sell it at a counter (E) — or ⚖ LIST it in the crate and SLEEP on it', 8);
} else if (stage === 3 && (game.listings.length || game.collection.length < n3)) {
stage = 4;
hud.showToast('Thats the whole hustle. ☆ want things · keepers gossip · 🚌 towns cost a day. Good digging.', 9);
offBeats();
}
});
}
// [Lane F R9] occupancyOf closure defers to `citizens` (declared just below); only invoked at enter() // [Lane F R9] occupancyOf closure defers to `citizens` (declared just below); only invoked at enter()
// time (door click), long after citizens is initialized — so the forward reference is safe. // time (door click), long after citizens is initialized — so the forward reference is safe.
const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS, const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS,

View File

@ -17,8 +17,9 @@ const SLEEVE = [0.31, 0.31, 0.0060]; // w, h, thickness (packed a touc
// 90s-Australian crate genres → sleeve card colour. // 90s-Australian crate genres → sleeve card colour.
const GENRE = [['ROCK', '#7a2a3a'], ['SOUL', '#b0894a'], ['JAZZ', '#2a4a6a'], ['DISCO', '#8a3a7a'], const GENRE = [['ROCK', '#7a2a3a'], ['SOUL', '#b0894a'], ['JAZZ', '#2a4a6a'], ['DISCO', '#8a3a7a'],
['COUNTRY', '#6a5a2a'], ['PUNK', '#3a3a3a'], ['REGGAE', '#2a6a4a'], ['POP', '#b05a8a']]; ['COUNTRY', '#6a5a2a'], ['PUNK', '#3a3a3a'], ['REGGAE', '#2a6a4a'], ['POP', '#b05a8a']];
const ARTISTS = ['THE VERANDAHS', 'DEZ & THE UTES', 'MERINO', 'HILLS HOIST', 'THE ESKIES', 'GOON BAG', // [R35] exported: the shell seeds a fresh game's FIRST WANT from this roster (THE FIRST FIVE MINUTES).
'STOBIE POLES', 'THE FIBROS', 'WARATAH', 'THONGS', 'THE CLOTHESLINE', 'GALAH']; export const ARTISTS = ['THE VERANDAHS', 'DEZ & THE UTES', 'MERINO', 'HILLS HOIST', 'THE ESKIES', 'GOON BAG',
'STOBIE POLES', 'THE FIBROS', 'WARATAH', 'THONGS', 'THE CLOTHESLINE', 'GALAH'];
const TITLES = ['Sunburnt', 'Back o Bourke', 'Servo at Midnight', 'Long Weekend', 'Tinny', const TITLES = ['Sunburnt', 'Back o Bourke', 'Servo at Midnight', 'Long Weekend', 'Tinny',
'Southerly Buster', 'Milk Bar Dreams', 'Cane Toad Blues', 'Ute Muster', 'Arvo']; 'Southerly Buster', 'Milk Bar Dreams', 'Cane Toad Blues', 'Ute Muster', 'Arvo'];
const CONDS = ['VG+', 'VG', 'G+', 'NM', 'EX']; const CONDS = ['VG+', 'VG', 'G+', 'NM', 'EX'];

View File

@ -21,7 +21,7 @@ const CSS = `
#pc-dbg b{color:#ffd75e;font-weight:600} #pc-dbg b{color:#ffd75e;font-weight:600}
#pc-dbg .warn{color:#ff9a6a} #pc-dbg .warn{color:#ff9a6a}
#pc-clock{font-size:15px;margin-bottom:3px} #pc-clock{font-size:15px;margin-bottom:3px}
#pc-toast{position:absolute;left:50%;bottom:64px;transform:translateX(-50%);background:rgba(20,16,10,.9);padding:10px 18px;border-radius:10px;font-size:15px;opacity:0;transition:opacity .25s;text-shadow:0 1px 2px #000} #pc-toast{position:fixed;left:50%;bottom:64px;transform:translateX(-50%);background:rgba(20,16,10,.9);padding:10px 18px;border-radius:10px;font-size:15px;opacity:0;transition:opacity .25s;text-shadow:0 1px 2px #000;z-index:31;max-width:80vw;font:15px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;color:#f4efe6;pointer-events:none}
#pc-help{position:absolute;left:12px;bottom:12px;background:rgba(20,16,10,.5);padding:7px 11px;border-radius:8px;opacity:.85} #pc-help{position:absolute;left:12px;bottom:12px;background:rgba(20,16,10,.5);padding:7px 11px;border-radius:8px;opacity:.85}
#pc-townpick{position:absolute;left:10px;top:10px;pointer-events:auto;opacity:.8;transition:opacity .12s} #pc-townpick{position:absolute;left:10px;top:10px;pointer-events:auto;opacity:.8;transition:opacity .12s}
#pc-townpick:hover{opacity:1} #pc-townpick:hover{opacity:1}
@ -67,6 +67,9 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
const $ = (id) => root.querySelector('#' + id); const $ = (id) => root.querySelector('#' + id);
const elTip = $('pc-tip'), elClock = $('pc-clock'), elFps = $('pc-fps'), elDraws = $('pc-draws'), const elTip = $('pc-tip'), elClock = $('pc-clock'), elFps = $('pc-fps'), elDraws = $('pc-draws'),
elTris = $('pc-tris'), elChunks = $('pc-chunks'), elToast = $('pc-toast'); elTris = $('pc-tris'), elChunks = $('pc-chunks'), elToast = $('pc-toast');
// [R35] toasts surface in EVERY mode (beats fire inside shops; a SLEEP taken at a counter must
// still show its morning paper) — same body-mount rule as #pc-game, same dispose duty.
document.body.appendChild(elToast);
$('pc-town').textContent = plan.name || '—'; $('pc-town').textContent = plan.name || '—';
$('pc-seed').textContent = plan.citySeed; $('pc-seed').textContent = plan.citySeed;
@ -379,6 +382,7 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
renderer.domElement.removeEventListener('click', onClick); renderer.domElement.removeEventListener('click', onClick);
if (onGameKey) removeEventListener('keydown', onGameKey); if (onGameKey) removeEventListener('keydown', onGameKey);
if (gameUI && gameUI.wrap) gameUI.wrap.remove(); // [R32] the game surface lives on document.body now if (gameUI && gameUI.wrap) gameUI.wrap.remove(); // [R32] the game surface lives on document.body now
elToast.remove(); // [R35] so does the toast
root.remove(); root.remove();
} }