Lab Journal
VirusMon: From Empty Repo to Multi-Zone in a Day

VirusMon: From Empty Repo to Multi-Zone in a Day

lab-journalvirusmon

One of those rare days where the plan ledger lined up so neatly that I just kept knocking down milestones. VirusMon went from an empty monorepo this morning to something I'd actually call a vertical slice by evening.

VirusMon

I'd staged four plans worth of design docs, so today was mostly execution. The arc roughly went: walking skeleton → battle engine → persistence & accounts → catching & party → multi-zone. Each one unlocked the next, and by the end the client boots, authenticates, resumes at its persisted location, walks between zones, fights wild encounters, catches things, and arranges a party.

Plan 1 — walking skeleton. Monorepo scaffold with shared/server/client workspaces, a Colyseus zone room with server-validated grid movement and rate limiting, and a Phaser client that renders the town map and syncs players. The one fix I'm glad I caught early: rejecting a client-injectable stepMs override outside tests. Easy to leave in, very ugly if it ships.

Plan 2 — battle engine. This was the meatiest chunk. Seeded mulberry32 RNG, type chart, species and moves content, instance creation, damage and turn resolution, XP and level-ups with stat growth, and a battle session orchestrating the engine against one client. Then the BattleScene UI on top, with zone room routing into wild encounters. The two BattleScene bugs I caught during plan review were both realmenuTexts not resetting on scene restart, and onMessage handlers not unbinding on shutdown. Either would have been a nasty intermittent later.

Plan 3a — persistence & accounts. SQLite storage core, bearer-token accounts with hashed tokens, roster and inventory storage with atomic adjustments. The interesting bugs here were all security-flavoured: hardening the anon-fallback check against non-string username tricks, fail-closed key cap against source-rotation floods, and — the one I'm most glad I noticed — the auth throttle was originally keyed globally, which is a trivial DoS. Now it throttles only failed auths and keys on the real socket IP (with a trust-proxy ledger note so I don't forget the deployment implication). Also stripped control/bidi/zero-width chars from player names, which I always forget until someone uses it to grief.

Plan 3b — catching & party. Catch protocol with a lime whitelist, catch rates, victory drops, party battles with faint cascade, transactional persist. Battle UI got the fight/catch/flee menu and party strip, and there's now an overworld roster panel on R. Small UX fix: UP/DOWN navigates the 2-item catch submenu, which was annoying me during playtest.

Plan 4 — multi-zone (started). Shared zone registry with town, route-1, and interior-1. Schema v2 adds persisted account location (zone, x, y), with an atomic v1→v2 migration. Per-zone rooms with zone-keyed encounter tables — town is now safe, which it absolutely was not before. Clients spawn from their persisted location with fallbacks, save on leave, and wrong-zone joins get redirected. Added GET /api/whereami so the client can ask the server which zone it belongs in before joining, and exit tiles now warp between zones (defeat warps to town).

The one subtle client bug worth recording: colyseus.js uses nanoevents, which appends handlers rather than replacing them. Zone redirect messages can arrive before ZoneScene.create() registers its own handler, and unregistered messages get dropped. So I buffer redirects at boot and pass the unsubscribe down to ZoneScene to remove the buffer handler before registering the real one. Took a minute to figure out why my "obvious" fix was double-firing.

Closing thought

The thing that made today work was having Plans 1–4 designed end-to-end before writing a line of code. Every "fix:" commit in the log is something the plan review or implementation surfaced before it became a debugging session. The cruft I flagged at the end of the day — unused adjustItem, dead setData/rows reset in RosterScene, balance math gaps — is exactly the kind of thing I want to bundle into a sweep rather than nibble at piecemeal. Tomorrow: probably character creator and proper zone content, now that ZoneRoom no longer hardwires townMap.