The state machine
Screens as a pure view->HTML map, one state global, and input by event delegation.
The game is a state machine with about 21 screens. The current screen is a string, S.view. Rendering is a pure view -> HTML string map, and input is event delegation. Those two facts are the whole UI architecture.
Rendering: a switch that returns strings
2328 function viewHtml() { 2329 var v = S ? S.view : "title"; 2330 switch (v) { 2331 case "title": return viewTitle(); 2332 case "tours": return viewTours(); 2333 case "campaign": return viewCampaign(); 2334 case "role": return viewRole(); 2335 case "crew": return viewCrew(); 2336 case "builder": return viewBuilder(); 2337 case "van": return viewVan(); 2338 case "stash": return viewStash(); 2339 case "library": return viewLibrary(); 2340 case "peak": return viewPeak(); 2341 case "lot": return viewLot(); 2342 case "confirm-drive": return viewConfirmDrive();
Because each viewX() returns markup as a string, tests can render any screen headlessly and assert on it — that is why the test surface exposes viewHtml(). render() is the only function that touches the DOM, and only when there is one:
2363 function render() { 2364 var html = viewHtml(); 2365 if (!HAS_DOM) return; // headless: state is updated, nothing to paint 2366 if (S && STOPS && STOPS[S.stop]) prefetchSetlist(STOPS[S.stop]); 2367 if (!app) app = document.getElementById("app"); 2368 app.innerHTML = bandify(html); 2369 var v = S ? S.view : "title"; 2370 if (S && typeof S.restoreScrollTo === "number") { window.scrollTo(0, S.restoreScrollTo); S.restoreScrollTo = null; } 2371 // Only jump to the top when the screen actually changes -- an in-place update 2372 // (buying gear, toggling a mod, installing a van part...) leaves scroll alone. 2373 else if (v !== lastRenderedView) { window.scrollTo(0, 0); } 2374 lastRenderedView = v; 2375 if (v === "driving") startDriveAnim(); 2376 if (v === "mini") startMiniLoop(); 2377 // Snapshot the run whenever the player is at the lot hub (a clean save point). 2378 if (S && v === "lot") saveGame(S); 2379 updateTrailMap(); 2380 updateMusicPanel(); 2381 applyShortcutHints(); 2382 }
Note the details that matter: it early-returns headless; it only scrolls to top when the view changes (so buying gear doesn't jump you around); and it snapshots the save whenever you land on lot. Save-on-lot lives here, not in the lot actions, precisely because render() is the one choke point every screen passes through.
Input: one listener, a dispatcher
There are no per-button handlers. Buttons carry a data-action attribute; a single document click listener finds the nearest one and hands the action string to handle():
3431 function onClick(e) { 3432 var t = e.target.closest("[data-action]"); 3433 if (!t) return; 3434 // first gesture unlocks audio; a soft tick gives every button tactile feedback 3435 Sound.init(); Sound.resume(); Sound.tap(); 3436 var a = t.getAttribute("data-action"); 3437 handle(a); 3438 } 3439 3440 function handle(a) { 3441 // navigation 3442 if (a === "go-title") { S = S || {}; S.view = "title"; render(); return; } 3443 if (a === "go-about") { S = S || {}; S.view = "about"; fetchBlogPosts(); render(); return; } 3444 if (a === "start-role") { S = S || {}; S.view = "role"; render(); return; } 3445 if (a === "go-tours") { S = S || {}; S.tourPreview = TOUR_YEAR; S.view = "tours"; render(); return; }
Actions are namespaced by prefix (pick-, mod-, gearbuy-, install-, ev-, van-, set-pace-, …), and plain <a> links carry no data-action, so the delegate ignores them and the browser just follows the href. Live text inputs (crew renaming, the van name) use a separate input listener that updates state without re-rendering, so focus isn't lost mid-keystroke.
The screen flow
title -> tour -> (campaign) -> role -> builder -> crew -> (van) -> LOT
LOT is the hub: shakedown / work / tape / van / stash / rest / drive
drive -> driving -> [events...] -> (peak) -> arrive -> LOT finale -> win / gameover
The lot is the hub; nearly everything returns there. A driving leg is its own sub-flow, covered next.
S.view+viewHtml()'s switch is the entire screen system; builders are pure string functions.render()is the single DOM choke point — and therefore the right home for save-on-lot and scroll logic.- Input is one delegated listener into
handle(); actions are prefix-namespaced strings.