Fow web UI / Frontend - #138
Draft
niugnip wants to merge 8 commits into
Draft
Conversation
The generic custom hyperlane tile ("hl") ships with blank artwork - its
connection lines are meant to be composited per-position from the
matrix the bot already computes (CustomHyperlaneService). The web map
never drew them, so custom hyperlanes rendered as empty tiles with no
indication of what they connect.
Ports the shape/rotation tables from the bot's HyperlaneTileGenerator
so the web view matches the Discord-rendered map exactly. Static
hyperlane tiles (hl_crossed_0, etc.) are unaffected - their lines are
pre-drawn into the artwork, so only "hl" gets the overlay.
Needs the matching hyperlaneMatrix field on the bot's tile payload.
Renders fogged/ghost tiles with last-seen labels, hides unidentified unit/control-token faction identity while keeping color, redacts player stats/objective progress/law and expedition completers for unidentified players, and adds a full-screen login/access-denied state for FoW games. Uses the existing Discord OAuth pathway only. Objective/score listings drop seat ordering for fogged viewers: with anonymous scorers on the card, a stable per-seat slot would let you locate a hidden player by position and then track them across objectives. Those viewers get a state-grouped list with the anonymous tokens last instead. The GM's own unfiltered view keeps normal seat order, since nothing is concealed from them. Also gates the game-state overlay, score breakdown and anchored player-stat tiles behind the HIDE_PLAYER_INFOS FoW option, again exempting only the GM's real view - X-Viewer-Is-Gm stays true while a GM previews a player, so it can't gate this on its own. Also switches exhausted-planet rendering to the per-tile `exhausted` field the backend now sends, instead of deriving it from each player's exhaustedPlanets list. The old approach only worked for players the viewer can identify, so in FoW an exhausted planet in a visible system belonging to an unidentified player rendered as ready. Exhaustion of a visible planet isn't secret - it says nothing about who controls it - so it now follows tile visibility like the units and tokens on that same planet already do. Requires the matching backend changes to the web data payload. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The event log has no fogged variant (backend /events is built from full, unfiltered state with no per-viewer redaction), so it's only ever valid for the GM's own real view - never for participants of a FoW game, and not while the GM is previewing as a specific player. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The panel/selection reset effect was keyed on gameData?.gameName, but gameData goes briefly undefined on every viewAsPlayerId change too (it's part of the player-data query key) - so clicking "view as player X" tripped the same reset meant only for switching games, closing the panel and dropping the selection right as that player's data started loading. Keyed on the route param instead, which only changes on actual navigation.
Testing the backend's neutral-player redaction fix surfaced two more bugs around the neutral (Dicecord) player, whose units/color/identity the backend correctly leaves unredacted (it's public knowledge, not a hidden opponent): - gameData.playerData deliberately excludes the neutral player (see filterPlayersWithAssignedFaction, used for ranking/tab UI), but MapUnitDetailsCard was using that same filtered list to resolve unit identity - so a neutral unit's owner could never be found and it always fell back to the "Unidentified" estimate path regardless of the backend fix. Added gameData.allPlayerData (unfiltered) for identity/color lookups that need to resolve every entity on the board, and pointed the tooltip at it; it now reads "Neutral" with correct stats and color instead of "Unidentified". - FowViewPanel's "Show game as" picker reads the raw query cache directly (to always list every player even mid-preview) rather than gameData.playerData, so it never filtered the neutral entry out - once the backend started including it, "Neutral" started appearing there as a selectable preview target. Filtered it with the same helper used everywhere else.
Hooks called after an early return (or short-circuited by ??) change the hook count between renders, so a mounted component re-rendering with a flipped branch throws "Rendered fewer/more hooks than expected". These were latent for a long time and only became reachable with the new FoW GM view-switching. Previously these components were mounted fresh per navigation, so the branch that skipped a hook was fixed for the component's whole lifetime. Switching "view as player" as GM re-renders the same mounted components against a different player's data - now the branch flips underneath them and the hook count changes. Hit it twice in a row while testing (MapUnitDetailsCard, then FactionControlBorderOverlay) before going looking for the rest. Found the remaining ones by temporarily enabling eslint-plugin-react-hooks, which is already a dependency but isn't wired into eslint.config.js - left that config change out of this commit, since turning on a repo-wide lint rule is a maintainer call rather than something this branch should decide: - MapPlanetDetailsCard: usePlanet() after an early return - MahactEdictTokens: useFactionColors() after an early return - MapTooltipPositioner: useAppStore() short-circuited by ?? - PlayerStatsArea: three useMemo calls after an early return - PlanetCirclesLayer: a useMemo after an early return - DebtTokens: useFactionColors() after an early return - Nombox: useFactionColors() after an early return - ScaledContent: useRef/useState/useIsoLayoutEffect/useMemo all gated behind the `enabled` prop - PathVisualization: useAppStore short-circuited by ??, plus two useMemo/useCallback pairs after an early return Each fix moves the hook calls above any conditional return, using optional chaining/fallback values where the hook's input depended on data that might not exist yet, so the hook still runs unconditionally every render.
Follows the backend no longer sending hidden players' factions: - Objective cards render unidentifiedScorerCount anonymous tokens instead of filtering a list of real factions client-side. With no identifiers arriving there is nothing to correlate across cards, so the per-objective shuffle that existed to frustrate that is gone. - ScoreTracker places hidden players from hiddenPlayerVps - bare score-track totals with no faction attached - rather than reading faction-keyed score breakdowns that no longer include them. - The General tab's strategy-card list shows played cards instead of unpicked ones under fog. Unpicked cards accumulate trade goods, which the backend now blanks alongside pickedByFaction (see the bot-side commit) - but the tab still needs to stop asking for the unpicked list at all, since which cards remain untaken is exactly what that redaction hides. - Factions in Game and Card Pool are hidden outright in the General tab under fog, rather than picked apart field by field. Factions in Game computes a "next player" badge as the slot after the active player within playerData, which already excludes hidden players - if the real next player is hidden, the badge lands on the wrong identified player, and that mismatch tells a fogged viewer no hidden player's SC falls between the active player and this one. Card Pool's deck sizes have their own redaction (HIDE_EXPLORES nulls cardPool entirely) that this tab wasn't accounting for either. Simpler to drop both sections than track every way game-state-shaped data can leak turn order. Also stops fogged views from leaking seat order: playerData arrives in seat order, which is itself hidden information, so the objective lists, the score track, and the player score-summary grid now all sort identified players by color for a fogged viewer instead - already visible to them, and revealing nothing extra. Normal games and the GM's own view are unaffected by any of this - they keep seat order and the full General tab exactly as before, since nothing is redacted for them in the first place.
The compact objectives view threaded unidentifiedScorerCount into its chip but not into the ObjectiveDetailsCard popover behind it, which defaults to 0 - so under fog the popover silently showed no anonymous scorers while the chip it expands showed the tokens. Also rewords a comment that still described the per-objective shuffle removed with the count-based redaction, and fixes the indentation of the chip's prop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Renders Fog of War games in the new web UI. Previously
MapTogglePageforce-redirected every FoW game to the old image-based view; that block is removed.Requires AsyncTI4/TI4_map_generator_bot#5438— merge that first, or this reads payload fields that don't exist yet.
What it does
fow:<color>sentinel.unidentifiedScorerCountand render as that many anonymous tokens; hidden players' score-track totals arrive faction-less viahiddenPlayerVps. No hidden identity ever reaches the client.Ordering under fog
Objective and score listings normally show players in seat order — but seat order is itself hidden information under fog. A fogged viewer gets identified players sorted by color instead (already visible on every token, so it reveals nothing), grouped by state (own, then identified scorers, then non-scorers), with anonymous tokens appended last so no slot is tied to a seat that could be tracked across cards. The GM's unfiltered view keeps normal seat order, since nothing is concealed from them.
An earlier revision shuffled anonymous scorers per-objective to frustrate cross-card correlation; with the backend now sending bare counts instead of factions there is nothing left to correlate, and the shuffle is gone.
The General tab's Factions in Game and Card Pool sections are dropped outright for a fogged viewer — both encode turn-order/game-state information in ways that are simpler to remove than to redact field by field — and its strategy-card list shows played cards rather than unpicked ones, since which cards remain untaken is exactly what the backend's strategy-card redaction hides.
Commits
Reviewable in order; each type-checks standalone.
hltile ships with blank artwork and its lines are composited from per-position matrix data, which the web map never drew. Shape/rotation tables ported from the bot'sHyperlaneTileGeneratorso it matches the Discord render.gameData?.gameName, which goes briefly undefined on everyviewAsPlayerIdchange.gameData.playerDatadeliberately excludes the neutral (Dicecord) player, but unit tooltips used that same filtered list for identity, so neutral units always fell back to "Unidentified". AddsallPlayerDatafor identity lookups that must resolve every entity on the board.The hooks commit
Hooks called after an early return, or short-circuited by
??, change the hook count between renders, so a mounted component re-rendering with a flipped branch throws "Rendered fewer/more hooks than expected".These were latent for a long time and only became reachable with GM view-switching: previously these components were mounted fresh per navigation, so the branch that skipped a hook was fixed for the component's lifetime. Switching "view as player" re-renders the same mounted components against different data, and the branch flips underneath them.
Found the rest by temporarily enabling
eslint-plugin-react-hooks, which is already a dependency but isn't wired intoeslint.config.js. That config change is deliberately not included — turning on a repo-wide lint rule is a maintainer call, and it currently reports pre-existing violations elsewhere in the tree. Happy to send it separately if wanted.Testing
yarn buildclean. Exercised against a local FoW save as GM, as a participant, and through GM preview-as-player, including with a player forced unidentifiable to confirm anonymous tokens render from the bare counts.Known cosmetic uncertainty: the anonymous-player token styling (dark fading centre + crossed-out eye) is new rather than matched to an existing placeholder — the existing ones all use a light-to-dark sphere gradient. Easy to restyle if it doesn't fit.
🤖 Generated with Claude Code