[bug] Mission 2 (Rooftop Relay) does not unlock after clearing Mission 1 #15

Closed
opened 2026-09-23 14:59:58 +00:00 by Mark · 1 comment
Owner

Bug found during play test.

Repro:

  1. Open the web prototype (cd web && python3 -m http.server, then load /).
  2. localStorage.clear() to start fresh.
  3. Deploy Mission 01 Sector 7 Underworks with default squad.
  4. Run the sim to conclusion (any outcome — win or loss).
  5. Click Save & Title.
  6. Reload the page.

Expected: Mission 02 Rooftop Relay unlocks (no longer shows LOCKED — clear the previous mission).

Actual: localStorage.ct_missions reads "1" (mission 1 marked complete), but the mission 02 button still has class mission locked and disabled attribute. Found via Playwright:

<button class="mission locked" data-i="1" disabled="">
  <b>02 // Rooftop Relay</b><br>
  <span class="dim">LOCKED — clear the previous mission</span>
</button>

Suspect: the unlock predicate reads ct_missions but the value is being compared as a string vs number, OR a separate missions_unlocked key is the source of truth and isn't being written on auto-end (only on explicit Save & Settle from the strategic view).

Acceptance:

  • After clearing mission 1 (via any conclusion), mission 02 becomes playable.
  • localStorage shows the unlocked set, not just a single counter.

Refs: Playwright play test 2026-09-23.

**Bug found during play test.** Repro: 1. Open the web prototype (`cd web && python3 -m http.server`, then load `/`). 2. `localStorage.clear()` to start fresh. 3. Deploy Mission 01 Sector 7 Underworks with default squad. 4. Run the sim to conclusion (any outcome — win or loss). 5. Click `Save & Title`. 6. Reload the page. **Expected:** Mission 02 Rooftop Relay unlocks (no longer shows `LOCKED — clear the previous mission`). **Actual:** `localStorage.ct_missions` reads `"1"` (mission 1 marked complete), but the mission 02 button still has class `mission locked` and `disabled` attribute. Found via Playwright: ``` <button class="mission locked" data-i="1" disabled=""> <b>02 // Rooftop Relay</b><br> <span class="dim">LOCKED — clear the previous mission</span> </button> ``` **Suspect:** the unlock predicate reads `ct_missions` but the value is being compared as a string vs number, OR a separate `missions_unlocked` key is the source of truth and isn't being written on auto-end (only on explicit `Save & Settle` from the strategic view). **Acceptance:** - After clearing mission 1 (via any conclusion), mission 02 becomes playable. - `localStorage` shows the unlocked set, not just a single counter. Refs: Playwright play test 2026-09-23.
Author
Owner

Fixed in 268d1c4 on main.

Root cause: ct_unlocked was being written inside the if (G.over.win) branch of the mission-end handler in web/js/ui.js, so a loss never advanced the unlock index even though ct_missions bumped at mission start (game.js). Reload after a loss → ct_unlocked stayed at 1 → mission 02 still rendered with class="mission locked" disabled.

Fix: hoist the ct_unlocked write outside the win branch so it fires on any mission conclusion. The best-time record stays win-only because a loss has no meaningful time to record.

Regression test added: tools/playwright_smoke.py section 1d — clears localStorage, deploys mission 0, forces endMission(false, ...), reloads, and asserts (a) the mission-01 button no longer has the locked class and (b) localStorage.ct_unlocked === '2'. Verified the test catches the regression by re-gating the unlock write (FAIL) then restoring the fix (PASS). All 9 make ci gates green on the push.

Fixed in 268d1c4 on main. Root cause: `ct_unlocked` was being written inside the `if (G.over.win)` branch of the mission-end handler in `web/js/ui.js`, so a loss never advanced the unlock index even though `ct_missions` bumped at mission start (game.js). Reload after a loss → `ct_unlocked` stayed at 1 → mission 02 still rendered with `class="mission locked" disabled`. Fix: hoist the `ct_unlocked` write outside the win branch so it fires on any mission conclusion. The best-time record stays win-only because a loss has no meaningful time to record. Regression test added: `tools/playwright_smoke.py` section 1d — clears localStorage, deploys mission 0, forces `endMission(false, ...)`, reloads, and asserts (a) the mission-01 button no longer has the `locked` class and (b) `localStorage.ct_unlocked === '2'`. Verified the test catches the regression by re-gating the unlock write (FAIL) then restoring the fix (PASS). All 9 `make ci` gates green on the push.
Mark closed this issue 2026-09-24 16:26:22 +00:00
Sign in to join this conversation.
No description provided.