From c2f24fc546df4193be56da48745d10f9147559de Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Mon, 21 Sep 2026 12:17:06 +1000 Subject: [PATCH] Add public faction-sheet inspectors and a twelve-faction gallery Add an enlarged public sheet beneath each player, available while actions are pending. Only faction identity and table rules mode enter the reader; it sends no action and receives no private player data. Basic/Advanced preview explicitly leaves the table mode unchanged. Native mode control, keyboard scrolling, Escape/focus restoration and internal links use existing dialog components. The internal gallery covers all twelve factions with bundled Homeworld artwork. Share every faction's Basic and Advanced paragraphs with the main reference. Replace Richese's generic placeholder, centralize Ecaz/Moritani guidance from settled contracts, correct CHOAM Auditor/Baliset/Jubba availability and align the Fremen/Ecaz Sietch Tabr exception. Full printed text, unresolved powers and combined-rule acceptance remain explicit. No new rule, AI strategy or gate. Four focused tests cover all12 identities, shared paragraphs, artwork files, internal links, mode separation, gallery defaults and per-viewer busy controls. The first busy-control assertion falsely matched CSS disabled variants; it now checks actual disabled attributes. All16 related focused cases pass. Independent review caught missing Moritani optionality and stale Fremen exception text; those were fixed and final review found no material issue. Auditor limits and full actual-card cancellation cost were checked against the existing contract. Types, lint, all 5000 offline tests and production build pass on stable source. No API/auth/persistence changes; HTTP tests were not repeated. Browser QA reused the existing two-player Basic auction without game actions. Both own and opposing inspectors work; Enter opens, Escape closes and returns focus, reopening resets Basic, and Advanced preview explicitly preserves the Basic table. ArrowDown scrolls the focused long sheet. At390px, dialog359x828, scroll client/width342 without horizontal overflow,16px text and44px controls. Gallery334px fits without overflow; Richese, Ecaz and Moritani selection and Advanced paragraphs render, faction change resets Basic and bundled art loads. Internal help and Advanced Moritani links expand their topics. Back and refresh preserve four hand cards and the pending minimum bid2; normal viewport restored. Screenshot capture timed out; full visual/screen-reader acceptance remains open. All 1539 opening games, 3259 saved seats and auxiliary rows remain identical, including the QA room. SQLite integrity and foreign keys pass. No new games, server restart, migration, reset or recurring automation. Private evidence: 20260921-faction-sheets. Verified source tree: 6b738958f30fc381d3e9eb26398239bbd88f1d4281f8ff5ff2bb3b3259494a93 --- README.md | 4 ++ app/globals.css | 12 +++++ components/faction-inspector.tsx | 80 ++++++++++++++++++++++++++++++++ components/game-table.tsx | 2 + components/rules-reference.tsx | 2 + docs/COMPONENT_INVENTORY.md | 7 +++ docs/CURRENT_STATUS.md | 30 ++++++------ docs/FACTION_SHEETS.md | 55 ++++++++++++++++++++++ docs/IMPLEMENTATION_STATUS.md | 11 +++++ game/faction-reference.ts | 56 ++++++++++++++++++++-- game/reference.ts | 46 ++++++++++-------- tests/faction-inspector.test.tsx | 74 +++++++++++++++++++++++++++++ 12 files changed, 341 insertions(+), 38 deletions(-) create mode 100644 components/faction-inspector.tsx create mode 100644 docs/FACTION_SHEETS.md create mode 100644 tests/faction-inspector.test.tsx diff --git a/README.md b/README.md index 0d079886..08e27801 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,10 @@ Use **Inspect forces** under any player to enlarge their counters and read publi reserves, Tanks and deployed groups, including Homeworld reserve breakdowns. See [force inspection](docs/FORCE_INSPECTION.md). +Use **Inspect faction** under any player for public powers and alliance guidance. +The sheet starts in the table’s rules mode and offers an Advanced preview without +changing the game. Browse all twelve in the [faction-sheet gallery](docs/FACTION_SHEETS.md). + Above the board, **Treachery draw pile** and **Spice draw pile** show live card counts separately from hands, auction cards and discards. See [draw-pile counts](docs/DRAW_PILES.md). diff --git a/app/globals.css b/app/globals.css index bc28abfe..8fecaac3 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2006,3 +2006,15 @@ footer { .force-reference { padding: 1rem; border: 1px solid #807352; border-radius: .75rem; font-size: 1rem; } .force-reference label { display: grid; gap: .5rem; } .force-reference select { min-height: 44px; width: 100%; font-size: 1rem; } +.faction-sheet { min-width: 0; margin-top: 1rem; overflow: hidden; border: 1px solid #807352; border-top-width: 4px; border-radius: .75rem; background: #20271f; color: #f2e8d3; } +.faction-sheet header, .faction-sheet section, .faction-sheet footer { padding: 1rem; } +.faction-sheet section + section, .faction-sheet footer { border-top: 1px solid #807352; } +.faction-sheet .faction-sheet-label { font-size: .875rem; color: #edcc8c; } +.faction-sheet p, .faction-sheet li { font-size: 1rem; line-height: 1.65; overflow-wrap: anywhere; } +.faction-sheet h3 { font: 1.8rem/1.3 Georgia, serif; } +.faction-sheet h4 { margin: 0 0 .75rem; font-size: 1.2rem; } +.faction-sheet ol { display: grid; gap: .9rem; margin: 0; padding-left: 1.4rem; } +.faction-sheet-art { display: block; width: 100%; max-height: 10rem; object-fit: cover; } +.faction-sheet footer a, .faction-sheet-help { display: block; padding-block: .65rem; font-size: 1rem; line-height: 1.5; text-decoration: underline; } +.faction-sheet-reader .faction-sheet-toggle { display: flex; align-items: center; gap: .75rem; min-height: 44px; margin-block: .75rem; font-size: 1rem; } +.faction-sheet-toggle input { width: 1.25rem; height: 1.25rem; flex-shrink: 0; } diff --git a/components/faction-inspector.tsx b/components/faction-inspector.tsx new file mode 100644 index 00000000..7242bdbd --- /dev/null +++ b/components/faction-inspector.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { useState } from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'; +import { Eye, X } from 'lucide-react'; +import { FACTIONS, faction, type FactionId } from '@/game/catalog'; +import { FACTION_RULES } from '@/game/faction-reference'; +import { HOMEWORLD_CARDS } from '@/game/homeworld-cards'; +import { Button } from './ui/button'; +import { Dialog, DialogClose, DialogDescription, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from './ui/dialog'; + +/** Static public reference only: no player, holdings, hidden choices or action callback. */ +export function FactionSheet({ factionId, advanced }: { factionId: FactionId; advanced: boolean }) { + const house = faction(factionId); + const rules = FACTION_RULES[factionId]; + const world = HOMEWORLD_CARDS.find(card => card.faction === factionId)!; + return
+
+

Faction sheet · {advanced ? 'Advanced' : 'Basic'}

+

{house.name}

+

{house.title}

+
+ +
+

Basic powers and alliances

+
    {rules.basic.map(text =>
  1. {text}
  2. )}
+
+ {advanced &&
+

Advanced powers

+

Use these with the Basic powers above and the Advanced game rules.

+
    {rules.advanced.map(text =>
  1. {text}
  2. )}
+
} + +
; +} + +function FactionSheetReader({ factionId, tableAdvanced }: { factionId: FactionId; tableAdvanced?: boolean }) { + const [advanced, setAdvanced] = useState(tableAdvanced ?? false); + return
+ {tableAdvanced !== undefined &&

This table uses {tableAdvanced ? 'Advanced' : 'Basic'} rules.{advanced !== tableAdvanced && ' You are previewing the other rules mode; the table has not changed.'}

} + + +
; +} + +export function FactionInspector({ factionId, advanced }: { factionId: FactionId; advanced: boolean }) { + const house = faction(factionId); + return + } aria-label={`Inspect faction: ${house.name}`}> + + + + +
Public faction sheet}>
+ {/* oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- The long faction sheet scrolls independently and needs keyboard access. */} +
+ {house.name} faction sheet + Read public faction powers and alliance guidance without changing the game. + + Faction sheet help +
+
+
+
; +} + +export function FactionSheetGallery() { + const [selected, setSelected] = useState('atreides'); + return
+ + +
; +} diff --git a/components/game-table.tsx b/components/game-table.tsx index 3f022723..69e4e5d4 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -3,6 +3,7 @@ import { EcazSetup } from './ecaz-setup'; import { StormCardInspector, StormCardLogInspector } from './storm-cards'; import { KwisatzInspector } from './kwisatz-inspector'; import { ForceInspector } from './force-inspector'; +import { FactionInspector } from './faction-inspector'; import { DrawPiles } from './draw-piles'; import { HandBrowser } from './hand-browser'; import { LobbyBotControls } from './lobby-bot-controls'; @@ -1111,6 +1112,7 @@ export function GameTable({ )} + {p.revealedTraitors.length > 0 && (

Revealed traitors:{' '} diff --git a/components/rules-reference.tsx b/components/rules-reference.tsx index cd5378ee..a32771f2 100644 --- a/components/rules-reference.tsx +++ b/components/rules-reference.tsx @@ -6,6 +6,7 @@ import { NexusCardGallery } from './nexus-cards'; import { StormCardGallery } from './storm-cards'; import { KwisatzReferenceCard } from './kwisatz-inspector'; import { ForceReferenceGallery } from './force-inspector'; +import { FactionSheetGallery } from './faction-inspector'; import { createStrongholdCards } from '@/game/stronghold-cards'; import { useSearchParams } from 'next/navigation'; import { Suspense, useState, useEffect, useRef } from 'react'; @@ -202,6 +203,7 @@ function RulesReferenceContent() { {topic.id === 'storm-cards' && } {topic.id === 'kwisatz-haderach' && } {topic.id === 'force-counters' && } + {topic.id === 'faction-sheets' && } {topic.id === 'richese-cards' && (