apps/client/src/gui
The in-game UI: what is part of the game, drawn in the scene with Babylon's GUI (@babylonjs/gui) on one full-screen texture over the picture. The game loop owns it (renderer/game.ts creates it with createGameUi and hands it what to show as it changes); React never sees it and it never reads the simulation. React (../ui) keeps the static overlays: 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).
The rule. UI that follows something in the world, or is part of playing (the prompt and the labels over things, the phone, the mission's words, the slots and the panels built on them) is drawn in the scene with Babylon's GUI and updated in the render loop; React only draws static overlays. A label over something in the world is placed on every frame the scene draws, never through a React render: the prompt used to follow the character ten times a second (on 39 of 239 frames, up to 43 px behind it); it is now placed on every frame, exactly where the character is drawn (E2E-320).
| File | What | Tested by |
|---|---|---|
game-ui.ts | The UI on a scene: the full-screen texture (at the page's own pixel size whatever the engine's scaling), the panels and where each goes (the mission panel top right, a race under the room clock, the strip bottom centre, a container's or a garage's panel above it, the phone above the minimap), the labels, the theme on every resize, the language on every switch; covers (a panel that takes the mouse is under a point: never a shot, never an aim) and claims (a key typed into a field is the field's: Enter presses the screen's first button, Esc gives the keys back); debug for window.__poc().gui | e2e (E2E-320 to 324) |
labels.ts | The labels that follow something in the world: what one says changes as the game loop says, where it stands is worked out by projecting its point in an observer of onBeforeCameraRenderObservable ahead of the GUI's own, every frame; what the tests read (where the point landed, on which frame) | e2e (E2E-320) |
panel.ts | A panel on the texture: the controls of a laid-out block tree at the places the layout gave them (rectangles, a text block a line, key caps, buttons lit under the pointer, a numeric input, a map picture and its dots), rebuilt only when what it shows changed; a field's value and a map's dots change in place; a panel taller than it may be scrolls with the wheel in a clipped window; its named blocks on the page, the rectangles it answers the pointer from (pointer) | panel.test, e2e |
layout.ts | The block model (text, buttons in a flow, a column, a grid or equal tiles, boxes, columns, a field, a map) and its layout to absolute rectangles: text wrapped with the canvas' own measure, every height known before anything is drawn; a panel's signature | layout.test |
theme.ts | The colours (the overlay's tokens), the type and the spacing at the window's size: smaller on a short window, bigger controls under a finger | theme.test |
label-blocks.ts | The prompt (prompt: the key cap and what E would do, coloured by what it is) and the car tag (car-tag) | blocks.test, e2e |
item-blocks.ts | The inventory strip (inventory, inventory-slot-<n>), a container's panel (container, container-items, container-yours), a shop's (shop-cash) and a garage's (garage, garage-respray, garage-store) | blocks.test, e2e (E2E-130 to 137, 164, 306) |
mission-blocks.ts | The mission panel (mission, mission-objective, mission-clock, mission-hold, a group's mission-members and mission-member-<id>, mission-escort, mission-chase, mission-photo, wallet, notices) and a race's readout (race) | blocks.test, e2e (E2E-70, 71, 292) |
phone-blocks.ts | The phone (phone and its phone-… blocks, the overlay's element ids before) from ui/phone-view.ts: home, messages, contacts, calls, the map, the camera, the Jobs app in the city (what arrives on top), an app's screen or a keypad, a conversation over any page; its buttons numbered for 1–9 | blocks.test, e2e (E2E-140 to 149) |
How a panel is made
A builder (*-blocks.ts) turns the game's state into a tree of blocks, pure and unit-tested with the real translations; layout.ts lays it out within the widest the panel may be; GuiPanel draws it where its rules put it. The view logic of the phone, the inventory and the mission's notices stays in ../ui (phone-view.ts, inventory-view.ts, mission-format.ts), shared with the game loop. A block with an id is listed in window.__poc().gui.controls with its text, its state (selected, a panel's kind, enabled) and where it is on the page: the end-to-end tests read it there and click it with the real mouse (tests/e2e/support/gui.ts: pollGuiText, pollGuiShown, pollGuiTags, clickGui).
Input
The GUI takes its pointer and keyboard events from the scene (Babylon's input manager on the canvas). A panel answers the pointer itself (GuiPanel.pointer; PanelPointing hands every pointer event of the scene to the topmost panel under it alone, as Babylon's picking did, and looks again at what lies under a still pointer when a panel is rebuilt or hidden, so the lit button and the cursor follow), from the layout it shows now, the same rectangles window.__poc().gui lists: Babylon's picking of the controls is not used for a click, since a rebuild replaces every control, Babylon picks a control only once a frame has laid it out, and it forgets a press whose control went (a click meeting a rebuild, as when a message arrives, was lost). A press is a down and an up of one pointer's left button on the same button or map, wherever the layout had it at each (another button or finger meanwhile leaves it be); a press on a field gives it the keys, a press anywhere else takes them back. The phone stands on its bottom edge, so its lists put what they gain on top (the newest message, contact and call first) and nothing under the pointer moves when something arrives. The game's own handlers (renderer/mouse.ts, renderer/keyboard.ts) ask it first: a press or a wheel turn over a panel that takes the mouse (the phone, the strip, a container's, a garage's) is never a shot nor a change of target, and the pointer over one does not aim; a key typed into a field (a keypad's code, after a click on it) is the field's. The buttons never take the keyboard, so W/A/S/D, E, T, Esc and 1–9 keep working while a panel is open. The mission panel, a race's readout and the labels let the mouse through.
Adding to it
A new panel is a builder of blocks in its own file, a GuiPanel with its rules in game-ui.ts and a method the game loop calls; a label over something in the world is a WorldLabel. Every visible word comes from ../ui/locales/*.json (the same i18next instance as the overlay's); give every block the tests should find an id.