Changelog
What changed in Battle Painters, release by release.
v0.3.5 — 2026-08-31
- Let the countdown wait cover building the scene it waits for
v0.3.4 — 2026-08-31
- Stop two waits from racing what they are waiting for
- Judge the rubber-band guard by the share of ticks that fought
- Let a click wait as long as a starved page needs
- Put a viewer back on the STATE lane before looking for a grid
- Give three waits the timeout they were written with
- Make the bomb-replay spec wait for the bombs
- Point every spec header at a file that exists
- Pin the wire-type literals the specs match frames by
- Pin the room-channel literal a spec injects on
- Give every spec the budget an in-round spec actually needs
- Stop three specs waiting on things a starved page delays indefinitely
- Run the high-contrast pause-button check
- Take the guest's peer id while it is still seated
- Read the replay HUD, the peer map and the results screen on their own terms
- Give three specs a scene the size of the claim they make
- Assert the claim table's wiring, not an invariant it does not hold
- Deliver the succession table to a client that has stopped draining
- Compare the team-spread knob against itself, not across 150 ticks
- Let the mesh-count wait be the assertion it already is
- Measure the input ladder against the host, not against a number
- Measure the AI claim table only while the round is running
- Stop the e2e rounds from rendering the maintainer's weather
- Hold the rejoin paint check to a floor the other brushes can't reach
- Regroup only when nothing is left to be connected to
- Judge the rejoin paint sync by a stroke, not a texel
- Keep the succession table reaching guests that go live late
- Let a returning guest get itself back into the lobby
- Retry a dropped chunk fetch before calling the round lost
- Keep bot chatter out of the logs-panel export comparison
- Stop a connectivity blip from swallowing the host's death
- Await the store flush in persistence specs instead of sleeping
- Wait for coverage samples, not a frame count, before asserting the HC chart
- Cover the menu's session plumbing
- Assert the guest's input frames flow rather than timing a one-second window
- Guard the specs' __bpGame reach-ins against the seam they read
- Keep the coarse-bucket mapping on the form the AI tuning was calibrated against
- Ratchet the lobby session branch floor
- Cover the host lobby session wiring
- Cover the client lobby protocol directly
- Let a follower outwait its successor's promotion
- Ratchet the coverage floors to what the suites reach
- Give both share cards one shell
- Draw both coverage graphs from one geometry
- Share the audio coordinators' per-frame plumbing
- Ratchet the CSS-selector locator count
- Drop MenuModel's refresh diff
- Give the results screens options objects
- Give the AI's cross-round state one owner
- Split the DOM helpers by what they depend on
- Single-source the slot count and tidy two AI reach-ins
- Mirror the lazy dialog entry points
- Share the system-share attempt between the menu and the room code
- Derive InputRouter's dispatch and teardown from one registry
- Re-send the succession table once every client has rendered
- Fetch the results share-image renderer on demand
- Give the match settings one list of legal values
- Complete the protocol rows and scan for undeclared wire fields
- Hand PauseController the round context
- Converge the host-gone dialog and the lobby hand-off on one helper each
- Bind the collision emitter once and drive the jump spec through the tick
- Seat player-player collision beside the other pure sim siblings
- Give the coarse grid one world→bucket mapping
- Repoint the benches at the sim's event vocabulary
- Make every uiStore removal path share one latch
- Tear the menu backdrop down from its host's ref cleanup
- Ground items with the scene-shared blob shadow
- Derive the ready check's wait set from the live transport
- Key both PeerManager inbound seams to connection identity
- Route every post-gap unfreeze through GameScene.resumeFromFreeze
- Derive the item-label parity guard from ITEM_TYPES
v0.3.3 — 2026-08-27
- Hand the late-joiner wait overlay to RoundNetBridge
- Seat the sim's event vocabulary and reconciliation math inside simulation/
- Assert AI decision narration on the structured event keys
- Extract RoundInput and RoundShell from RoundController
- Extract the round's audio-visuals into RoundPresentation
- One game-layer bus per round
- Collapse the round reach-ins into one frozen RoundContext
- Make RoundState the single writer for round flags and PauseController the sole pause-overlay owner
- Dispatch pause/resume by role through one coordinator intent
- Pluralize through the locale's own rules; move SEO meta out of the i18n core
- Run the Settings keymap capture through a pure state machine
- Split the keyboard stack into keymaps, layout detection, and labels
- Derive the results view from one model
- Give the host-exit dialog its own stylesheet
- Share the lobby portrait pop and keymap option mapping; pin config builders
- Let the async store flush land before the replay-cap reload check
- Declare the replay config flag; park natural spawns through a real API
- Carve out src/consent, seat the layers, and make the util boundary executable
- Grow the uiStore's conventions: factory props, surface ids, modal truth
- Make releasePeerManager the one transport handoff; arm eviction per phase only
- Build every outbound frame outside the sessions through frames.*
- Decompose the boot into src/boot modules with data-flow ordering
- Cover the results canvas kit and the i18n stragglers
- Make the round's ordering contracts structural
- Release MenuModel's pref subscription on singleton reset; document the observable idioms
- Put the audio volume/mute prefs on subscribable singleton cores
- Unify the status observables on the createPrefSingleton core
- Route Game's between-round guard through createDispatcher; document the registry
- Route the round barriers' inbound frames through createDispatcher
- Route the client join screen's inbound frames through createDispatcher
- Route the host lobby's inbound frames through createDispatcher
- Add the table-driven message dispatcher; fill defaults at session ingress
- Drive the wire protocol from one declarative PROTOCOL_SPEC table
- Describe MenuScreen's real role in its header
- Mount the update toast and offline badge through the uiStore
- Declare the remaining import-time-effect modules in sideEffects
- Make the replay cap a pref singleton inside replayStore
- Name pause-quota evictions and pin the reason table to SEVERITY
- Damp SCENE_READY duplicates in the scene-ready barrier
- Own deferred round/net timers in subscription bags
- Hide the HUD pause button for spectators on touch devices
v0.3.2 — 2026-08-25
- Second geometry pass on the studio, driven by per-object camera mugshots
- Fix the studio objects' geometry at the primitive level
- Complete the studio's floor ring where the default camera looks
- Show the same weather through every window of a room
- Make the three rooms disjoint, intersection-audited, with one continuous yard
- Give the studio its colour identity: salon wall, pigment shelf, teal wainscot
- Deepen the artist's studio into a full working room
- Refresh the promo gallery to showcase the current build
- Add the artist's-studio room theme
- Add the Corners and Lanes arena layouts
- Pace client input sends at 1.5-2x the observed STATE tier
- Add selectable arena obstacle layouts (Classic / Pillars / Cross / Surprise me)
- Exit-to-menu specs use the secondary results action: since Play Again became the local match-over primary, confirmResults() (the primary click) starts a rematch — the two cleanup specs now leave through exitResults(), and the driver documents which button each helper presses
- New item: the Sponge (🧽) wipes all paint in a 3.5-unit radius around the pickup back to bare canvas in an expanding ease-out wipe — the eraser counterpart of the Paint Bomb, working even while ink-drained. PaintGrid/PaintSystem gain a first-class erase path (owner-0 stamping with the same tally/bucket/generation bookkeeping; diffuse texels restored from the pristine background mirror, sheen back to matte), the wire grows MSG.SPONGE mirroring MSG.BOMB (PROTOCOL_VERSION 6; append-only item ids in the STATE codec and the .bpr table keep old files decoding), and the AI prices it positionally from the shared coarse ownership grid — high in enemy territory, near-worthless on friendly ground (theory.md §10.2). Ships with the 3D model, spawn-ring wipe burst, bomb-clip sfx and haptics reuse, debug-bar entry, minimap marker via the shared labels, and locale names ×7
- Late joiners spectate live: a mid-round connect receives the spectate config on MSG.WAIT (optional-with-default; bare WAIT keeps the parked overlay for legacy hosts and unwatchable states) and the lobby hands off to the ordinary client round with no local slot — every player interpolates, no input is ever sent, pause and blur handlers plus the HUD pause button are disabled, and a Spectating chip shows. The host streams STATE to a match-scoped spectator set that survives per-round session rebuilds, MSG.REQ_PAINT is the client's scene-is-ready signal (re-registers the viewer after heartbeat drops from the engine-chunk stall and fetches the paint laid before the join; sent again after each boundary countdown to survive the round rebuild race), the first spectate round skips countdown and scene barrier while later rounds ride the normal boundary flow, and round/match ends flow into the ordinary results screens with the lobby return dealing the viewer a real seat
- Shareable lifetime-stats card: the Stats dialog's Download Image renders the local record (rounds, wins, win rate, best coverage, best streak, top characters with win bars) to a PNG via StatsImage.js over the shared results canvas kit — the brag artifact a leaderboard would provide, with zero backend and the stats never leaving the device
- Keyboard remapping: the four key sets become remappable slots — Settings → Controls captures Turn Left / Turn Right / Dash per set (Escape cancels, cross-set collisions rejected, Reset restores the factory keys), overrides persist per device and hydrate at boot, and because a remap redefines what the slot is, the four-way claim logic, the playerKeyMaps wire indices, and replays all keep working unchanged; chips render through the layout-aware labels, and the capture listener attaches in a layout effect with a ref-buffered sequence so fast presses are never dropped
- AI leader interception and team item claims: a race the bot narrowly loses to a rival who leads it on coverage keeps an additive denyItem interception bonus (per-tier knob — hard 0.3, hell 0.5 — scaled by race closeness and the standings gap, zero against trailing rivals), and team rounds share a per-round item-claim table threaded from Simulation into every controller so committing to an item takes it out of teammates' scans (2.5 s TTL, position-keyed, clearly-better challengers take over; FFA controllers carry no table); same-seed team bench keeps the team outcome while redistributing pickups toward the closer claimant, FFA aggregates are unchanged at these weights
- Local rematch and faster AI stuck recovery: the match-over screen's Play Again restarts a local match with the identical settings straight from the results (online parties keep rematching through Return to Lobby), and the AI's stuck detector samples positions into a 250 ms ring so a corner pin is escaped in about a second instead of up to two (same 0.8 wu / 1 s threshold; same-seed bench keeps the tier ordering with hell slightly stronger)
- Robustness and accessibility: every uiStore surface renders inside a per-surface error boundary that raises the framework-free fatal dialog instead of blanking the UI tree, a lost WebGL context freezes the live round through the window-blur auto-pause (host broadcasts the pause) before the reload prompt shows, replay saves hitting QuotaExceededError evict the oldest recording and retry so the newest round wins, the pre-round countdown overlay is an assertive live region announcing 3/2/1/Go alongside the audio, and the Visual effects setting grows a middle mode — full / essential / off, where essential keeps gameplay-legibility bursts and stills only the room's decorative ambience (ambienceSuppressed gate; legacy stored booleans normalize; reduced motion still defaults to off)
- Split GameScene's two tangential jobs into collaborators: ArenaEnvironment owns the static arena set (lighting, painted ground, toy-box wall frame, the high-contrast material swap — mirroring RoomEnvironment's shape) and SnapshotApplier owns the client-side application of host state (STATE snapshots with the interpolated paint trail, item reconciliation, RLE paint-sync bands) plus the per-round scratch state that used to sit on the scene
- Architecture extractions: pref singletons persist themselves (createPrefSingleton storeKey + hydrate — SettingsDialog drops ten paired storeSet calls, boot hydrates without writing back), boot prefs read moves to the pure src/boot/readBootPrefs.js (triState/mediaMatches helpers, Jest-covered), match-settings→config mapping is shared by both start flows (matchSettingsToConfig), HostLobbyModel's keymap resolver is renamed canonicalKeyMapBySlot (no more same-name/different-type trap with MenuModel), round outcome resolution is a pure scoring helper (resolveRoundOutcome), host-exit broadcasting is one _broadcastExit, DebugBarBootstrap.forRound owns the DEV toolbar wiring, bomb-paint bus wiring is shared by round and replay (wireBombPaint), ReplayController's scene rebuilds go through one _recreateScene, and the AI drops its two orphaned delegation wrappers
- Hot-path performance: the paint canvas gets a CPU-side diffuse mirror so live dabs never read the canvas back (dirty-rect uploads read the mirror; batched seeks blit once), PaintGrid flags dirty coarse buckets and bumps a generation counter so buildCoarse re-argmaxes only touched buckets and the minimap skips its downsample while nothing was painted, the sim tick reuses its per-frame objects (clampToArena/jumpArcStep out-params, reused update/stroke results, numeric countdowns with countdownExpired, a shared empty expiry list, collision/pickup scratch buffers, PaintGrid's dirty-rect result), Simulation.tick reads the uniform {paintStroke} result from every player kind (the bench adapter is gone), the camera centroid accumulates without per-frame arrays, the HUD timer animates via transform with a data-progress seam and writes text only on change, direction arrows share two scene-cached blink materials, and the AI builds one evaluation context per think cycle with reused quadrant tallies; same-seed ai:bench output is bit-identical
- CI and tooling: run the suite on pull requests (drafts: checks only), shard the static e2e job two ways with merged blob reports, add a pnpm audit step and security-only Cargo advisories, enforce Jest coverage floors per pure layer, lint/format/typecheck scripts/ (second tsc config with @types/node), resolve the transitive audit advisories via pnpm overrides, add a bundle-size trend job posting a sticky PR comment (scripts/bundle-report.mjs), cover src/ui/hooks, colorVisionPref, hapticsPref, ReplayCapture, and RoundHud with Jest, replace three positive fixed sleeps with polls, add focus-trap / aria-live / axe accessibility specs, and tick the HUD at 0 so the round-over announcement fires
- Fix a batch of latent round/netcode defects: getPaintPercentages returns an array on the null path, Simulation owns its dispose, PlayerState.reset and MSG.RESUME drop the network smoothing buffers (no stale-velocity extrapolation after a pause), the HUD no longer receives a per-frame coverage tally it never read, the host projects items lazily and streams STATE only to seated peers, cadence timers carry their overshoot (advanceCadence), HostSession resolves slots through a peerId mirror, STATE rows and small-bomb scatters are shape-validated on the guest, one playerDisplayName helper replaces twelve inline fallbacks, and two RoundController fields are initialised in the constructor
v0.3.0 — 2026-08-23
- Carry STATE over an unreliable no-retransmit datachannel where the transport provides one: symmetric negotiated bp-state channel on the PeerJS connection, sn out-of-order guard at the client, reliable fallback everywhere else; also fix the online-match clock read racing the HUD's first tick
- Let a fresh join reach a migrated broadcast room (retarget hostId on accept, drop the dead-host phantom peer), hold pure prediction on a reconciliation ring miss instead of the heading-dragging stale blend, and add the carry-expiry e2e
- Ignore a superseded connection's late close in PeerManager so a fast reconnect is not torn down by the old link dying
- Fix two migration-layer review findings: a rejoin without a takeover AI still receives the paint-grid sync, and the host-migration match carry applies only while a carried follower actually holds a seat
- Fix seven review findings: reconciliation ring-shift (no re-applied error), heading adoption on divergence snaps, benign rejoin re-claims, paint-sync abort latch and owner validation, bucket-tally verification, config array snapshots, session replay player for remounted menus, surface-store loading-screen dismissal, and focus-existing launch handling
- Register the installed PWA as a .bpr file handler: opening a replay file lands straight in playback via the launchQueue and the live menu's play handler
- Ship STATE in a compact positional wire form (stateCodec: bit-packed flags, mm-rounded floats, ~3x smaller serialized), unpacked at client ingress; PROTOCOL_VERSION 5
- Send STATE at a per-peer cadence tiered on each guest's own RTT, so one laggy peer no longer drags the whole room to 12 Hz
- Reconcile the guest's own player by input sequence: MSG.IN carries a seq the host echoes per slot in STATE, so prediction error is measured against the client's own history instead of the stale host position
- Results stats iterate active slot indices and resolve row/line colors through picked characters, matching the arena palette on non-contiguous rosters
- Re-validate touch exclusivity when split-screen turns off or a touch-input slot becomes human, evicting extra touch humans to keyboard
- Add haptic feedback: gamepad rumble and device vibration for local players on collisions, pickups, and bomb bursts, behind a Vibration setting
- Add PWA install polish: manifest screenshots/shortcuts/launch handler and a ?replays=1 deep link opening the replay picker
- Carry the match score and identities through a host migration
- Store replays as gzipped full-precision JSON instead of raw object graphs
- Upload only dirty paint regions to the GPU each frame
- Reuse per-controller snapshots and the item projection across AI ticks
- Take per-frame allocations and layout reads off the split-camera path
- Replace the tile scorers' per-call friendly Set with a per-round bitmask
- Maintain the AI coarse grid incrementally in PaintGrid
- Keep the Go-burst's ember particles out of the glow layer
- Add a colorblind-friendly player palette as a third accessibility tier
- Add a graphics-quality setting that retunes render resolution live
- Resolve gamepad assignments as ordinals over live pads, not raw indices
- Persist the menu slot setup and match settings across launches
- Surface a recoverable dialog for chunk-load failures and lost WebGL contexts
- Dead-reckon remote players briefly instead of freezing on a late snapshot
- Cap guest-initiated pauses at five per round
- Restore the paint grid to a rejoining guest via MSG.PAINT_SYNC
- Fill the guest's paint stroke between consecutive painted snapshots
- Resolve character-remapped player colors from the active palette everywhere
- Guard REQ_REJOIN so a peer can only reclaim the slot it dropped from
- Validate PONG echoes and forget departed peers' RTT samples
- Re-arm the guest's stale-host watchdog at pause edges
- Unify team membership behind one canonical teamOf() split
- Hold the item-spawn cadence while the field is at the item cap
- Make StrategySelector the single owner of AI personality across round restores
- Fix 4-player spawns for non-contiguous online slots by ranking corner assignment
v0.2.20 — 2026-08-10
- Serve only small raster favicon candidates and moderate-cache the icon files so Google shows the app icon in results
v0.2.19 — 2026-07-28
- Give search engines square thumbnail candidates so results stop letterboxing the wide key art
v0.2.18 — 2026-07-20
- Keep the Go! burst rings out of the lazy GlowLayer and un-race the flash spec
v0.2.17 — 2026-07-20
- Note the celebration, UI blips, and timer pulses in the manuals
- Choreograph the results screen: staggered ranks, pip light-up, sparkline draw, confetti
- Give every UI interaction a synthesized blip and the portrait pick a pop
- Celebrate the winners on the live scene and punch up the round's start and finish
v0.2.16 — 2026-07-20
- Assert log-panel narrowing by content and run flatpak-builder directly on the runner
v0.2.15 — 2026-07-20
- Fail the production build when the menu entrypoint outgrows its byte budget
- Render store listing art in exact per-store sizes via capture:store
- Stage real payloads for the Snap and Flathub lanes and sign the MAS pkg and MSIX
- Make the paint-coalescing and camera-orbit specs tolerate a loaded machine
- Carve item valuation out of tileScoring into an itemScoring module
- Publish the release history as a static /changelog/ page
- Generate a per-version changelog from the git tag history
v0.2.14 — 2026-07-20
- Point the minimap e2e seam at RoundHud and suppress the tutorial in multiplayer sessions
- Cap Playwright at 4 workers so concurrent software-WebGL rounds stay responsive
- Expose the recorded frame count on ReplayCapture for the replay e2e seam
- Chart the new helper modules in the README tree and manuals
- Stage store packages and capture sessions through shared script kits
- Back the UI control components with pure, unit-tested logic modules
- Hand RoundController's HUD and replay wiring to RoundHud and ReplayCapture
- Lift the results-card canvas primitives into resultCanvasKit
- Carve the player checkpoint capture into a pure playerSnapshot module
- Route every workflow's Node+pnpm setup through the shared composite action
- Localise the menu footer nav's accessible name
- Keep the first-launch tutorial out of specs that click Start directly
v0.2.13 — 2026-07-17
- Tuck the sandbox out of the centre pane and hang the classroom clock by the chalkboard
- Declutter the classroom: school door, snake plant, full desk row, no art corner
- Give the classroom its own door, shelf, art line, and world map
v0.2.12 — 2026-07-16
- Keep hill backdrop depth fixed so the rocking swings clear it
- Space classroom windows in proportion to the wall width
- Deepen the classroom-bedroom split with rocking swings, a tidier room, and a number line
v0.2.11 — 2026-07-16
- Give the classroom a curtainless window band onto a playground and seat the teacher at the board
- Furnish the classroom theme with desks, decor, and a linoleum floor
v0.2.10 — 2026-07-16
- Restyle the Stats and tutorial dialogs on the Settings/About design language
- Regroup online parties under a successor host after a silent host drop
- Show a first-launch How to Play dialog gated on a persistent seen-flag
- Add a selectable Art Classroom room theme behind a theme registry
- Split the window builders into per-concern modules over the room kit
- Extract playerMotion, timedEffects, and spawnLayout from PlayerState
- Track lifetime player stats locally with a menu Stats dialog
- Build the AI coarse grid lazily and reuse per-player observations across ticks
- Cover pointerShared, the Transport contract, and gamepadMenuNav with Jest mirrors
v0.2.9 — 2026-07-13
- Publish static guide and privacy pages with crawlable interlinks
v0.2.8 — 2026-07-13
- Split itemModels into per-item builder modules over a shared kit
- Share per-tick player projections across AI snapshots
- Cover AITargeting with a Jest mirror and sync the AI docs
- Format the gameEventTypes freeze test
v0.2.7 — 2026-07-10
- Replace polling-shaped hard waits with expect.poll in e2e specs
- Extract target selection from AIController into AITargeting
- Drive menu navigation from gamepads on non-TV platforms
- Auto-retry the client reconnect with visible backoff attempts
- Cover the joinFail transport seam in the createTransport tests
- Show classified join-error copy for bad codes vs connectivity failures
- Coalesce paint GPU texture uploads to once per rendered frame
- Maintain incremental per-owner paint tallies for O(1) coverage
- Add tests for the UI store and transition wrapper
- Throttle the minimap paint-layer rescan
- Add tests for the small pure util and DOM modules
- Add invariant tests for the AI tuning tables
- Convert comment headers to JSDoc on exported APIs
- Skip AI snapshot-input building when no AI controllers exist
- Share the dash latch and canvas projection between mouse and touch input
- Extract the lobby overlay focus-preserving notify helper
- Add a jest coverage script
- Add an HTML report artifact for CI e2e triage
- Fail instead of skip when the AI or replay hook is missing in e2e
- Correct accessibility doc's themes-and-contrast section
- Remove the ready-mode PlayerCard variant and its stranded CSS
- Drop three unreferenced test-seam exports
- Remove the unused Plain-English AI label tables
- Delete the dead ReadyDialog module pair
- Split replayBinary encode/decode into per-section helpers
- Split tileScoring's itemBaseValue and cellWeight into per-case helpers
- Split aiSteering into threat, wall, and item-race sub-behaviors
- Decompose StrategySelector.choose into perception helpers
- Extract focusTrap's Tab-wrapping into a helper
- Extract EditableName error bubble and chip into module helpers
- Split Minimap.update into paint, item, and player draw passes
- Decompose makeCoverageGraph into grid, label, and polyline appenders
- Extract CameraMouseInput pointer handlers from the constructor
- Extract TouchInput touch handlers from the constructor
- Split RoundNetBridge.build into bus wiring and coordinator deps
- Extract buildClientCfg and the terminal-dialog table from _onJoinMessage
- Extract Game._showRoundOver's between-round guard and lobby handler
- Split NetworkCoordinator.setup into host and client session builders
- Split RoundController._update into countdown and playing ticks
- Split ResultImage._drawSpotlight into card and text draw helpers
- Split makeSplatShape into per-feature generators
- Split buildDustMotes into sunbeam and mote-scatter builders
- Split buildLamp into stand and shade builders
- Split swatchCardMaterial into paper and column draw helpers
- Decompose buildPlushBear into body, face, and bow builders
- Decompose buildPalette into board, dab, and brush builders
- Decompose buildPaintCans into body and open/closed-top builders
- Decompose buildChair into seat, leg, stretcher, and back builders
- Decompose buildDoor into casing, frame, panel, and hardware builders
- Decompose buildBed into frame, bedding, pillow, and board builders
- Split paintings artMaterial into per-motif draw helpers
- Split buildPrecip into material and per-particle helpers
- Split GameScene._buildWalls into materials, panels, and posts
- Split itemModels brush, potion, and bomb builders into sub-part helpers
- Extract AIController._sampleCandidate from _pickAreaScore
- Decompose buildSunMoon and buildWindowFrame into per-part builders
- Decompose ResultImage coverage chart and items card into sub-draw helpers
- Split PaintSystem._redrawFromCells into fast and fallback cell painters
- Extract HostSession peer-event handlers from the constructor
- Split NetworkCoordinator client handlers into per-message methods
- Split createRoomKit helper attachment by concern
- Decompose buildBookshelf into carcass, bay, and decor builders
- Decompose buildRockingHorse into per-assembly builders
- Decompose buildToyChest into per-part builders
- Extract SparkleTrail star texture and mesh pool from _build
- Decompose playMagicSparkles into texture, resource, and per-sparkle helpers
- Decompose playSmallExplosions into shared-resource and per-burst helpers
- Decompose playExplosion into per-layer particle builders
- Split itemModels._buildShoe into named phase sub-builders
- Decompose GameScene.buildScene into phase helpers
- Decompose RoundController._setup into _setup* phase helpers
- Split ItemState.checkPlayerCollisions into per-pass helpers
- Decompose Player.update render-half into per-aspect _sync* helpers
- Extract PlayerState._integrateJump from update()
- Dedupe ResultImage layout math, player-color, and stats-card scaffolding
- Split MenuBackground into pure geometry, canvas renderer, and mount
- Decompose AIController.update into per-behavior tick helpers
- Split tileScoring's oversized scorers into named pure sub-functions
- Share the player name-label and direction-arrow billboard material
- Split the clock item model into per-sub-assembly builders
- Share the soft-disc gradient texture and unlit-alpha material
- Decompose RoundController _setup and route teardown through a disposer bag
- Extract GameScene's slot and control-role logic into a pure module
- Decompose buildWindow into per-section builders
- Extract the AI strategy-scoring math into a pure, testable module
v0.2.6 — 2026-07-04
- Share the menu/lobby store seam and keymap-claim algorithm
- Split the shoe item model and share its material-cache scaffold
- Decompose the AI target-selection branches into pure pickers
- Extract shared timed-effect lifecycle and material helpers
v0.2.5 — 2026-07-04
- Extract the AI steering and dash policy into a pure, testable module
- Extract the AI charge-decision math into a pure, testable module
- Extract the split-screen overlay chrome out of RoundController
- Move the AI charge and steering tuning constants into tables.js
- Extract a shared consent-gated activation base for ads and analytics
- Collapse the pref singletons into a factory and unify subscription teardown
v0.2.4 — 2026-07-03
- Contain the room-code field in a real join form
- Keep server dotfiles out of the service-worker precache
v0.2.3 — 2026-07-03
- Stop the concentric-sweeper exploit: contest the wall band, interdict the frontier
v0.2.2 — 2026-07-03
- Price CPU ground by rank: hell hunts the live pairwise margin late
- Wire the recorded standings into AIController item pricing
- Teach CPUs team division of labor and item-race contesting
- Bend Hell routes around write-offs and re-tune the tier vector
- Price bot routes by the paint they earn
- Give Normal bots a competent dodge
- Sharpen AI interception and item commitment
- Teach Hard/Hell bots to spend the dash cooldown
v0.1.46 — 2026-07-02
- Add a dash: tap-to-burst speed with cooldown
v0.1.45 — 2026-07-02
- Name the collected item in the pickup toast
- Steer toward taps and clicks outside the battleground
- Fix light-theme chart labels, 720p overflow, and off-brand replay HUD
v0.1.44 — 2026-07-01
- Reshape the Magic Shoe ankle zone into a fitted high-top
v0.1.43 — 2026-06-23
- Make player cards opaque so the splatter backdrop never bleeds through
v0.1.42 — 2026-06-22
- Stop the Orejime CMP double-loading en + fr on a non-en boot
v0.1.41 — 2026-06-22
- Fix unstyled online panel on the menu (orphaned lobby.css)
v0.1.40 — 2026-06-22
- Fix Start button + dialog cards unstyled (orphaned kit CSS)
v0.1.39 — 2026-06-22
- Lazy-load PeerJS, lobby + Settings off the menu boot chunk
v0.1.38 — 2026-06-22
- Cut menu main-thread work to improve Lighthouse performance
- Add upload-assets to .gitignore
v0.1.37 — 2026-06-21
- Add Mac App Store App Sandbox entitlements to the store build
v0.1.36 — 2026-06-21
- Add store packaging + CI lanes for mobile/TV/desktop-stores/Xbox
- Add BP_STORE/BP_TV build flavors and a store-safe (no self-updater) Tauri overlay
v0.1.35 — 2026-06-21
- Give the brush item a dark-blue handle and shrink the brush, shoe, and clock items
v0.1.34 — 2026-06-21
- Attach the speed-penalty bandage to the brush body bone and seat it flush
v0.1.33 — 2026-06-18
- Rework the Magic Shoe as a laced Converse high-top
v0.1.32 — 2026-06-17
- Replace the speed-penalty bandage billboard with a 3D crossed-plaster on the brush
v0.1.31 — 2026-06-13
- Give the balloons a subtle latex sheen
v0.1.30 — 2026-06-13
- Hollow out the toy chest with toys nestled inside
v0.1.29 — 2026-06-13
- Improve the plush bear: projecting snout, lively eyes, inset ears, hugging arms
v0.1.28 — 2026-06-13
- Round out the rocking horse: connected rounded legs, neck, mane, saddle, tail
v0.1.27 — 2026-06-13
- Revert "Round out the plush bear and rocking horse into proper toys"
- Round out the plush bear and rocking horse into proper toys
v0.1.26 — 2026-06-13
- Shape the shoe into a Converse high-top and lathe the brush smooth
v0.1.25 — 2026-06-13
- Item polish: the alarm clock tells the real time, the brush goes paint-free
v0.1.24 — 2026-06-13
- Stabilize the ink sheen under camera motion and polish the item trio
v0.1.23 — 2026-06-13
- Flatten the ink to one even sheen and sharpen the emoji likeness
v0.1.22 — 2026-06-12
- Match the item models to their emojis and settle the ink into a satin sheen
v0.1.21 — 2026-06-12
- Soften the ink relief and rework the shoe, brush, and clock models
v0.1.20 — 2026-06-12
- Refine the scene: Splatoon-style ink, realistic items, glow that spares players
v0.1.19 — 2026-06-12
- Upgrade the game scene: 3D item models, wet-paint sheen, VFX glow, wall trim
v0.1.18 — 2026-06-12
- Slim the surface handle and share the HUD screen-reader announcer
v0.1.17 — 2026-06-12
- Make HUD toasts show the sentence around the player name
v0.1.16 — 2026-06-12
- Share the match-settings bar, fix latest-ref effect deps, close HC palette gaps
v0.1.15 — 2026-06-12
- Re-render the host lobby through HostLobbyModel's own store seam
v0.1.14 — 2026-06-12
- Consolidate UI helpers and remove render-phase model writes
v0.1.13 — 2026-06-12
- Attach imperative ref hosts once per mount, not once per re-render
v0.1.12 — 2026-06-12
- Run the UI on Preact via preact/compat
v0.1.11 — 2026-06-12
- Drop an inert lint suppression in the replay transport
- Rewrite the UI documentation for the React architecture
- Convert the replay picker and consent fallbacks; retire the DOM primitives
- Convert the host and client lobbies to React
- Convert the main menu to React
- Convert the replay playback chrome to React
- Convert the in-round HUD to React
- Convert the results screen and host-exit dialog to React
- Convert the between-round screen to React
- Convert the loading / ready-check screen to React
- Convert the pause dialog family to React
- Convert the settings dialog to React
- Convert the About dialog and doc viewer to React
- Convert the ready dialog to React; add the match-settings subscribe seam
- Convert the share dialog to React
- Add React UI primitives with attribute-identical DOM
- Lay the React UI foundation: build config, surface store, root, hooks
v0.1.10 — 2026-06-12
- Add an App Store-style promo page at /promo/
v0.1.9 — 2026-06-11
- Add a promo-video capture pipeline and a shared capture kit
v0.1.8 — 2026-06-11
- Open the desktop app maximized
v0.1.7 — 2026-06-11
- Add a self-updating desktop app via the Tauri updater
v0.1.6 — 2026-06-11
- Make installed-PWA updates reliably detected, re-prompted, and applied
v0.1.5 — 2026-06-11
- Refresh lobby labels in place when the language changes
- Keep the lobby active when the language changes from its Settings dialog
- Restyle the room-code row: ticket chip on its own line, accented Invite Link
- Add a lobby invite link that deep-links guests straight into the join lobby
v0.1.4 — 2026-06-11
- Update the app icon artwork and regenerate all derived icons
v0.1.3 — 2026-06-10
- Deflake the head-to-head rotation specs
v0.1.2 — 2026-06-10
- Publish a GitHub Release for every deployed version
- Auto-bump the patch version on every web deploy
- Add TV device support: Remote input, D-pad navigation, 10-foot mode
- Poll the you-card hover shadow instead of sampling one frame
- Support the stated browser floor (Chrome 87 / Edge 88 / FF 78 / Safari 14.1)
- Harden the workflows: fail-fast CI graph, least privilege, timeouts
v0.1.0 — 2026-06-10
Initial public release.