Skip to content

apps/client/src/ui ​

The overlay: React 19 components laid over the canvas as plain HTML, translated with i18next, mounted once by mount.tsx. React is for static overlays only: the helper UI (the legend and its tabs, the toolbar, the account and server forms, the game pages, the chat) and the stats and the game's events (the HUD and its hit log, the Wanted stars and the Heat, the stealth badge, the clock, the minimap). What is part of the game (the prompt and the labels that follow something in the world, the phone, the mission panel and a race's readout, the inventory strip and the panels built on slots) is drawn in the scene with Babylon's GUI by ../gui, at the frame rate. The game loop never imports React: it publishes into the store (store.ts, read with useSyncExternalStore) and calls nothing in here; the overlay reads the store and calls the callbacks main.ts hands it, never the simulation. The pure view modules of the in-game UI (phone-view.ts, inventory-view.ts, mission-format.ts, minimap-draw.ts) live here and import nothing of React.

FileWhatTested by
Overlay.tsxThe root: the HUD block (<pre id="hud">, the e2e tests read its text), the Wanted level, the stealth badge, the clock, the chat, the legend and the minimap; exports OverlayState, OverlayProps and PartPropse2e
MainMenu.tsxThe main menu over the burning car: the title screen, the list (Play online, Play local in the desktop app, Community levels, Galleries, Settings, Controls, Admin for an admin of the page's platform, Quit in the desktop app); Play online's cities with their pictures, its platform asked first (#menu-online-status[data-state]), greyed out until it answers with this game's protocol, its address typed there (#menu-platform); Play local's Host (the app's own server on a city) and Join (#menu-join-address), where friends join (#menu-hosting); the galleries; keys and cues; #main-menu, items #menu-<id>
PauseMenu.tsxThe pause menu (Esc): Resume, Settings, Controls, Quit to the main menu; whether the city waits; where friends on the network join while the app hosts (#pause-hosting); #pause-menu, #pause-state, items #pause-<id>
SettingsPanel.tsxThe Settings screen of both menus: the rows of settings.ts in four sections, sliders of segments and switches, a slider that tunes a switch dimmed while it is off (the zoom out at speed's strength and response)
settings.tsThe player's settings (volumes, controls, the HUD, the camera's height and its zoom out at speed; the keys the player rebound, only where they differ; the platform's address and the last address joined, edited on the menu's screens that use them), kept in localStorage (foi.settings) as a store, ranges, rows and nudgeSetting
bindings.tsKey remapping: the actions, their default keys (today's), the fixed keys (Esc, Enter, the number keys), rebind (a swap), the keyboard's translator (a key to the default key of its action) and the labels every text shows (keyLabels, i18next's default variable k.<action>, set by i18n.ts's followKeys)
boot.tsWhat the page starts as by its address (the menu, on a way to play's screen with ?menu=online or local; a gallery, a game page, the game), ?level= on the game server's socket, the menu's address from a game (back to the screen of its from)
legal.tsThe privacy policy's and the terms' version and date, and who runs the platform as they name them (/info's legal, neutral words without)
LegalPage.tsxThe privacy policy or the terms as a page of its own (?privacy, ?terms): their sections from the locales, the platform's facts filled in, the language button, the other document
ConsentDialog.tsxThe first start's question over the main menu: crash reports and anonymous statistics, both off until the player says yes (#consent, #consent-yes, -no, -save)
telemetry.tsCrash reports and usage events, sent only with the player's consent to the page's platform: an install id the player can renew, scrubbed stacks, the page's errors watched, a minute's frame times; the desktop app's main process told of the consent (shareConsent)
online.tsWhere the menu's two ways to play go: Play online's server (the page's, or the one a platform names), the addresses of a city online, hosted and joined (`from=onlinelocal`), a friend's address as read out, a platform's as typed
versions.tsWhether a server (its welcome, a platform's /info) plays with this game: the same protocol version; else both releases, for the player
menu-levels.tsThe levels the menu offers (the ones that ship, their games' titles and blurbs) and their pictures under previews/
menu.cssThe menus' look: letterbox bars, scanlines, the stepped selection, the readouts, the Settings rows
Legend.tsxThe card in the bottom-left corner: six tabs (keys, view, chasers, games, social, server; WAI-ARIA tabs, arrow keys move; the Social tab asks the platform again when picked), the toolbar under them, Hide and the Legend buttone2e (E2E-13, 43, 62–64)
KeysList.tsxThe key legend, in the order of the keys in locales/en.json, the keys named as the player bound them; on the menus' Controls screen a button per action changes its key (#bind-<action>; a key another action had swaps, Esc gives up, the fixed keys cannot be taken) and #bind-defaults puts every key backe2e (E2E-13)
Toolbar.tsxPause, language, music, HUD and reset-level buttons (the reset greyed out in a run’s room, whose level nobody resets, with the reason beside it: aria-disabled, #reset-level-run)
ViewToggles.tsxThe NPC view cones and the collision wireframe boxese2e (E2E-44, 47, 48) ; Go to (a road of the level), the clock (an hour and the pace) and the weather, debug commands (DebugOnly.tsx; E2E-670, 672)
Checkbox.tsxThe labelled checkbox every on/off switch of the legend usese2e (E2E-44, 48, 50, 58, 59)
ChasersPanel.tsxThe Chasers tab: pager, pages, the armed box and the spawn and clear buttons, debug commands (DebugOnly.tsx)e2e (E2E-50, 52, 57–59; the real mouse and the keyboard 670, 671; off 672)
DebugOnly.tsxThe legend’s debug options greyed out (a disabled fieldset) on a server whose welcome says debug: false (a platform’s rooms, no FOI_DEBUG=1), under a line that says why, instead of doing nothing; an older server’s welcome without it leaves them offered (debugOff)e2e (E2E-672); DebugOnly.test.ts
ChaserPages.tsxIts Style, Car tactics and Player pagese2e (E2E-57–59)
chaser-request.tsThe panel's state as a pure reducer (preset per style, crew bounds, page wrap) and the request it turns intochaser-request.test
GamesPanel.tsxThe Games tab: the account, the games and their rooms (Play, Join), the link to the game pagese2e (E2E-74, 85, 86, 171)
AccountBar.tsxWho the player is on the platform (the device's guest or an account), the email sign-in and sign-out, a link to the admin page for an admin (#browse-admin); Steam's sign-in, linking an e-mail account to Steam, a refusal with "Get it on Steam" (#browse-steam…); ids by prefix (games-…, browse-…)e2e (E2E-74, 175, 585–593)
SocialPanel.tsxThe Social tab: friends (#social-friend-name, #social-add-friend, rows #social-friends li[data-friend] with Accept, Join, Invite, Message, Remove; #social-refresh), the party (#social-party-create, #social-party, #social-party-leave, invitations #social-invites li[data-party]), the room's players (#social-room li[data-name]: Mute, Vote out, Report, Add friend)e2e (E2E-210–219)
ChatPanel.tsxThe chat (#chat, #chat-lines): closed, the last lines a while and no key taken; open (Enter), the field (#chat-input), the channel (#chat-channel: the room, the party, a friend), the quick-chat phrases (#chat-quick-<id>) and the emotes (#chat-emote-<id>), Esc closes; the vote to kick (#chat-vote, #chat-vote-yes, #chat-vote-no)e2e (E2E-213–218, 220)
social-view.tsThe chat log as pure data: the room's lines and the feed's merged, a muted player's left out, the lines shown closed and open, how a phrase, an emote or the room's own line reads, who may be voted out, the room a page plays insocial-view.test
hooks.tsusePlatform (the page's platform client) and useRunner (a call at a time, its error)e2e
platform.tsThe platform API from the page: where it is (the page's ?api=, the player's address, the build's FOI_PLATFORM_URL, the page's own /api; none from a file), what a platform says of itself (/info), the guest session, the games (searched, sorted), a game's page and preview, rooms, ratings, reports, remixes; who the session is (/me: its roles, limits and today's use) and whether the session kept is an admin's, asked without making a guest; Steam's sign-ins, no guest where refused (PlatformRefusal)platform.test
BrowsePage.tsxThe game pages, shown instead of the game: the page of games (?browse: search, sorts, the lists of friends' games and Your games, cards with the preview, title, creator, rating and blurb), a game's page, the admin page or the creation page; the account and the language abovee2e (E2E-170, 176, 585)
YourGames.tsxYour games (?browse&list=mine): the creator's drafts with their last build (a waiting build's place in the queue, data-position, the list asked again while a build waits or runs; a link to look round its level: ?preview=) and their playtest rooms to join, then the games they published, whatever their visibility; a guest asked to sign ine2e (E2E-508, 583)
GamePage.tsxA game's page (?game=<id>): preview, blurb, missions, Play and Join, stars, a report, Remix and the Claude Code command that continues ite2e (E2E-172–175) Each mission with its mode, its players and a Play: a solo one's opens its run, a multiplayer one's a lobby (.game-lobby-join for its public lobbies); a curated game has no stars, remix or report.
LobbyPage.tsxA lobby's page (?lobby=<id>, #lobby[data-state][data-host][data-you]): the mission's card and rules in words (#lobby-rules), the teams' slots (.lobby-team[data-team], .lobby-member[data-name][data-ready][data-host]), Join, Ready, Switch, Leave, the host's Start and Kick, the countdown (#lobby-countdown), the way back to the run, the results (#lobby-results[data-outcome], .lobby-seat) and the vote (.lobby-offer[data-offer]), the link and the friends to invite; follows the feede2e (E2E-658–662)
TogetherPage.tsxThe Play together screen (?together): Quick play's playlists (.together-playlist[data-playlist]: into a lobby, or straight into a run under way), each playlist's missions across levels with a lobby to open from it (.together-playlist-missions), the player's lobby, the invitations, the public lobbies with a Join eache2e (E2E-659, 660)
lobby.tsThe lobby pages' calls without React, a mission's rules in words, the reader's place and the teams they may switch to, the run's address that keeps the lobby (inLobby), the feed followed for one lobbylobby.test
RespectParts.tsxRespect on the pages: a tier's badge (.tier-badge[data-tier]), the account's own beside its name (#browse-standing[data-respect]), what a run paid in its results (#run-standing[data-respect], .respect-step[data-step], .respect-endorsed, .respect-endorse[data-name], .respect-star), an account's conduct on the admins' page (#admin-conduct-value[data-conduct], .admin-conduct-line[data-why])e2e (E2E-663–667)
respect.tsRespect's calls without React, a tier's key and sign, a season's name, a run's Respect step by step in words, the reasons conduct movedrespect.test
ProfilePage.tsxThe player's own standing (?profile, #profile[data-respect][data-band]): Respect, tier, the next tier, the season's, the conduct band in a sentence and why it moved, the export as a filee2e (E2E-663)
LeaderboardPage.tsxThe leaderboard (?leaderboard, &season=): the season's players by Respect, the seasons before and all time as tabs (.leaderboard-tab[data-season], .leaderboard-row[data-name][data-respect][data-you])e2e (E2E-668)
Run.tsxA run's room in the overlay: the gathering, the countdown, the lives and retries, in a run of teams the scoreboard (#run-teams, each team's score in its colour, crossed out once nobody of it is left) and the roster in the teams' colours, whom a player out of the run watches (#run-watching, Space the next teammate) (RunHud); and the results: who won (a team by name, a free-for-all's player; data-winner, by a forfeit) or a draw, each seat's team and race place, the way back to the game's page (RunResults). run-view.ts reads a run's frame for window.__poc().run, names the winner and picks whom a player out of the run watches.
team-colors.tsA run's teams' colours (by their place in the run, the same on every screen), the players' colours by client id in a run of teams, the minimap's markers tinted with them.
AdminPage.tsxThe admins' page (?admin): an account searched by id or name, its limits a day beside the defaults and today's use, its override's form (builds, rooms, creations, budget-exempt, until, a note) with Save and Reset, every override, the changes in the audit log, the platform's creations (every one under way and the last ones over, whoever's, with Cancel) and the model's budget (#admin-queue, #admin-budget-today); anyone else told the page is for admins (#admin-denied); ids #admin-…e2e (E2E-585, 586, 604–606)
admin.tsThe admin page's calls without React (/admin/…), the override form's fields and the body they send, what a change changed field by field; the platform's creations and the budget, a creation cancelled; dollars as the page shows themadmin.test
steam.tsSigning in through Steam from the page: where the round trip starts (to sign in or to link), its one-time code taken at start (gone from the address first, then exchanged, used to link or kept for the e-mail and password), what came of itsteam.test
PlayRefused.tsxA city its platform will not place the player in (a platform for the game's owners): why, "Sign in through Steam", "Get it on Steam" (#play-refused)e2e (E2E-589)
CreatePage.tsxThe creation page (?create): the brief's form (600 characters; Riverbend, a new town or a region; small to large; 1 to 3 missions), the creations left today and when they start again, whether creation is open (off, the account, the budget and when it resumes); the owner's creations with their place in line or why they wait, their step, how long they took, Cancel; each result's preview, missions, text map or places, Playtest, Look round, Publish and Revise (a new creation on the same draft); &job=<id> puts one in view; ids #create-…e2e (E2E-600–611)
create.tsThe creation page's calls without React (/jobs, a draft's playtest and publish), how long a creation waited or ran, the hours until a time, whether a result can be playedcreate.test
CreationToast.tsxIn a game the platform opened, the toast of one of the player's creations that ended (the feed's creation, #creation-toast), a link to its resulte2e (E2E-608)
LobbyPrompt.tsxIn a game, the question a party's member is asked when their leader took the party into a lobby (#lobby-prompt): Join (#lobby-prompt-join) or Stay (#lobby-prompt-stay, leaving it)e2e (E2E-649)
GameParts.tsxA game's rating in words and its preview picture (a placeholder without one)e2e (E2E-170)
browse.tsWhich game page an address asks for (?browse, ?game=<id>, ?admin, ?create with its job) and the links between them, the api parameter kept
browse.cssThe game pages' layout: #ui filling and scrolling the window, the cards' grid, one column on a phonee2e
ServerForm.tsxThe Server tab: the field that reloads the page against another game servere2e (E2E-17, 63)
inventory-view.tsWhat the inventory strip and the container panel show (gui/item-blocks.ts draws them), from the server's inventory message and the room's items: each slot's name and count, the container's panel, the next slotinventory-view.test
phone-view.tsWhat the phone shows and does (gui/phone-blocks.ts draws it), as pure data: the page, a conversation over it, what the world opens taking the phone out and putting it away (followServer), every page's buttons in order (buttonsOf; the Jobs page's from jobs-view.ts: the invitations, the lobbies needing players, the missions of this level and the others, Quick play, what arrives put on top by onTop), the digit each answers to (keysOf: on the Jobs page from the bottom), back a pagephone-view.test
jobs-view.tsThe phone's Jobs app as pure data (jobsOf, jobButtons, onTop): the invitations, the public lobbies needing players across the playlists, this level's missions and the other levels' multiplayer ones, Quick play; what arrives put on top so nothing shown movesjobs-view.test
City.tsxThe room clock top centre (#city, #clock); a race under way is drawn under it by the in-game UI (gui/mission-blocks.ts)e2e (E2E-292)
mission-format.tsThe clock as minutes and seconds, the notices on screen (a few seconds each, a phone message longer, the newest four), for the mission panel (gui/mission-blocks.ts)mission-format.test
Stealth.tsxThe stealth badge under the Wanted stars (#stealth, data-visibility, data-alert): hidden or seen, and the compound's alert, where the room has guards or camerase2e (E2E-190)
Minimap.tsxThe minimap canvas: the static layer once per level (minimap-draw.ts), the markers on top (in a run of teams, the players in their team's colours, team-colors.ts; the guards' view cones always, coloured by phase, and the cameras' while they watch), the phone's waypoint with an arrow at the edgee2e (E2E-28)
minimap-draw.tsThe city's static map drawn into an offscreen canvas (the ground by height, the districts tinted and titled, the roads, the footprints, the strips, the game's zones): the minimap's layer and the phone's map picturee2e (E2E-28, 148)
minimap-data.tsWhat the game loop publishes for it: the static layer from the level (districts, roads, footprints, the ground's height, the garages for Go to) and the markers; the map's projection both ways, the waypoint's edge arrowminimap-data.test ; the game's restricted zones and race gates, and whether a race's arrow is drawn
mission-route.tsThe route line to the mission's target over the level's roads (@foi/world's roadRoute), worked out again only when the target moves or the player has gone a few metres, for the minimap
i18n.ts, locales/The i18next instance (React-free: the in-game UI reads it too; mount.tsx binds it to React) and the bundled languages (English, Italian)e2e (E2E-13, 322)
overlay.cssTokens, the grid that keeps the HUD, the legend and the minimap apart, the card, the breakpointse2e (E2E-64)

Layout ​

#ui is a fixed grid over the canvas: the HUD top-left (never wider than the room the in-game UI's mission panel leaves top right), the legend bottom-left in the space left under the HUD (it scrolls inside itself when the window is short), the minimap bottom-right at min(200px, 24vw, 24dvh) (the in-game UI's phone stands above it when out). Under 900 px the card spans its column; under 560 px of height the type shrinks; a coarse pointer gets 44 px controls; reduced motion drops the transitions. The tokens (colours, spacing, type sizes, control height, insets with the safe areas) are custom properties on #ui.

Input ​

The game counts mouse presses and wheel turns on the canvas only and key presses on the canvas or the page's body only (renderer/mouse.ts, renderer/keyboard.ts), so the legend, its button and the minimap take pointer events while the HUD lets them through, and a key typed in a field is the field's. A mouse click on a button or a box blurs it (releaseFocus in Legend.tsx), so the keys drive the game right after. The in-game UI's own input rules (its panels on the canvas, its fields) are in its README.

Conventions ​

  • Element ids are the end-to-end tests' contract (tests/e2e/support/poc.ts finds controls by them): keep them when moving a control; showLegendTab brings the right tab up. The in-game UI's blocks keep the same names in window.__poc().gui (tests/e2e/support/gui.ts).
  • What is part of the game goes in ../gui, never here: React is for static overlays (the helper UI, the stats and the game's events).
  • Every visible word comes from locales/*.json; add a key to both languages.
  • A new section is a component in its own file and a tab in Legend.tsx (TABS and SECTIONS); a new game switch is a field in OverlayState, written by the game loop, and a callback in OverlayProps.