Fix-It-First
A timed relay race through 3 cross-trade stations. Players choose between two phase groupings at the level-select — Diagnosis (probe + repair existing systems) or Build (wire/install from scratch) — then pick a difficulty (Easy/Medium/Hard/Expert). Each run plays Frame Up → Circuit → Pipe & Duct under a shared countdown clock. Heavily modeled on Site Sprint Relay; reuses the Circuit and Connection mini-game playfields and introduces a new Frame Build mini-game.
Game Flow
Menu → /levels (2 phase sections × 4 difficulties) → Level Intro → Countdown → Station Intro → Playing → Station Complete → [next station or Results]
Same state machine as Site Sprint Relay. See store.ts.
Modes
| Mode | Slug | Frame pool filter | Circuit pool filter | Connection pool filter | Connection levelKind |
|---|---|---|---|---|---|
| Diagnosis | diagnosis | tags.includes("diagnose") | tags.includes("diagnose") | tags.includes("diagnostic") | "diagnostic" |
| Build & Install | regular | tags.includes("build") | tags.includes("build") || tags.includes("match-fix") (excluding practice) | !tags.includes("diagnostic") | "standard" |
Routes: /fix-it-first/play/[mode]/[difficulty] (e.g. /fix-it-first/play/diagnosis/easy).
Stations
| # | Station | Phase ID | Shared Mini-Game | Notes |
|---|---|---|---|---|
| 1 | Frame Up | frame-station | FrameBuildPlayfield | pickN from frame pool filtered by mode. Build = pick four right-length planks for the dock; Diagnose = inspect the pre-built frame and swap mismatched planks. |
| 2 | Circuit | circuit-station | CircuitPlayfield | pickN from circuit pool filtered by mode. |
| 3 | Pipe & Duct | flow-station | ConnectionPlayfield | pickN from connection pool filtered by mode; passes levelKind. |
Per-station + total time budgets live in config.ts.
Pool additions
To make Easy & Medium playable in Diagnosis mode, the following items were added to the shared pools (and benefit Circuit Breaker / Flow State as well):
- Circuit (
lib/mini-games/circuit/content/pool.ts):cb-easy-diagnose-outlet,cb-easy-diagnose-light-switch,cb-medium-diagnose-gfci,cb-medium-diagnose-fan-light. - Connection (
lib/mini-games/connection/content/pool.ts):fix-easy-plumbing-trap-fault,fix-easy-hvac-undersized-elbow,fix-medium-plumbing-uphill-drain,fix-medium-hvac-reducer-backwards.
The new shared Frame Build mini-game lives at lib/mini-games/frame-build/ with its own content pool covering both build and diagnose items at every difficulty.
Frame Build mini-game
A drag-and-place mini-game representing the in-person framing activity (slot wooden planks into a dock to square the work area). One config defines:
- 4
slots(top,bottom,left,right) each with arequiredLengthInches. - An
inventoryofPlankDefobjects (mix of correct planks + decoys close in length). - Optional
prePlacedarray used bydiagnosemode to seed the slots; entries withisFault: truecarry afaultReasonshown when the player taps to inspect.
Build mode auto-completes when the player has placed a correct plank in every slot. Diagnose mode is the same playfield but starts with the prePlaced planks (some faulty) — the player taps a placed plank to inspect, removes the bad one, and drags a replacement from the tray. Wrong-length placement attempts increment wrongAttempts, which feeds into accuracy.
Visuals are pure CSS (warm wood gradients + repeating-linear-gradient grain overlay) so the game ships without art. Real wood textures can be generated later via bun generate:images:fix-it-first (see games/fix-it-first/image-gen.config.ts) — the playfield resolves them via the assetBasePath prop.
Blueprints
All three stations share a unified blueprint pattern. Each pool item carries an optional blueprintImageId referring to a generated WebP under /mini-games/{slug}/{blueprintImageId}.webp (blueprints belong to the mini-game, not the host game, so every consumer reads the same asset). The toolbar exposes a Blueprint button that opens a modal showing the image alongside a context summary (cut list for frame, parts list / schematic / grid outline for flow, multimeter log for circuit). The frame station also shows an inline blueprint preview at the top of the playfield.
| Station | Pool field | Image lives at | Prompts authored in |
|---|---|---|---|
| Frame Up | FrameBuildConfig.blueprintImageId | /mini-games/frame-build/bp-frame-*.webp | lib/mini-games/frame-build/content/blueprint-prompts.ts (FRAME_BLUEPRINT_PROMPTS) |
| Circuit | CircuitChallengeConfig.blueprintImageId | /mini-games/circuit/bp-*.webp | lib/mini-games/circuit/content/blueprint-prompts.ts (BLUEPRINT_PROMPTS + describeCircuitBlueprint()) |
| Pipe & Duct | ConnectionConfig.blueprintImageId | /mini-games/connection/bp-*.webp | derived per-pool-item via describeConnectionBlueprint() in lib/mini-games/connection/content/blueprint-prompts.ts |
The drafting style is unified via BLUEPRINT_STYLE in lib/images/prompts.ts; each domain extends it with trade-specific symbol guidance (NEC electrical, ANSI woodworking dimensions, IPC plumbing / ASHRAE HVAC). All blueprint prompts use the PRO_MODEL (Gemini pro image model) for crisp text + line work. Generate via bun generate:images:circuit, bun generate:images:connection, bun generate:images:frame-build.
Generation is on-demand: bun generate:images:fix-it-first, bun generate:images:flow-state, bun generate:images:circuit-breaker. Until assets exist, the modals hide the image and show the inline detail / placeholder.
Scoring
Identical formulas to Site Sprint Relay (per-station score + time bonus + difficulty multiplier). See scoring.ts and grade thresholds tuned for 3-station max in config.ts.
Persistence
Best runs are stored under one slot per (mode, difficulty) pair (8 total slots) keyed ${mode}_${difficulty} in runs. getCumulativeBest() sums all slots for an aggregate best. See hooks/use-game-actions.ts.