3.6 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Running the game
Open index.html directly in a browser — there is no build step, no bundler, and no package manager. All JS files are loaded as plain <script> tags in dependency order.
For local development use any static file server, e.g.:
python3 -m http.server 8080
# then open http://localhost:8080
Architecture
The game is split into seven files. Load order in index.html reflects the dependency chain:
settings.js → player.js → conveyor.js → lab.js → highscore.js → game.js → intro.js
File responsibilities
| File | Exports | Role |
|---|---|---|
settings.js |
SETTINGS (frozen object) |
All numeric constants and colours. No side-effects. Tweak values here only. |
player.js |
Player class |
Grid position (col 0-2, row 0-2), inventory, movement cooldown, walk animation. No rendering. |
conveyor.js |
LeftConveyorSystem, RightConveyorSystem, Good, Medication classes + state enums |
Belt physics and item lifecycle. No rendering. |
lab.js |
Lab class, LabState enum |
Accepts deposited goods, detects specials, runs analysis timer, fires onMedicationReady callback. No rendering. |
game.js |
Game.init(name1, name2) |
Game loop, all canvas rendering, input handling, 2P split-screen logic. Calls HighScoreScreen.show() on time-out. |
intro.js |
IntroScreen.show() |
Two-step intro: player count selection → name entry → Game.init(). |
highscore.js |
HighScoreScreen.show(names, scores, playerCount) |
Persists top-10 to localStorage (kidneylab_scores), renders results. Both 1P (scalar) and 2P (array) calls are supported. |
2-player split-screen (game.js)
All mutable state is held in per-player arrays (gPlayers[i], gScores[i], etc.). The RS proxy object exposes _pIdx-indexed getters so renderOneSide() can be called twice without duplication.
P2's half is drawn with a mirror transform (ctx.translate(W, 0); ctx.scale(-1, 1)). Text inside that transform must go through the ft(text, x, y) helper, which applies a local scale(-1, 1) to cancel the outer flip.
P2 keyboard input (WASD) has left/right swapped to match the mirrored visual: pressing visual-left (A) calls move('right'). The virtual joystick applies the same swap via the swapLR flag in makeJoystick().
Interaction model
Interactions are purely position-based — no action button. handleInteractionsFor(pIdx) runs every frame:
col === 0→ attempt pickup from left belt at current rowcol === PLAYER_COLS - 1, row === 2→ deposit all carried goods to labcol === PLAYER_COLS - 1, row === 1→ pick up medication from right beltcol === PLAYER_COLS - 1, row === 0→ deliver medication to patient
Layout constants (all in settings.js)
x: 0 350 600 900
|←LEFT→ |←MIDDLE → |←RIGHT →|
belts player grid lab/belt/patient
Rows are shared vertically: BELT_ROWS: [155, 300, 445] (top→bottom, y-centre).
Player pixel position is computed in Player.pixelX() — evenly distributed across the middle zone with a 30 px margin from each edge.
Mobile joystick
makeJoystick() in game.js builds a virtual joystick per player and appends it to the game screen div. The joystick uses position: fixed anchored via .joy-left / .joy-right CSS classes. It is hidden on non-touch devices via @media (pointer: coarse). Repeated movement while held is driven by setInterval at 160 ms; the player's own moveCooldown (180 ms) naturally gates actual step rate.