Doug's Game Dev Log
About
2026-08-28

Every Screen Says What Is Happening

This one is entirely about the screens around the run: the end screen, the pause screen, the options screen, and the co-op lobby. The connecting thread is that all of them used to leave the player guessing about something, whether that was "how did that run actually go", "which stage am I even paused in", or, worst of all in co-op, "did my click register or not".

One end screen instead of two

Victory and game over were separate code paths that happened to look similar. They are now a single drawEnd.ts (the file is literally the old drawVictory.ts, renamed), with the win/lose difference reduced to the title and the colors.

The screen itself got much richer, in two columns:

  • Play time for the run, formatted by a small fmtDuration helper.
  • A tower totals table: each tower type you built, with its final level, total damage dealt and total kills. aggregateTowerTotals walks the surviving tower entities and sums per type; fmtCompact keeps the numbers readable when damage runs into the millions.
  • A bonus table listing every upgrade card you actually took, with its level, in the stable order of upgradeLevels.
  • A medal icon colored by the tier you were playing (bronze, silver, gold, endless), from the same palette everywhere else uses.

Layout took three passes. First version had tiny sprites and a fixed three-column table geometry that fell apart with long numbers; the review pass enlarged sprites and fonts, centered everything, and moved the menu button below the table instead of floating above it. All non-title text uses the mono card font, which lines the columns up without hand-tuned x positions. And the level label shortened from LVL to LV, because at that font size the third character was costing more than it explained.

Pause knows where you are

The pause screen said "PAUSED" and nothing else. In a game with eleven stages and four difficulty tiers, tabbing back in after five minutes meant genuinely not knowing what you were playing.

There is now a subtitle line between the title and the buttons: the stage name followed by the run type, like "Grasslands - BRONZE", colored by medal tier using the end screen's palette. Which promptly created a 2px overlap with the Resume button, because the subtitle went in at y=268 and the button's top edge sat at y=270. The whole pause block is now re-centered on the 720px canvas: title, subtitle, then buttons evenly spaced below. Adding a line to a hand-positioned layout means repositioning the layout, every time.

Options in two columns

The options list had grown past the point where a single column made sense. It is now split by category: audio on the left (player name, SFX volume, music volume), graphics on the right (fullscreen, show FPS, screen shake, quality).

Screen shake is the new one, a persisted checkbox that defaults on. Turning it off suppresses the camera offset from explosions, cannon fire and life loss, in both the single-player and co-op paths. Camera shake is great game feel and it makes some people motion sick; that is not a tradeoff, it is a setting.

The quality cycle order was also backwards, running high to medium to low, so the first click on a fresh save downgraded you. It now cycles low to medium to high, default still high.

The guest stops guessing

In co-op, the host owns the simulation and the guest's clicks are intents that come back confirmed in the next authoritative snapshot. That round trip is fast but it is not instant, and the UI was not acknowledging it at all. Four fixes:

  • "Waiting host confirmation..." now shows on the tower-pick overlay after a guest picks a card. The overlay has to stay open until the host confirms, and before this it just showed a blank card area, which reads exactly like a bug.
  • Wave buttons go gray while pending. Clicking the AUTO checkbox or NEXT WAVE immediately renders both controls gray and non-interactive until the confirming snapshot arrives. This follows the pattern already used for aim buttons: a pure lib/waveAreaPending.ts for the state transitions, a mutable net/waveAreaPending.ts store, cleared on run start alongside the pending aim state.
  • Ghost placements are capped. A guest places pending ghost towers before the host confirms them, and the only gate was "inventory > 0", which does not account for ghosts already queued. So you could place eight ghosts holding one tower. countPendingByType (pure, tested) now gates both ghost creation and intent emission behind inventory[armed] - pending(armed) > 0.
  • Placement mode exits when you run out. Once a guest's remaining count hits zero, the armed cursor clears itself and returns to the standard cursor, matching what single player and the host already did.

There was also a co-op bug hiding in plain sight for weeks: no keyboard input worked at all during a co-op session. keyboardInputSystem and the ESC handler both lived after the coopUpdate early return in gameUpdate, so control flow never reached them. Both moved inside coopUpdate's playing block, and 1-4 tower select, N for next wave, T for the tower queue, F for fullscreen and ESC to cancel all work in co-op now. Speed cycling with S stays a deliberate no-op, since the co-op shadow world keeps speedTiers at [1].

Finally, the lobby: the host's room code now has a Copy code button under START, shorter than the main button so it reads as secondary. Its label flips to "COPIED!" as feedback and resets when the lobby is re-entered. Reading a six-character code out loud over voice chat is fine right up until two of the characters are ambiguous.

Next: the game leaves the browser.