Compare commits

..

No commits in common. "25cef5983e63fca204da2f9793f0c2fc1a530a20" and "c9e233a7e5613e291f006db805af9753362b9134" have entirely different histories.

8 changed files with 12 additions and 212 deletions

View File

@ -4,19 +4,6 @@
--- ---
## 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,21 +1870,3 @@ 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

@ -1,36 +0,0 @@
# 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.

Before

Width:  |  Height:  |  Size: 378 KiB

View File

@ -2158,9 +2158,6 @@ 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' };
@ -2168,14 +2165,11 @@ 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']) == base + 1 and rw['live'] == base + 1: if 'WANT IT' in rw['before'] and 'ON THE WANTLIST' in rw['after'] and len(rw['wants']) == 1 and rw['live'] == 1:
OK(f"wantlist: ☆→★ through the real card — the save carries {len(rw['wants'])} want(s) " OK(f"wantlist: ☆→★ through the real card — the save carries {rw['wants'][0]}")
f"(the seeded hunt + {rw['wants'][-1]})")
else: else:
FAIL(f"wantlist: want did not take — base {base}, {rw}") FAIL(f"wantlist: want did not take — {rw}")
# the rumor may pick ANY current want (seeded index over the live list) artist = (rw['wants'][0].get('artist') or rw['wants'][0].get('title') or '').upper()
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(); }""")
@ -2196,21 +2190,20 @@ 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 any(a and a in rr['rumor'] for a in artists): if rr.get('rumor') and artist and artist in rr['rumor']:
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 any of {artists} in {rr.get('tried')} open shops — {rr}") FAIL(f"rumor: no keeper named {artist!r} 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'] == base and rc['n'] >= 1: if not rb.get('err') and not rb.get('disabled') and rc['wants'] == 0 and rc['n'] >= 1:
OK('wantlist: bought the hunted sleeve — ITS want cleared itself (the seeded hunt stays), ' OK('wantlist: bought the hunted sleeve — the want cleared itself, the find is in the crate')
'the find is in the crate')
else: else:
FAIL(f"wantlist/buy: {rb} · after {rc} (want base {base})") FAIL(f"wantlist/buy: {rb} · after {rc}")
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')
@ -2364,89 +2357,6 @@ 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
@ -2603,7 +2513,6 @@ 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,7 +32,6 @@
<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>
@ -64,8 +63,6 @@ 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);
@ -295,40 +292,6 @@ 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,9 +17,8 @@ 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']];
// [R35] exported: the shell seeds a fresh game's FIRST WANT from this roster (THE FIRST FIVE MINUTES). const ARTISTS = ['THE VERANDAHS', 'DEZ & THE UTES', 'MERINO', 'HILLS HOIST', 'THE ESKIES', 'GOON BAG',
export const ARTISTS = ['THE VERANDAHS', 'DEZ & THE UTES', 'MERINO', 'HILLS HOIST', 'THE ESKIES', 'GOON BAG', 'STOBIE POLES', 'THE FIBROS', 'WARATAH', 'THONGS', 'THE CLOTHESLINE', 'GALAH'];
'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: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-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-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,9 +67,6 @@ 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;
@ -382,7 +379,6 @@ 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();
} }