added documentation

This commit is contained in:
2026-10-23 20:11:23 +02:00
parent 0913adaca5
commit 498df86962
10 changed files with 1285 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
# Frontend (`actionBlocks_website/`)
React 19 + TypeScript, bundled with Vite. Styling uses **CSS Modules only**: every component has a `*.module.css` next to it. The only global stylesheet is `src/theme/global.css`, which holds design tokens and the base reset. The game boards are drawn on `<canvas>`.
```
actionBlocks_website/
├── index.html # trailing-slash redirect, loads config.js and the app
├── vite.config.ts # base './', output dir 'static', dev proxy for /hub
├── public/ # copied to the build output unchanged
│ ├── config.js # runtime config (overwritten in the container)
│ └── assets/ # exchangeable graphics
│ ├── favicon.svg
│ └── items/*.svg # one icon per item id (+ unknown.svg)
├── docker/
│ ├── nginx.conf
│ └── 40-runtime-config.sh # writes config.js from env SERVER_URL
└── src/
├── main.tsx # entry: preload assets, render <App/>
├── App.tsx # top-level screen switch
├── config/
│ ├── runtimeConfig.ts # resolves the hub URL
│ └── keybindings.ts # all key bindings, DAS/ARR timings, help texts
├── net/
│ ├── types.ts # TypeScript mirror of the server DTOs
│ ├── hubClient.ts # typed SignalR wrapper
│ └── useGameConnection.ts # React hook: connection + all server state
├── input/useKeyboardInput.ts # keyboard → game actions (with DAS/ARR)
├── render/
│ ├── draw.ts # canvas drawing: blocks, items, ghost, board, previews
│ └── shapes.ts # piece shapes for previews
├── theme/
│ ├── global.css # CSS variables (colors, font), base styles
│ ├── colors.ts # block colors used on canvas
│ └── assets.ts # registry of every graphic + item visuals
├── pages/ # HomePage, LobbyPage, GamePage (+ .module.css)
└── components/ # BoardCanvas, MiniBoard, PiecePreview, ItemBar,
# ItemFlashOverlay, Logo, ui/Button, ui/Panel
```
## Data flow
```mermaid
flowchart TD
Hub[HubClient] -- LobbyUpdated / MatchState --> Hook[useGameConnection]
Hook -- status, playerId, lobby, match, flashes --> App
App -->|no lobby| Home[HomePage]
App -->|lobby waiting| Lobby[LobbyPage]
App -->|match present| Game[GamePage]
Game --> KB[useKeyboardInput]
KB -- sendInput / selectTarget --> Hook
Hook --> Hub
```
The app has **no router and no global store**. All server-derived state lives in a single hook, `useGameConnection`. `App` picks the screen from that state:
| State | Screen |
|---|---|
| no lobby or no player id | `HomePage`: enter a name, create a lobby or join one by code |
| `match` present | `GamePage` |
| lobby `phase === 'playing'` but no snapshot yet | "Starting..." placeholder |
| otherwise | `LobbyPage`: code, invite link, players, Start (host), last results, controls and item help |
### `useGameConnection` (`src/net/useGameConnection.ts`)
- Creates one `HubClient` on mount and starts it. If the connection fails it retries every 2 s.
- Keeps `status`, `playerId`, `lobby`, `match` and `flashes` in React state.
- On `LobbyUpdated`, it stores the lobby. When the phase is `waiting`, it also clears `match`, which returns the UI to the lobby screen.
- On `MatchState`, it stores the snapshot. Every `itemUsed` event becomes an **`ItemFlash`** `{ key, targetId, text, color }`, which is removed again after 1.4 s.
- When the connection is lost it clears lobby, match and player id. The server has removed the player by then (see [Architecture](architecture.md#state-lifetime)).
- It exposes stable action callbacks: `createLobby`, `joinLobby`, `leaveLobby`, `startGame`, `sendInput`, `selectTarget`.
### `HubClient` (`src/net/hubClient.ts`)
A thin wrapper around `@microsoft/signalr`:
- Request/response methods (`createLobby`, `joinLobby`, `leaveLobby`, `startGame`) use `invoke`. They turn server errors into a plain `Error` carrying the `HubException` message.
- Streaming input (`input`, `selectTarget`) uses `send`, which is fire-and-forget. These calls are dropped while disconnected.
In development, React StrictMode mounts the hook twice. The first connection is stopped while negotiating, and the console logs `Failed to start the connection: … stopped during negotiation`. This is harmless and does not happen in production builds.
## The game screen (`src/pages/GamePage.tsx`)
Layout: **hold + stats + "1 YOU"**, then the **own board** with the garbage meter and the inventory bar below it, then the **next queue**. To the right is a wrapping grid of **opponent mini boards**.
- **Own entry.** `me = match.players.find(p => p.id === playerId)`. If it is missing, the player joined mid-match and the page shows a "SPECTATING" box plus all boards as minis.
- **Sizes.** The own board's cell size comes from the window height: `clamp(16, (innerHeight - 170) / 20, 32)` px. Mini boards use 14/12/9 px for ≤ 2, ≤ 4 and more opponents.
- **Target selection.**
- The selected target lives in local state, `targetId`. Slot `1` is yourself and slot `n ≥ 2` is `opponents[n-2]`, in match order excluding yourself.
- A key press updates the local state and calls `selectTarget` on the server.
- If the target dies, an effect resets it to yourself, which mirrors the server rule. The selected board gets a cyan frame, and its slot number is filled cyan.
- **Overlays on the own board:** `ItemFlashOverlay` (flashes whose `targetId` is you), the countdown number, and "YOU WIN!" or "<name> WINS".
- **Blackout.** `hideStack={me.invisibleRemaining > 0}` makes `drawBoard` skip locked cells, item icons and the ghost.
- **Garbage meter.** A red bar whose height is `pendingGarbage × cell`.
- **Inventory bar.** 10 slots, with the oldest item outlined, plus the hint line `F: <next item> > <target>`.
- **Mini boards.** `MiniBoard` is memoized. It shows the slot number, the name, the board, a flash overlay and the opponent's **item bar beneath the board**.
## Keyboard input (`src/input/useKeyboardInput.ts`)
- Bindings come from `config/keybindings.ts` and are matched on `KeyboardEvent.code` (physical keys).
- Only active while `enabled` (the player is alive and the match is not finished). Key events coming from `<input>` and `<textarea>` are ignored.
- **Left/right** sends one move immediately. After `DAS_MS` (167 ms) it repeats every `ARR_MS` (33 ms). Held directions form a stack, so releasing one resumes the other.
- **Soft drop** counts held soft-drop keys. It sends `SoftDropStart` on the first press and `SoftDropStop` when the last one is released.
- **Number keys** call `onTarget(slot)`.
- Browser key-repeat events are ignored. `preventDefault` stops arrows and space from scrolling the page.
- On window **blur** every held key is released, which also stops a running soft drop.
## Rendering (`src/render/draw.ts`)
All drawing is plain Canvas 2D in a pixel style:
- `setupCanvas` sizes the canvas for `devicePixelRatio` and disables image smoothing.
- `drawBlock` draws one block: a base color with a lighter top/left edge and a darker bottom/right edge. **Change this function to change the block look everywhere.**
- `drawItemIcon` draws the item's SVG (from `assets.ts`) over a cell. While the image is still loading it falls back to a colored square.
- `drawBoard` draws, in order: background, grid lines (only when cells are ≥ 14 px), locked cells and item icons (skipped when `hideStack`), the ghost (semi-transparent outline), and the active piece with its item icon.
- `drawPreviewPiece` draws hold and next pieces centered in a box, using the shapes from `render/shapes.ts`.
`BoardCanvas` redraws in an effect whenever its `state` prop changes, i.e. on every snapshot. It also renders the "OUT"/place overlay for eliminated players and any children as overlays.
## Theming and graphics
Everything visual can be swapped without touching component logic:
| What | Where |
|---|---|
| UI colors, font, border style | CSS variables in `src/theme/global.css` (`--color-*`, `--font-pixel`, `--border`) |
| Block colors (1–7 pieces, 8 garbage), grid, ghost alpha | `src/theme/colors.ts` |
| Item icons, names, flash colors, help texts | `itemVisuals` in `src/theme/assets.ts` |
| Image files | `public/assets/…`, referenced only from `assets.ts` |
| Logo | `uiAssets.logo` in `assets.ts`. `null` renders the colored text logo |
| Block drawing style | `drawBlock` in `src/render/draw.ts` |
| Font | Google Fonts link in `index.html` plus `--font-pixel` |
The pixel font (*Press Start 2P*) has **no arrow, ellipsis or dash glyphs**, so UI text uses ASCII only ("LEFT RIGHT", "...", "-", ">").
Asset paths go through the `asset()` helper in `assets.ts` (`import.meta.env.BASE_URL + path`), so they stay correct when the site is served under a sub-path.
## Runtime configuration
`src/config/runtimeConfig.ts` resolves the hub URL in this order:
1. `window.__ACTIONBLOCKS_CONFIG__.serverUrl` from `config.js`. In Docker this file is generated from `SERVER_URL` at container start.
2. `VITE_SERVER_URL`, read at build time.
3. `<page origin>/hub`.
## Sub-path support
The site can live at `https://host/` or at `https://host/some/path/`:
- `vite.config.ts` sets `base: './'`, so all built URLs are relative (`./static/…`, `./config.js`, `./assets/…`).
- `index.html` starts with a tiny script that redirects `/path` to `/path/`. Without the trailing slash, relative URLs would resolve against the parent directory.
- Hashed bundles go to `static/` (`build.assetsDir`), which keeps them separate from the exchangeable graphics in `assets/`.