Remaster: cel-shaded skin, a crowd that tracks trade, and a funk band

Two skins over one game. 1979 is the machine as it was; the remaster is
the same rules in cel-shaded vector art with a modern palette. The
simulation is untouched - only the paint differs - and the toggle sits
under the set.

The street is now driven by the game. Before prices are in, the number
of people walking on comes from the forecast; afterwards it comes from
the glasses that actually sold, so the report shows the crowd you
earned. A heat wave has them fanning themselves; a downpour leaves one
figure hurrying past under an umbrella.

Selling no longer snaps straight to the books. The stand trades for a
few seconds first, tally climbing and till filling, which is where that
crowd animation pays off. Skippable.

The synth grew a kit (pitched-sine kick, snare with body, hats), a
sweeping resonant lowpass for the bass, chords and a shuffle - enough
for a funk band. The chiptunes stay for the 1979 skin.

Boot is now a cassette load, which doubles as the gesture browsers
require before audio will play.

Fixes found on the way: the picture scaled about its centre while flex
auto-margins collapsed to zero on overflow, which pushed the report's
buttons off the bottom; and the validation line moved the button when
it appeared, so it is now reserved and faded. Sibling tabs in one
browser also keep their score tables in step.
This commit is contained in:
2026-09-08 15:59:52 -07:00
parent 175589223a
commit 7fcb863771
23 changed files with 1909 additions and 370 deletions
+117
View File
@@ -0,0 +1,117 @@
import { useEffect, useRef, useState } from 'react'
import { soldBusyness } from '../game/engine'
import type { DayConditions, DayResult, Player } from '../game/types'
import { synth } from '../audio/synth'
import { Btn, Line } from './Crt'
import { Scene } from './Scene'
const TRADING_MS = 2900
const STORM_MS = 3400
/** Fast at first, settling at the end, the way a rush tails off. */
const easeOut = (p: number) => 1 - Math.pow(1 - p, 2.2)
export function TradingScreen({
results,
players,
conditions,
onDone,
}: {
results: DayResult[]
players: Player[]
conditions: DayConditions
onDone: () => void
}) {
const [progress, setProgress] = useState(0)
const ticked = useRef(0)
useEffect(() => {
const total = results.reduce((n, r) => n + r.glassesSold, 0)
const span = conditions.storm ? STORM_MS : TRADING_MS
const startedAt = performance.now()
let raf = 0
if (conditions.storm) synth.thunder()
const frame = (now: number) => {
const p = Math.min(1, (now - startedAt) / span)
setProgress(p)
// A blip per few glasses, so a busy day sounds busy.
if (!conditions.storm && total > 0) {
const sold = Math.round(easeOut(p) * total)
const step = Math.max(1, Math.ceil(total / 14))
if (sold >= ticked.current + step) {
ticked.current = sold
synth.tick()
}
}
if (p < 1) raf = requestAnimationFrame(frame)
else onDone()
}
raf = requestAnimationFrame(frame)
return () => cancelAnimationFrame(raf)
}, [results, conditions.storm, onDone])
const eased = easeOut(progress)
const sold = (r: DayResult) => Math.round(eased * r.glassesSold)
const soldTotal = results.reduce((n, r) => n + sold(r), 0)
const madeTotal = results.reduce((n, r) => n + r.decision.glasses, 0)
if (conditions.storm) {
return (
<div className="stack">
<Line className="center inv-line">DAY {conditions.day} IN LEMONSVILLE</Line>
<Scene conditions={conditions} traffic={0.2} />
<Line className="warn">A THUNDERSTORM HIT LEMONSVILLE EARLIER</Line>
<Line className="warn">TODAY, JUST AS THE STANDS WERE BEING</Line>
<Line className="warn">SET UP. EVERYTHING WAS RUINED!!</Line>
<Line />
<div className="row center">
<Btn kind="primary" onClick={onDone}>
SEE THE DAMAGE
</Btn>
</div>
</div>
)
}
return (
<div className="stack">
<Line className="center inv-line">DAY {conditions.day} &mdash; OPEN FOR BUSINESS</Line>
<Scene
conditions={conditions}
price={results[0].decision.price}
traffic={soldBusyness(results[0].glassesSold)}
/>
<Line className="center accent">
{soldTotal} OF {madeTotal} GLASSES SOLD
</Line>
<div className="till" aria-hidden>
<span style={{ width: `${Math.round(progress * 100)}%` }} />
</div>
{results.length > 1 &&
results.map((r) => {
const player = players.find((p) => p.id === r.playerId)!
return (
<div className="report-row" key={r.playerId}>
<span className="report-label">{player.name}</span>
<span className="report-dots" aria-hidden />
<span className="report-value">{sold(r)}</span>
</div>
)
})}
<Line />
<div className="row center">
<Btn kind="ghost" onClick={onDone}>
SKIP TO THE BOOKS
</Btn>
</div>
</div>
)
}