Skip to content

Latest commit

 

History

History
305 lines (284 loc) · 37.8 KB

File metadata and controls

305 lines (284 loc) · 37.8 KB

Original prompt: cant get this app to start locally

  • Investigated npm run build failure on macOS.

  • Root cause: src/engine/components/Unit.ts re-exported from ./unit, which can resolve ambiguously on case-insensitive filesystems because the facade itself is Unit.ts.

  • Applied fix: changed the facade to re-export from ./unit/index explicitly.

  • Verified npm run build succeeds.

  • Verified npm run type-check succeeds.

  • Verified npm run dev starts successfully; Next selected http://localhost:3001 because port 3000 was already in use locally.

  • The develop-web-game Playwright client could not be used because the playwright package is not installed in this environment.

  • No further action required for the original startup/build issue.

  • Investigated pathfinding regression on elevated maps.

  • Reproduced the bug below the game loop: Recast paths on elevated bundled maps were truncating partway up/down ramps, while the flat test map still reached its destination.

  • Replaced the flat-per-cell navmesh geometry path with a shared ramp-aware geometry builder and wired editor validation to the same logic.

  • Removed the terrain-grid fallback after confirming the root issue was navmesh geometry, not movement execution.

  • Added ramp metadata normalization so both bundled ramps and editor-inferred flat ramps derive their direction and endpoint elevations from surrounding walkable terrain before Recast heightfields are built.

  • Replaced the fallback-specific tests with Recast connectivity regressions for contested_frontier, crystal_caverns, titans_colosseum, and a synthetic flat-ramp editor map.

  • Verified npm run type-check, targeted recastRampConnectivity and pathfindingSystem tests, full npm test (72 files / 2547 tests), and npm run lint with only pre-existing warnings.

  • TODO: Verify full long-haul spawn-to-spawn routes on scorched_basin and void_assault if we want cross-map regression coverage beyond the local elevated-move cases.

  • Updated the PWA install UI so the global bottom-right install prompt no longer renders from the app layout.

  • Reworked src/components/pwa/InstallPrompt.tsx into a compact InstallAppButton that reuses the existing install flow but renders as an icon-only control.

  • Added the compact install button beside the existing mute/fullscreen controls on the home page, game setup page, and editor header.

  • Verified npm run type-check and npm run build pass after the UI change.

  • Verified targeted ESLint on the touched files reports only two pre-existing warnings: the unused eslint-disable in src/app/game/setup/page.tsx and the existing custom-font warning in src/app/layout.tsx.

  • Browser-level visual verification of the install button placement is still blocked here because the repo does not include a usable playwright runtime, and the install prompt itself depends on a browser-only beforeinstallprompt event.

  • Continued investigating the pathfinding stop-after-an-inch bug after gameplay reports showed it also happened on multiple bundled maps near starting bases, not just on ramps.

  • Root cause: the nested pathfinding worker could finish loading its navmesh after startup buildings and decoration collisions were already registered on the authoritative main-thread RecastNavigation TileCache. In that case the worker never received those existing obstacles, so it planned straight through the starting HQ/decor while movement/collision stopped units almost immediately.

  • Applied fix in src/engine/systems/PathfindingSystem.ts: retain registered decoration collisions, and whenever the worker reports navMeshLoaded, replay all current building and decoration obstacles into the worker so worker-side path queries match the authoritative obstacle state.

  • Added a regression test in tests/engine/systems/pathfindingSystem.test.ts that simulates late worker readiness and asserts existing building plus large decoration obstacles are replayed into the worker while tiny decorative clutter is ignored.

  • Updated docs/architecture/OVERVIEW.md to document that worker navmesh loads/reloads now trigger a replay of dynamic obstacles.

  • Verified npm test -- tests/engine/systems/pathfindingSystem.test.ts passes.

  • Verified npm run type-check is still blocked by a pre-existing unrelated issue in tests/scripts/launch-voidstrike.test.ts where the Vitest globals (describe, it, expect, afterEach) are not declared.

  • Switched the elevated-map movement investigation to the production build path and isolated the real root cause in input projection, not Recast corridor generation.

  • Built a direct terrain probe comparing RTSCamera.screenToWorld() against a real Three.js raycast into the rendered terrain mesh. On elevated maps such as contested_frontier, the old heightfield iteration could snap a click from the visible upper plateau onto the lower cliff layer instead.

  • Applied the fix in src/rendering/Camera.ts: when a terrain object is registered, screenToWorld() now raycasts against the actual terrain mesh first and only falls back to heightfield iteration if no terrain object is available.

  • Wired the production game camera to the terrain mesh in src/components/game/hooks/useWebGPURenderer.ts, and also corrected the editor terrain raycast path in src/editor/core/Editor3DCanvas.tsx to recurse into terrain chunks.

  • Added tests/rendering/Camera.test.ts as a regression using contested_frontier; it demonstrates the old plateau-edge miss and verifies the camera now matches the rendered terrain hit.

  • Updated docs/architecture/rendering.md to document that click projection uses the terrain render mesh on multi-elevation maps.

  • Verified npm test -- tests/rendering/Camera.test.ts tests/engine/systems/pathfindingSystem.test.ts passes.

  • Verified npm run build passes with the production build path.

  • Verified targeted ESLint on the touched files reports only pre-existing warnings in src/components/game/hooks/useWebGPURenderer.ts and src/editor/core/Editor3DCanvas.tsx; no new lint errors were introduced.

  • Continued the elevated-map stop-after-an-inch investigation after gameplay reports ruled out both ramps-only and camera-click projection.

  • Proved the actual root cause with direct TileCache experiments on crystal_caverns: building obstacles were being inserted at y=0, so on elevated maps they only affected the ground layer while the real HQ/platform navmesh sat around y≈8.8. That is why test_6p_flat worked and elevated maps did not.

  • Applied the root fix in src/engine/pathfinding/RecastNavigation.ts, src/workers/pathfinding.worker.ts, and src/editor/services/EditorNavigation.ts: dynamic obstacles now sample the terrain/navmesh height at their footprint before being inserted into TileCache, and obstacle updates now loop until TileCache reports upToDate.

  • Added tests/engine/pathfinding/recastDynamicObstacleElevation.test.ts to verify an elevated crystal_caverns HQ obstacle forces a reroute instead of leaving the path straight through the base footprint.

  • Reverted the temporary non-root collision/camera hypothesis changes:

    • removed the hard-collision margin experiment in src/engine/systems/movement/PathfindingMovement.ts
    • removed the temporary camera raycast changes and deleted tests/rendering/Camera.test.ts
  • Updated docs/architecture/OVERVIEW.md to document that elevated dynamic obstacles are inserted on the sampled terrain/navmesh layer rather than hard-coded y=0.

  • Verified npm test -- tests/engine/pathfinding/recastDynamicObstacleElevation.test.ts tests/engine/pathfinding/recastRampConnectivity.test.ts tests/engine/systems/pathfindingSystem.test.ts passes (23 tests).

  • Verified npm run build passes in production mode.

  • Verified targeted ESLint on the touched source/test files reports only pre-existing EditorNavigation.ts console warnings; no new lint errors.

  • Verified npm run type-check is still blocked by the pre-existing Vitest-global issue in tests/launch/launch-voidstrike.test.ts.

  • Installed playwright under $HOME/.codex/skills/develop-web-game so the required Playwright client can run without changing repo dependencies.

  • Ran the required Playwright client twice against the production server (output/web-game-prod/shot-0.png and output/web-game-prod-2/shot-0.png). The client renders /game/setup correctly, but automated Start Game button clicks still do not transition into gameplay in this environment, so browser smoke verification remains blocked by the same UI automation limitation rather than the pathfinding code.

  • TODO: Have the user manually retest the production build on an elevated map near the starting HQ/platform now that dynamic obstacles are on the correct nav layer.

  • Changed direction from speculative fixes to live reproduction telemetry in the actual browser production path.

  • Added a local telemetry client in src/engine/debug/pathTelemetry.ts plus a Node route at src/app/api/debug/pathfinding/route.ts that appends JSONL events to output/live-pathfinding.jsonl.

  • Instrumented the browser input path in src/engine/input/handlers/GameplayInputHandler.ts to log right-click screen/world targets and the exact MOVE commands issued from the live game.

  • Instrumented the authoritative worker path in src/engine/workers/GameWorker.ts, src/engine/workers/WorkerBridge.ts, src/engine/workers/types.ts, and src/engine/systems/PathfindingSystem.ts so live reproductions capture command receipt, path requests/results, tracked unit snapshots, and explicit movement-stalled events from the real simulation.

  • Updated docs/architecture/OVERVIEW.md to document the local live path telemetry flow and output file.

  • Verified npm run build passes with the telemetry changes.

  • Verified targeted ESLint on the touched telemetry files passes.

  • Verified the telemetry sink end-to-end with a synthetic POST to http://127.0.0.1:3001/api/debug/pathfinding, which wrote to output/live-pathfinding.jsonl.

  • Restarted the production server on port 3001 and left a live tail running on output/live-pathfinding.jsonl so the next manual reproduction can be inspected immediately.

  • The first telemetry build regressed gameplay input because worker-side telemetry forwarding was too broad: PathfindingSystem traces for all background gatherer repaths were being bridged to the main thread, creating unnecessary message volume on startup.

  • Fixed that in src/engine/workers/GameWorker.ts by only forwarding system-originated path telemetry when it belongs to an actively tracked user-command trace.

  • Rebuilt production, cleared the live trace file, and restarted the production server on port 3001 with the reduced telemetry scope.

  • After user testing still showed missing move input, trimmed GameplayInputHandler telemetry again so the right-click path no longer walks selected entities/components before issuing the command; the UI trace now records only screen/world click position plus selected count.

  • Rebuilt production and restarted port 3001 again on that simplified input-path build.

  • Live production telemetry finally isolated the elevated-map stop path precisely:

    • the nested worker path query returns found:false immediately for elevated worker move orders
    • MovementOrchestrator re-requests a path 10 ticks later
    • PathfindingSystem.queuePathRequest() then hits the failed-path cache for the same destination cell and clears targetX/targetY, which is why units go idle after moving only a short distance
  • Root fix applied in src/engine/systems/PathfindingSystem.ts: worker findPath requests now resolve startHeight/endHeight from the same terrain source used by navmesh generation, falling back to GameCore.getTerrainHeightAt() when no custom terrain height provider was injected. This fixes the authoritative worker case, where the terrain grid exists but terrainHeightFunction was null, so elevated queries were being sent to the nested path worker at height 0.

  • Added a regression in tests/engine/systems/pathfindingSystem.test.ts that verifies elevated worker path requests send nonzero terrain-derived heights even without a custom height callback.

  • Updated docs/architecture/OVERVIEW.md to document the terrain-grid fallback for worker query heights.

  • Verified npm test -- tests/engine/systems/pathfindingSystem.test.ts passes.

  • Verified npm run build passes on the production path after the height fix.

  • TODO: Restart the production server on 3001, clear output/live-pathfinding.jsonl, and have the user rerun the same elevated worker move to confirm worker path_result events switch from found:false to real Recast paths.

  • Investigated the economy/UI desync where workers visibly mine and return cargo but the mineral counter never increases.

  • Root cause: in worker mode, ResourceSystem was crediting minerals into the worker's authoritative playerResources map and GameWorker.sendRenderState() was serializing that updated resource state, but useWorkerBridge only copied gameTime out of each render snapshot. The HUD reads from the main-thread Zustand store, so gathered minerals and worker-side supply changes never reached the UI.

  • Applied fix:

    • added syncPlayerResources() to src/store/gameStore.ts so the main thread can atomically mirror minerals, plasma, supply, and max supply from worker authority
    • added src/components/game/hooks/syncWorkerPlayerResources.ts and wired useWorkerBridge to copy the local player's renderState.playerResources into Zustand on every worker render update
    • updated docs/architecture/OVERVIEW.md to document that worker snapshots now drive the local HUD resource state
  • Added regression coverage in tests/components/game/hooks/syncWorkerPlayerResources.test.ts for both successful local-player sync and no-op behavior when the player is absent/spectating.

  • Verified npm test -- tests/components/game/hooks/syncWorkerPlayerResources.test.ts passes.

  • Verified npm test -- tests/engine/systems/resourceSystem.test.ts tests/components/game/hooks/syncWorkerPlayerResources.test.ts passes (62 tests).

  • Verified targeted ESLint on the touched files passes cleanly.

  • npm run type-check is still blocked by the pre-existing Vitest globals issue in tests/launch/launch-voidstrike.test.ts.

  • Ran the required Playwright smoke script against http://localhost:3001/game/setup and inspected output/web-game-resource-sync/shot-0.png plus shot-1.png; automation still stayed on the setup screen after the Start Game click, so live gameplay verification of mining remains blocked by the existing setup-flow automation limitation rather than this resource-sync fix.

  • Completed validation for the deterministic-math cutover and legacy fixed-point removal.

  • Verified npm run type-check, npm run build, and npm test pass after the cutover. npm run lint also passes with warnings only; the warnings are pre-existing and outside the cutover scope.

  • Browser validation against the production server on 127.0.0.1:3100 succeeded in the existing Playwright session:

    • /game/setup renders correctly
    • Start Game transitions into /game
    • the loading screen advances into the live HUD
    • the in-game Idle selector and command card still respond
    • no new browser errors appeared; console output stayed at the existing favicon.ico 404, audio/alert/not_enough_plasma.mp3 404, and [GPUTimestampProfiler] Already initialized warning
  • Fresh headless Playwright launches in this environment fail before gameplay with THREE.WebGLRenderer: Error creating WebGL context. and the generic Next.js client error screen. That reproduces even without touching game state and is environment/WebGL related, not evidence of a regression from the deterministic-math refactor.

  • While validating command flow, confirmed an existing UX/code mismatch in src/engine/input/handlers/CommandInputHandler.ts: command-target mode executes move on left-click and cancels on right-click, while the UI tooltip still says Move to location (right-click).

  • Investigated the report that build-menu clicks do nothing and no scaffold/blueprint appears.

  • Headed Playwright repro on the live dev server showed the actual behavior split:

    • wall commands still enter placement mode immediately (Placing wall_segment...)
    • unaffordable structure commands stay in the submenu with no visible explanation because the command card only emitted audio alerts on disabled clicks
  • Added getDisabledCommandFeedback() in the command-card layer so disabled clicks now emit the same audio cue plus a visible ui:error reason such as Not enough minerals, Not enough plasma, Supply blocked, or Requires <building>.

  • Added regression coverage in tests/components/game/getDisabledCommandFeedback.test.ts for resource, supply, and requirements feedback selection.

  • TODO: If players are still confused about building starts, consider adding a more explicit resource-state hint in the setup/HUD because normal starts currently begin at 50 minerals while most structures cost more.

  • Follow-up repro showed the real first-click placement bug still existed even with enough resources:

    • after setresources 500 0, clicking Build Basic -> Supply Cache entered build mode
    • but the preview still initialized at the preview object's default (0,0) until a mousemove arrived in building context
    • the first placement click therefore used stale preview coordinates, so a quick button-click -> map-click flow could cancel without placing anything
  • Fixed src/engine/input/handlers/BuildingInputHandler.ts so:

    • onActivate() seeds the preview from the current pointer via InputManager.containerToWorld()
    • the left-click placement path re-samples event.worldPosition before reading getSnappedPosition() / isPlacementValid()
  • Added tests/engine/input/handlers/buildingInputHandler.test.ts covering both activation seeding and first-click placement using the actual click world position.

  • Follow-up browser repro showed one more race: the HUD switched isBuilding immediately, but WebGPUGameCanvas only changed InputManager context in a later React effect, so a fast menu-click -> terrain-click sequence still hit GameplayInputHandler.

  • Fixed src/components/game/CommandCard/hooks/useUnitCommands.ts so build and wall command actions switch InputManager context synchronously when they arm placement mode.

  • Live DOM instrumentation then showed terrain clicks were reaching the canvas/container, so the remaining blocker was inside the build handler path.

  • Root cause: useGameInput() only pushed placementPreviewRef.current / wallPlacementPreviewRef.current into the handlers during a one-shot effect keyed on the ref objects, but those refs are populated later by useWebGPURenderer. The handlers could therefore keep a permanent null preview reference.

  • Fixed src/components/game/hooks/useGameInput.ts to retry preview-ref wiring with requestAnimationFrame until the renderer-created preview instances exist, then hand them to the building/landing/wall handlers.

  • After that fix, the blueprint started following the cursor and invalid terrain clicks now cancel cleanly instead of doing nothing.

  • Found a second authoritative-state bug that explains the user's 100 minerals report: WorkerGame.spawnInitialEntities() was hardcoding every spawned player back to 50 minerals / 0 plasma, so the worker could still reject structure builds even when the setup UI or temporary HUD state showed more.

  • Fixed useWorkerBridge / WorkerBridge / GameWorker to forward numeric starting-resource values into the worker spawn message and apply them to worker-side player resources at base spawn.

  • Added tests/engine/workers/gameWorker.test.ts coverage to lock worker spawn resources to the provided starting-resource payload.

  • The new worker regression test initially failed because spawnInitialEntities() now reaches sendRenderState() and Vitest does not define worker postMessage; fixed the test harness by stubbing postMessage in tests/engine/workers/gameWorker.test.ts.

  • Verified npm test -- tests/engine/workers/gameWorker.test.ts tests/engine/input/handlers/buildingInputHandler.test.ts tests/components/game/getDisabledCommandFeedback.test.ts passes.

  • Verified npm run build passes after the worker starting-resource fix.

  • Live browser repro on http://127.0.0.1:3101/game/setup now succeeds end to end:

    • selected High starting resources from the setup UI
    • started the game, used the Idle button to select a worker, opened Build Basic, chose Supply Cache
    • clicked valid terrain and confirmed the placement banner cleared, the scaffold appeared, minerals dropped from 500 to 400, and idle workers dropped from 6 to 5
    • artifacts: output/live-build-verify-2/05-supply-cache-mode.png, 06-before-place-click.png, 07-after-place-click.png, plus result.json
  • The required develop-web-game wrapper script still hangs in this environment before producing artifacts, so the successful gameplay verification used a direct Playwright script against the same live page instead.

  • Investigated the lobby start regression where the first Start Game click flashed /game and dumped the player back to /game/setup.

  • Reproduced the exact route bounce in next dev: /game/setup -> /game -> /game/setup on the first click, which matched a React Strict Mode mount/unmount/remount probe rather than a form submit.

  • Root cause: src/app/game/page.tsx called endGame() directly from the gameplay page effect cleanup. In development, Strict Mode immediately invokes that cleanup during its remount probe, which cleared gameStarted before the route could settle.

  • Applied fix:

    • added src/app/game/gamePageLifecycle.ts so gameplay teardown is deferred by one microtask and only runs if the page stays unmounted
    • updated src/app/game/page.tsx to use that helper instead of unconditionally clearing the session on every effect cleanup
    • added tests/app/game/gamePageLifecycle.test.ts to lock the immediate-remount case and the real-unmount teardown case
  • Updated docs/architecture/OVERVIEW.md to document that /game teardown is Strict-Mode-safe.

  • Verified npm test -- tests/app/game/gamePageLifecycle.test.ts passes.

  • Verified npm run build passes.

  • Verified browser automation against both http://127.0.0.1:3101/game/setup (next dev) and http://127.0.0.1:3102/game/setup (next start) now keeps the first click on /game and advances into the loading screen instead of bouncing back.

  • Ran the required Playwright client after the fix and captured output/web-game-start-fix/shot-0.png.

  • Re-verified the same lobby-start report against the current workspace on 2026-03-15.

  • Confirmed the existing Strict-Mode-safe /game teardown fix is still present in src/app/game/page.tsx and src/app/game/gamePageLifecycle.ts; no additional code change was needed.

  • Browser checks:

    • manual Playwright probe on http://127.0.0.1:3101/game/setup (next dev) transitions to /game on the first Start Game click
    • manual Playwright probe on http://127.0.0.1:3001/game/setup (launch path) also transitions to /game on the first click
  • required develop-web-game client run against http://127.0.0.1:3001/game/setup captured output/web-game-lobby-start-verify/shot-0.png, which shows the in-game loading screen after a single click

  • Verified npm test -- tests/app/game/gamePageLifecycle.test.ts still passes.

  • Verified npm run build still passes.

  • Continued the lobby-start investigation after the manual retest still reported “click start twice.”

  • Found a second real root cause beyond the earlier /game teardown bounce:

    • the visible Start Game button could render before the setup page finished hydrating, so an early click was silently dropped because the client handler was not attached yet
    • regular browser sessions could also stay on a stale cached /game/setup shell because public/sw.js served navigation HTML with stale-while-revalidate
  • Applied fixes:

    • added src/app/game/setup/getStartGameButtonState.ts and updated src/app/game/setup/page.tsx so Start Game stays disabled with a Preparing lobby... hint until hydration completes
    • changed public/sw.js navigation requests to network-first with cache fallback and bumped the service-worker cache namespace to v2, while keeping hashed static shell assets on stale-while-revalidate
    • added regressions in tests/app/game/setup/getStartGameButtonState.test.ts and tests/app/serviceWorkerRouting.test.ts
  • Verified:

    • npm test -- tests/app/game/gamePageLifecycle.test.ts tests/app/game/setup/getStartGameButtonState.test.ts tests/app/serviceWorkerRouting.test.ts
    • npm run build
    • required develop-web-game client against http://127.0.0.1:3200/game/setup now captures output/web-game-lobby-start-hydration-fix/shot-0.png, which shows the loading screen after a single automated click in the same workflow that previously stayed on setup
    • fresh browser cache inspection on http://127.0.0.1:3200/game/setup now reports voidstrike-assets-v2, voidstrike-shell-v2, and voidstrike-data-v2
  • Observation-only repro on 2026-03-15 against a fresh next dev instance at http://127.0.0.1:3001.

  • Setup used for the watch:

    • map: Scorched Basin
    • 4 players, FFA, all AI
    • fog of war disabled
    • observed in spectator mode via headed Playwright session
  • Observed for just over 10 minutes of in-game time (from roughly 00:27 to 11:18 on the game clock).

  • Captured screenshots:

    • initial upper-right sample: .playwright-cli/page-2026-03-15T17-22-22-429Z.png (00:55)
    • upper-left sample: .playwright-cli/page-2026-03-15T17-24-07-806Z.png (02:40)
    • upper-right sample: .playwright-cli/page-2026-03-15T17-25-42-498Z.png (04:15)
    • upper-left ramp-exit sample: .playwright-cli/page-2026-03-15T17-28-18-587Z.png (06:51)
    • upper-right combat sample: .playwright-cli/page-2026-03-15T17-31-03-878Z.png (09:36)
    • end-of-window sample: .playwright-cli/page-2026-03-15T17-32-45-830Z.png (11:18)
  • Result from this specific repro:

    • no browser/game freeze occurred during the 10-minute watch
    • both upper spawns appeared able to leave their base area
    • at 06:51, the upper-left spawn clearly had multiple units moving down the ramp/off the plateau, so this run did not reproduce a general “cannot pathfind out of base” failure on Scorched Basin
    • no new browser-console errors appeared beyond the existing startup warnings about definitions initialization
  • Current suspicion remains code-side rather than conclusively disproven:

    • AI rally/recovery logic in src/engine/systems/ai/AITacticsManager.ts still hard-codes basePos + (10, 10) for several army regroup/recovery paths, which could fail on some rotated/elevated spawns even though it did not fail in this Scorched Basin run
  • Next useful repros:

    • exact map/slot that previously showed “stuck at the edge of the upper base portion”
    • enable AI/pathfinding debug logging before the match if we need to catch repeated regroup commands to one coordinate
  • Audited shipped map JSONs on 2026-03-15 against the current generator pipeline.

  • Verified the game ships maps from src/data/maps/json/index.ts, which currently loads:

    • battle_arena, contested_frontier, crystal_caverns, scorched_basin, test_6p_flat, titans_colosseum, void_assault
  • Validation result:

    • npx --yes tsx scripts/validate-maps.ts passed for every bundled JSON
    • schema validation, deserialization, terrain dimensions, and “spawn on walkable terrain” checks all passed
  • Direct comparison against generator outputs:

    • regenerated the current LLM-script maps into a temp directory and compared shipped JSONs semantically
    • crystal_caverns, void_assault, scorched_basin, and titans_colosseum matched the LLM generator on gameplay-relevant top-level fields (spawns, expansions count, watch towers, ramps count, destructibles)
    • contested_frontier did not: shipped JSON has 36 ramps while current LLM generation produces 49
    • regenerated the older script as well; shipped contested_frontier matches the older generator, while the other ranked maps do not
  • Implication:

    • the current 4-player Scorched Basin issue is unlikely to be caused by stale/legacy map JSON, because the shipped scorched_basin.json already aligns with the LLM-generated structure and bundled pathfinding connectivity tests pass
  • Investigated and fixed the multiplayer lobby-start regression reported on 2026-03-28 where guests could join but then got stuck loading or hit Connection Lost, while the host saw the remote player as immediately defeated.

  • Root causes confirmed in live two-browser repros:

    • Join Game was only reachable from a fresh setup page after enabling public-host mode because the header action was incorrectly gated on lobbyStatus === 'hosting'
    • private code-join lobbies tore themselves down as soon as the last Open slot was filled because networking enablement only looked for remaining open slots or a public lobby flag
    • the /game/setup lobby hook closed active peer/signing state during the /game/setup -> /game navigation, disconnecting the guest right after Start Game
    • guest-side multiplayer store wiring used a synthetic host peer ID instead of the host's real signaling pubkey, which would break signed command verification once the match was running
  • Applied fixes:

    • added src/app/game/setup/lobbySessionPolicy.ts and switched useLobbySync to keep networking alive for connected guest slots
    • updated src/hooks/useMultiplayer.ts to preserve active lobby sessions and reconnect callbacks across the setup-to-game transition, defer real teardown to game exit, and map the guest's host peer to the real host pubkey
    • updated src/store/multiplayerStore.ts and src/app/game/page.tsx so real multiplayer cleanup runs on actual /game exit instead of on setup-page unmount
    • updated src/app/game/setup/page.tsx so Join Game and Browse Lobbies are available from a fresh setup page without requiring public-host mode
  • Added regressions:

    • tests/app/game/setup/lobbySessionPolicy.test.ts
    • tests/store/multiplayerStore.test.ts
  • Verified:

    • npm test -- tests/app/game/setup/lobbySessionPolicy.test.ts tests/app/game/setup/getStartGameButtonState.test.ts tests/app/game/gamePageLifecycle.test.ts tests/store/multiplayerStore.test.ts
    • npx eslint src/app/game/setup/lobbySessionPolicy.ts src/hooks/useLobbySync.ts src/hooks/useMultiplayer.ts src/store/multiplayerStore.ts src/app/game/page.tsx src/app/game/setup/page.tsx tests/app/game/setup/lobbySessionPolicy.test.ts tests/store/multiplayerStore.test.ts
    • npm run build
    • live headed Playwright two-browser verification in next dev:
      • private code join: output/playwright/multiplayer-verify-private-1774741115339/result.json
      • public-host code join: output/playwright/multiplayer-verify-public-1774741179155/result.json
    • live headed Playwright two-browser verification in production (next start):
      • private code join: output/playwright/multiplayer-verify-private-prod-1774741262397/result.json
  • npm run type-check is still blocked by pre-existing unrelated test-harness errors in tests/engine/input/handlers/buildingInputHandler.test.ts and tests/engine/workers/gameWorker.test.ts; the multiplayer changes themselves build and lint cleanly.

    • contested_frontier is the only ranked shipped map that still appears to be on the older non-LLM layout
  • Additional catalog note:

    • battle_arena is correctly hidden from the regular lobby via isSpecialMode: true
    • test_6p_flat is still bundled as a normal selectable map and is not generated by either map-regeneration script
  • Regenerated and swapped the bundled src/data/maps/json/contested_frontier.json to the current output from scripts/regenerate-maps-llm.ts.

  • Post-swap validation:

    • npx --yes tsx scripts/validate-maps.ts passed for the full bundled map set
    • npm test -- tests/engine/pathfinding/recastRampConnectivity.test.ts passed after the swap
  • Important nuance in the regenerated contested_frontier:

    • the new JSON adds 13 ramp entries compared with the previously shipped version (49 total vs 36)
    • the validator now reports all six spawn cells as terrain=ramp elev=220 rather than ground, though they remain walkable and the existing connectivity regression still passes
  • Follow-up worth doing in a live match:

    • spectate a Contested Frontier game after the swap to confirm AI movement around main-base exits and initial worker behavior still looks sane with spawn cells marked as ramp terrain
  • Investigated multiplayer command transmission and determinism under sustained live play on 2026-03-28 after the earlier lobby-start regression fix.

  • Additional root cause found during deeper multiplayer tracing:

    • the UI-thread proxy Game instance could still register inbound multiplayer handlers even when the worker-owned simulation was already handling them, which produced duplicate verification paths and false [Game] SECURITY: rejections for otherwise valid remote commands
  • Applied follow-up multiplayer determinism fixes:

    • added src/engine/core/multiplayerMessageHandling.ts plus a multiplayerMessageHandling ownership flag in src/engine/core/GameCore.ts
    • updated src/engine/core/Game.ts and src/components/game/hooks/useWorkerBridge.ts so multiplayer inbound message handling is owned by the worker in worker-bridge matches and only by the main thread in direct-mode matches
    • added a browser debug hook in src/components/game/hooks/useWorkerBridge.ts exposing globalThis.__voidstrikeMultiplayerDebug__ so live browser automation can request authoritative simulation checksums and read multiplayer sync state from the running worker
    • added scripts/verify-multiplayer-checksum.js to spin up a real 2-human + 2-AI match, issue commands from both humans, and verify remote action visibility plus checksum parity over time
    • added tests/engine/core/multiplayerMessageHandling.test.ts to lock the ownership split between main-thread and worker-managed multiplayer sessions
  • Sustained production verification passed on http://127.0.0.1:3308:

    • scenario: Scorched Basin, 2 humans + 2 AI, High resources, Fastest speed, fog disabled
    • host created a private lobby, guest joined by code, and both human players issued live commands including move/hold/stop plus repeated move orders during the five-minute run
    • command visibility checks showed the same commanded remote unit state on both clients for host-issued and guest-issued actions, confirming network transmission and remote render-state updates
    • authoritative checksum checkpoints matched on both clients throughout the run:
      • initial: tick 20, checksum 767486150
      • minute 1: tick 1285, checksum 282837654
      • minute 2: tick 2495, checksum 1162993385
      • minute 3: tick 3700, checksum 3528400692
      • minute 4: tick 4885, checksum 3938796473
      • final: tick 6085, checksum 2918746334
    • final multiplayer state on both clients remained connectionStatus: connected and desyncState: synced, with the host still bound to player1 and the guest still bound to player2
    • no Connection Lost, no Game Desynchronized, and no [Game] SECURITY: / CRITICAL log entries were emitted during the run
  • Verification artifacts:

    • result bundle: output/playwright/multiplayer-checksum-five-minute-1774744461322/
    • summary JSON: output/playwright/multiplayer-checksum-five-minute-1774744461322/result.json
    • logs: output/playwright/multiplayer-checksum-five-minute-1774744461322/host.log and output/playwright/multiplayer-checksum-five-minute-1774744461322/guest.log
    • captured end-state visuals/text: host-final.png, guest-final.png, host-final.txt, guest-final.txt
  • Investigated the follow-up report that the guest seemed to start a multiplayer match without a loading progress bar on 2026-03-28.

  • Findings from live two-browser production captures:

    • the in-canvas LoadingScreen already rendered correctly for the guest once WebGPUGameCanvas mounted
    • the real UX gap was earlier in the /game route handoff: both host and guest could briefly show the plain route-level black fallback before the game canvas chunk mounted, making the guest transition look like “no progress bar” if the black frame happened to last longer on that machine
  • Applied fix:

    • added src/app/game/GameLoadingFallback.tsx, a lightweight route-level loading shell with an immediate visible progress bar
    • updated src/app/game/page.tsx so both the pre-hydration state and the dynamic import fallback use GameLoadingFallback instead of the blank black screen whenever gameStarted is true
    • added tests/app/game/GameLoadingFallback.test.ts to lock the presence of the immediate loading shell and progress bar markup
    • updated docs/architecture/OVERVIEW.md to document that /game now shows a lightweight loading shell during the route-to-canvas handoff
  • Verified:

    • npm test -- tests/app/game/GameLoadingFallback.test.ts tests/app/game/setup/lobbySessionPolicy.test.ts tests/app/game/setup/getStartGameButtonState.test.ts tests/app/game/gamePageLifecycle.test.ts tests/store/multiplayerStore.test.ts tests/engine/core/multiplayerMessageHandling.test.ts
    • npx eslint src/app/game/page.tsx src/app/game/GameLoadingFallback.tsx src/app/game/setup/lobbySessionPolicy.ts src/hooks/useLobbySync.ts src/hooks/useMultiplayer.ts src/store/multiplayerStore.ts src/components/game/hooks/useWorkerBridge.ts src/engine/core/Game.ts src/engine/core/GameCore.ts src/engine/core/multiplayerMessageHandling.ts tests/app/game/GameLoadingFallback.test.ts tests/app/game/setup/lobbySessionPolicy.test.ts tests/store/multiplayerStore.test.ts tests/engine/core/multiplayerMessageHandling.test.ts
    • npm run build
    • fresh production (next start) two-browser loading-handoff capture on http://127.0.0.1:3309:
      • artifact bundle: output/playwright/host-guest-loading-compare-fixed-1774745792096/
      • both host-100ms.png and guest-100ms.png now show the new immediate loading shell instead of a blank screen
      • by 200ms, the regular in-canvas loading screen is already visible and progressing on both clients