Skip to main content

HUD Layout and Panels

This page explains the PlaySoloComponent UI: which panels exist, what each shows, and when they appear.

For controls to interact with the HUD, see Controls.

Layout Structure

The HUD overlays the PixiJS canvas. The canvas fills the viewport via #pixiHost. The HUD shell (play-solo__shell) sits on top using absolute positioning — nothing is rendered below the canvas.

┌─────────────────────────────────────────────────────────┐
│ [Back] [Observer Camera] [Phase Panel] [Scoreboard] │ ← Top bar (overlaid)
├─────────────────────────────────────────────────────────┤
│ │
│ PixiJS Canvas │
│ │
│ [Floating Context Panel] (overlaid) │
│ [Toolbar / Deck UI] (overlaid) │
└─────────────────────────────────────────────────────────┘

Top Bar Panels

Observer Camera Panel

Shows the currently viewed lane. Clicking a scoreboard row changes the camera focus and updates this label.

  • Signal: cameraLaneId: Signal<LaneId>
  • Hint text: always visible — guides new players to use the scoreboard

Phase Panel

Shows the current match phase and round. Updates every frame from snapshot().phase.

ElementContent
Phase chipPreparation, Combat, or Game Over
Round chipRound N
Detail textTimer countdown or wave status
Skip buttonVisible during preparation when the player is alive and not in replay

The skip button calls togglePreparationReady(), which dispatches set-preparation-ready. The button label switches between "Ready" and "Cancel Ready".

Scoreboard

Shows all four players (player + 3 NPCs) as clickable rows. Each row displays:

ElementNotes
Lane color swatchLane accentColor from config
Label"You" for 'player', "NPC N" for NPCs
Status"Eliminated", +Xg / round, or "Combat ready"
Gold pillCurrent gold
Core health pillCurrent core HP with a heart icon
Ready pillShown during preparation when the player is ready

Clicking a row calls focusLane(laneId) — this sets cameraLaneId and calls SoloPixiCameraController.focusLane(laneId).

The scoreboard header has two controls:

  • Stats button (G) — toggles the stats panel
  • Help icon (?) — shows the help overlay on hover

Stats Panel

Opens when showStats() is true. Toggle with G or the Stats button.

Shows per-player live stats in a table:

ColumnNotes
Core healthRemaining HP
GoldCurrent gold
TowersNumber of placed tower structures
Economy towersTowers with incomePerRound > 0
KillsTotal enemies eliminated
Active enemiesEnemies currently in the player's lane
Pending sendsQueued send packages not yet spawned
QueueQueued builder actions
Total leak damageCumulative leak damage taken

Below the table, animated line charts show historical trends per player for:

  • Economy (gold over time)
  • Core health (stability over time)

Charts are populated from replayFrames in replay mode, or from liveSnapshot during a live match.

Floating Context Panel

Appears when the player clicks a plot in their own lane. Position follows the plot's world position projected to screen space.

The panel shows different content depending on the plot and structure:

StateContent
Empty tower plotList of buildable tower families from the deck
Tower placed (no variant)Branch variant buttons + stat upgrade buttons
Tower with variantStat upgrade buttons, relocate button, sell button
Send plotSend structure info, upgrade buttons, send-target selector

The panel closes when clicking empty space, pressing Escape, or switching lanes.

Toolbar (Deck + Send Queue)

Below the canvas, the toolbar shows:

  • Available tower families from the player's deck (with keyboard shortcuts 14)
  • The send structure panel with target selection

Clicking a tower family button enters placement mode. The canvas shows a placement ghost.

Help Overlay

Visible while hovering the ? help icon in the scoreboard header. It shows camera, placement, and UI controls as a quick reference.

Deck Selection Screen

Shown before startMatch() is called. The player picks a deck from SOLO_DECKS. Clicking a deck calls startMatch(deckId), which:

  1. Calls createSoloMatch({ playerDeckId, seed, debug, startingRound })
  2. Resets all UI signals
  3. Calls focusLane('player')
  4. Starts the requestAnimationFrame loop

Replay System

After a match ends (phase.kind === 'game-over'), the replay system activates.

Frames are captured every 250 ms during the live match into replayFrames. The replay panel lets the player:

  • Scrub through frames with a slider
  • Play back at 0.5×, , , or speed
  • Pause and resume

In replay mode, snapshot() returns replayFrames[replayState.frameIndex] instead of liveSnapshot().

info

During replay, player commands are disabled. canIssuePlayerCommands() returns false while isReplayActive() is true.