Phase 29 — Map Toggles #20

Merged
developer merged 8 commits from feature/ui-map-toggles into development 2026-05-19 22:37:30 +00:00
Owner

Summary

Phase 29 of ui/PLAN.md — the gear-icon popover that controls per-game map
visibility plus the wrap-mode toggle.

  • State: MapToggles rune in GameStateStore with persistence in the new
    game-map-toggles cache namespace; a new server-side turn force-resets every
    flag to defaults.
  • Renderer: setHiddenPrimitiveIds (hide-by-id without remount,
    hit-test-aware) + setVisibilityFog (layered overpaint on a viewport-level
    fog layer that sits below every primitive copy).
  • State binding: reportToWorld now also returns categories and
    planetDependents so the map view can cascade planet hiding onto the
    markers / in-space / incoming groups anchored on it; cargo routes and
    pending-Send overlay filter the same way.
  • UI: map-toggles.svelte popover (≥ 44 px tap target, popover on desktop
    / bottom-sheet on mobile) with three fieldsets — Objects, Planets, View.
  • Tests: 8 new test files covering the new helpers, the renderer's
    hide-by-id contract, the popover lifecycle, persistence + reset, the
    fog-paint-ops contract, the categories + cascade map, plus a Playwright e2e
    across the four projects.
  • Docs: ui/docs/renderer.md, ui/docs/storage.md, ui/docs/game-state.md,
    docs/FUNCTIONAL.md (§6.7) + RU mirror, ui/PLAN.md decisions block.
  • Drive-by: pkg/calc/race.go typo Fligth → Flight and the one Go
    caller.

Test plan

  • cd ui/frontend && pnpm test (707 / 707 green)
  • pnpm exec playwright test on chromium-desktop, webkit-desktop,
    chromium-mobile-iphone-13, chromium-mobile-pixel-5 (all green on
    gitea ui-test run #220)
  • Manual smoke on dev-deploy:
    - Zero-turn map: visibility fog ON shows planets inside the unfogged
    circles, fog OFF shows the regular dark canvas.
    - Legacy report with maxed-out drive tech: visibility fog covers the
    whole map (correct — visibility radius exceeds map size), planets
    stay visible on top.
    - Torus seam: visibility circles wrap continuously across the world
    edge, no "sector" artifact.
    - Wrap-mode flip: camera centre preserved, no Pixi remount.
## Summary Phase 29 of `ui/PLAN.md` — the gear-icon popover that controls per-game map visibility plus the wrap-mode toggle. - **State**: `MapToggles` rune in `GameStateStore` with persistence in the new `game-map-toggles` cache namespace; a new server-side turn force-resets every flag to defaults. - **Renderer**: `setHiddenPrimitiveIds` (hide-by-id without remount, hit-test-aware) + `setVisibilityFog` (layered overpaint on a viewport-level fog layer that sits below every primitive copy). - **State binding**: `reportToWorld` now also returns `categories` and `planetDependents` so the map view can cascade planet hiding onto the markers / in-space / incoming groups anchored on it; cargo routes and pending-Send overlay filter the same way. - **UI**: `map-toggles.svelte` popover (≥ 44 px tap target, popover on desktop / bottom-sheet on mobile) with three fieldsets — Objects, Planets, View. - **Tests**: 8 new test files covering the new helpers, the renderer's hide-by-id contract, the popover lifecycle, persistence + reset, the fog-paint-ops contract, the categories + cascade map, plus a Playwright e2e across the four projects. - **Docs**: `ui/docs/renderer.md`, `ui/docs/storage.md`, `ui/docs/game-state.md`, `docs/FUNCTIONAL.md` (§6.7) + RU mirror, `ui/PLAN.md` decisions block. - **Drive-by**: `pkg/calc/race.go` typo `Fligth → Flight` and the one Go caller. ## Test plan - [x] `cd ui/frontend && pnpm test` (707 / 707 green) - [x] `pnpm exec playwright test` on `chromium-desktop`, `webkit-desktop`, `chromium-mobile-iphone-13`, `chromium-mobile-pixel-5` (all green on gitea ui-test run #220) - [x] Manual smoke on `dev-deploy`: - Zero-turn map: visibility fog ON shows planets inside the unfogged circles, fog OFF shows the regular dark canvas. - Legacy report with maxed-out drive tech: visibility fog covers the whole map (correct — visibility radius exceeds map size), planets stay visible on top. - Torus seam: visibility circles wrap continuously across the world edge, no "sector" artifact. - Wrap-mode flip: camera centre preserved, no Pixi remount.
developer added 8 commits 2026-05-19 22:37:19 +00:00
feat(ui): Phase 29 map visibility toggles
Tests · Go / test (push) Successful in 2m31s
Tests · UI / test (push) Failing after 8m7s
2bd1b54936
Adds the gear-icon popover on the map view with per-game persistence
of every category toggle plus the wrap-mode radio. Hide-by-id and
visibility-fog facilities land on the renderer so every flip applies
within one frame without a Pixi remount; the wrap-mode toggle keeps
its existing remount + camera-preserve path. A new server-side turn
force-resets every flag to defaults so a hidden category never makes
the player miss the next turn's news.

Also fixes the FligthDistance → FlightDistance typo in pkg/calc/race.go
(plus the single Go caller); the TS side keeps duplicating the formula
until a race-level WASM bridge lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three independent bugs in `tests/e2e/map-toggles.spec.ts` made the
fresh-Phase-29 suite red on CI #216:

1. `visiblePlanets` filtered on `p.id < 1_000_000`, which JS interprets
   in signed space — high-bit-prefix primitives (cargo route 0x80…,
   battle 0xa0…, bombing 0xc0…) are stored as negative Numbers and
   leaked into the planet list. Filter switched to a `0 < id < 1e7`
   window that matches the engine planet-number range exactly.
2. The `visibleHighBitCount` helper now ToUint32-converts the id
   before masking so the bitmask comparison works regardless of
   whether the id is stored as positive or negative.
3. The fog and wrap-mode tests read the renderer state synchronously
   after the click — the Svelte effect re-runs asynchronously, so the
   tests saw stale state. Both now `waitForFunction` on the canonical
   "settled" signal: empty fog circles for the fog flip, and a new
   `getMapMode()` debug accessor for the wrap-mode remount.

Renderer side: registers a `MapModeProvider` next to the existing
camera / fog providers and exposes `getMapMode()` through the debug
surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(ui-e2e): tighten Phase 29 effect tracking + radio wiring
Tests · UI / test (push) Failing after 7m19s
7c46aa4bec
Run #217 surfaced three independent bugs that survived the first
fixup pass:

1. `visibleHighBitCount` masked the id with `(prim.id >>> 0) & 0xf…`,
   but JS bitwise AND always returns a signed int32 — the mask had
   to be re-converted with `>>> 0` AFTER the AND, not before. Result
   was always 0 on the previous run, masking the next two bugs by
   making the persistence test's high-bit-count assertions a
   tautology.
2. `applyVisibilityState` was wrapped in `untrack`, so the
   `toggles.X` reads inside `computeHiddenIds` / `computeFogCircles`
   never landed in the effect's dependency set — toggling fog or any
   marker / group / kind flag did not re-run the effect, so the
   renderer never received the new hide / fog input. Explicit
   `void toggles.X` reads now live at the top of the effect so every
   key is tracked synchronously.
3. The wrap-mode radios fired on `onchange`, which Svelte 5
   suppresses on a re-activation of an already-checked input — the
   Playwright `.click()` flake on the second wrap test reflected the
   missed event. Switched to `onclick` and short-circuited when the
   target mode is already active.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous logic re-mounted the renderer whenever
`store.wrapMode` flipped, because the `sameSnapshot` gate
included `handle.getMode() === mode`. Pixi 8 does not reliably
re-initialise an `Application` on the same canvas — the symptom
showed up as the chromium tab silently closing during the
Phase 29 wrap-mode e2e ("Target page, context or browser has
been closed").

The renderer already exposes an in-place `setMode` that swaps
the wrap-clamp / torus-copy visibility synchronously while
preserving the camera; the playground-map.spec.ts wrap toggle
has been driving it for several phases without issue. Drop
mode from the snapshot gate and route the change through
`handle.setMode(mode)` instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase 29 fog overlay rendered as a handful of random arc
segments instead of a clean union of holes around LOCAL planets
— Pixi v8's `Graphics.cut()` does not reliably subtract multiple
overlapping circles from a base path.

Replaced the cut-based approach with a layered overpaint: a
fog-tinted rectangle fills the world, then opaque background-
coloured circles are painted on top for every visibility circle.
The natural rendering order unions overlapping circles for free —
no geometry, no `cut()` quirks, one extra fill per circle.

Renamed the toggle from `visibilityFog` to `visibleHyperspace`
across the store, i18n strings, popover, tests, and docs. The
overlay still implements the visual "fog" effect at the renderer
level (FOG_COLOR, setVisibilityFog, getMapFog); the toggle is
named after the player-facing concept it controls — the portion
of the map that is visible (intelligence/scan coverage) — rather
than the obscured part.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lifted the Phase 29 fog draw sequence out of `setVisibilityFog`
into a pure `fogPaintOps` helper that returns an ordered list of
fill operations (one fog rect, then one background-coloured
circle per visibility entry). The renderer now dispatches each op
straight onto a Pixi `Graphics`; the indirection lets the layered-
overpaint contract be tested without booting Pixi.

`tests/fog-paint-ops.test.ts` covers: empty input → no ops; single
circle → fog rect + bg circle in that order; multiple circles → N
bg circles after the fog rect; overlapping circles emitted
independently (the rendering order unions them); zero / negative
world dimensions → no ops.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two visible regressions in the in-game map's fog overlay surfaced
on dev-deploy:

1. With three LOCAL planets close together, only the last planet
   glyph stayed visible inside the bg holes — the other two were
   obscured. The previous implementation stacked the fog rectangle
   plus every bg circle onto a single `Graphics` via repeated
   `g.rect(...).fill(...).circle(...).fill(...)...`. Pixi v8's
   multi-shape Graphics is supported in theory, but in practice
   only the last shape's fill seems to land, dropping the earlier
   bg holes (and the planet glyphs on top look like they vanished
   along with their hole). Splitting each op onto its own
   `Graphics` inside a per-copy `Container` removes the ambiguity
   — one shape, one fill, one render pass.

2. A planet near the right world edge produced a "sector" — the
   bg circle painted into the area past the seam, but the
   neighbouring tile's fog rectangle then overpainted that bleed,
   leaving a quarter-circle hole. In torus mode each visibility
   circle is now drawn at the nine wrapped positions
   (`(dx, dy) ∈ {-1, 0, 1}²`); the wrapped copies in the
   neighbour-tile-aligned positions keep the hole continuous
   across the seam. No-wrap mode keeps a single emission per
   circle, because wrapped circles would leak into the visible
   world rectangle as unwanted holes.

The `fogPaintOps` helper now takes the wrap mode as a parameter;
`tests/fog-paint-ops.test.ts` covers the torus expansion
(nine-wrap product per circle, the seam-fix case at x = 950) and
re-asserts the no-wrap path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(ui-map): move fog overlay to a viewport-level layer below the copies
Tests · UI / test (push) Successful in 2m50s
Tests · UI / test (pull_request) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m47s
Tests · Go / test (pull_request) Successful in 2m5s
53b892ae00
Two regressions surfaced once visible-hyperspace toggled on a real
dev-deploy map:

1. On the zero-turn map the bg holes painted ON TOP of the planet
   glyphs — every LOCAL planet looked like a hollow circle of
   background colour instead of the planet pixel inside an
   unfogged area.
2. On a legacy report with a drive tech that pushes the visibility
   radius well past the world dimensions the bg circles overlapped
   to cover the entire viewport. Combined with the wrong z-order
   the result was a uniformly black canvas with every primitive
   hidden.

The per-copy implementation added the fog container via
`copy.addChildAt(container, 0)` and trusted Pixi v8 to insert the
container at the start of the copy's children. Whether by a Pixi
quirk or by some interaction with how `populatePrimitives` orders
its `c.addChild(g)` calls, the fog ended up rendering after every
primitive in practice — the symptoms above are a perfect match for
that ordering.

Restructured the fog rendering so the z-order is structural
rather than relying on `addChildAt`:

- A single `fogLayer: Container` is added to the viewport BEFORE
  the nine torus copies. Pixi renders viewport children in order,
  so the layer is guaranteed to paint first; every copy renders
  on top.
- `fogPaintOps` now emits world-space coordinates with wrap
  offsets baked in (9 fog rects + 9 bg circles per visibility
  entry in torus mode, 1 + N in no-wrap mode). The renderer
  populates `fogLayer` with one `Graphics` per op — no per-copy
  iteration on the fog side.
- The previous `fogGraphics: Container[]` closure state is gone.
  Each `setVisibilityFog` flip drops every child of `fogLayer`
  and rebuilds it. The dispose path drops the children
  eagerly before `app.destroy({children: true})` walks the tree.

The fog-paint-ops test exercises the new contract: the no-wrap
path keeps one rect + N circles, the torus path expands to nine
rects + nine wrapped circles per entry (including the seam-fix
case at x = 950).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
developer merged commit 0da2f4b6fb into development 2026-05-19 22:37:30 +00:00
developer deleted branch feature/ui-map-toggles 2026-05-19 22:37:30 +00:00
Sign in to join this conversation.