Files
lemonade/src/components/TradingScreen.tsx
T
jcoffey-dev 7fcb863771 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.
2026-09-08 15:59:52 -07:00

118 lines
3.5 KiB
TypeScript

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>
)
}