Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
74595dd
chore: refresh metagame feeds (#1048)
matthewevans May 25, 2026
2289643
release: v0.1.36
May 25, 2026
38442b8
Fix draft pod resume and match handoff
matthewevans May 25, 2026
6228c52
Fix draft match concessions and bot identities
matthewevans May 25, 2026
27eea7c
i18n: add Polish locale, key-parity/UTF-8 gate, and fill missed chrom…
matthewevans May 25, 2026
9192310
feat(preview): flip Kamigawa cards 180° + proliferate side-select
matthewevans May 25, 2026
cddf2b3
Fix draft tournament controls and AI presentation
matthewevans May 25, 2026
0cefbad
i18n: translate pacing category labels/descriptions + fr/it Gameplay …
matthewevans May 25, 2026
2edb294
fix(draft): translate SetSelector load error at render, not in effect
matthewevans May 25, 2026
89b0af2
Add Silent Arbiter coverage support
Whovencroft May 25, 2026
7e3c710
Add all-colors static parser support
acocalypso May 25, 2026
1342970
Add Fleet Swallower coverage support
Whovencroft May 25, 2026
1e14782
Fix batched attack trigger context counts
mike-theDude May 25, 2026
986618a
Add noncreature damage-source parser support
Whovencroft May 25, 2026
ced45dd
Support opponent-controlled damage source triggers
Whovencroft May 25, 2026
e294b3b
Fix relative attack target controller parsing
mike-theDude May 25, 2026
5860d7b
feat(card-bot): Discord /card engine-parse lookup bot
matthewevans May 25, 2026
3140c0f
feat(deck-builder): local engine-backed card search, commander hover …
matthewevans May 25, 2026
e0e09b4
Add Inversion Behemoth parser support
servandorodrigues May 25, 2026
72d8bd8
Skip target slots for mass-broadcast effects
mike-theDude May 25, 2026
51e2743
Support evolved trigger events
Whovencroft May 25, 2026
c9ddd23
fix: filter modal modes with no legal targets before presenting choice
May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .claude/skills/add-engine-effect/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ Only needed if the effect pauses for player input.
- `client/src/components/modal/NamedChoiceModal.tsx` for `NamedChoice`
- `client/src/components/modal/ModeChoiceModal.tsx` for `ModeChoice` and `AbilityModeChoice`

Route any new frontend-authored text (titles, prompts, buttons) through `t()` (`useTranslation("game")`, keys in `client/src/i18n/locales/en/game.json`). Card/Oracle/enum text stays raw. See `$add-frontend-component` Phase 2.5 and `client/src/i18n/README.md`.

### Phase 5b — Multiplayer State Filtering (if applicable)

**Goal:** Hidden information is correctly filtered in multiplayer games.
Expand All @@ -250,7 +252,7 @@ Only needed if the effect reveals or hides information (hands, face-down cards,
Add the TypeScript variant.

- [ ] **`client/src/components/log/` — Game log rendering**
Handle the new event in the log component so players see what happened.
Handle the new event in the log component so players see what happened. Translate the log **template** (`t("log.yourEvent", { ... })`) and interpolate engine data (object IDs, amounts, enum strings) raw.

### Phase 7 — AI (if applicable)

Expand Down
32 changes: 30 additions & 2 deletions .claude/skills/add-frontend-component/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ description: Use when adding or modifying frontend UI components — interactive
> 1. **The frontend is a display layer, not a logic layer.** It renders engine-provided state and dispatches user actions — nothing more. It must never compute, derive, filter, or re-interpret game data. If a component needs a value the engine doesn't expose, the fix is to add it to the engine's output — not to calculate it client-side. Any "smart" frontend code is a bug.
> 2. **CR-correctness is non-negotiable.** The frontend must never contradict the Comprehensive Rules. If it displays information (legal targets, valid choices, game state), that information must come directly from the engine, which is the CR-validated source of truth. Never approximate engine logic in TypeScript.
> 3. **Build reusable component patterns.** New overlays and modals should follow existing patterns (CardChoiceModal, ModeChoiceModal). Extract shared behavior into composable components rather than duplicating across one-off implementations.
> 4. **All frontend-authored text is internationalized.** Every user-facing string the frontend authors (titles, labels, buttons, tooltips, placeholders, log templates, status messages) MUST go through `t()` via `react-i18next` — never a hardcoded literal in JSX. The boundary rule: *a string gets `t()` if and only if the frontend authored it.* Engine/card-database pass-through (card names, Oracle text, interpolated enum strings like phase/mana/counter type) stays **raw** — it is localized by a separate MTGJSON content pipeline, not `t()`. See `client/src/i18n/README.md` (the authority) before adding any string.

The React/TypeScript frontend communicates with the Rust engine through a transport-agnostic adapter layer. Game state flows from engine → adapter → Zustand stores → React components. Player actions flow in reverse via `dispatch()`. This skill covers wiring new UI components into this pipeline.

Expand Down Expand Up @@ -151,11 +152,14 @@ Used by: Scry, Dig, Surveil, Reveal, Search, DiscardToHandSize.

```tsx
// client/src/components/modal/YourOverlay.tsx
import { useTranslation } from "react-i18next";

import { useGameStore } from "../../stores/gameStore";
import { useUiStore } from "../../stores/uiStore";
import { useGameDispatch } from "../../hooks/useGameDispatch";

export function YourOverlay({ data }: { data: YourChoiceData }) {
const { t } = useTranslation("game"); // namespace = source directory, not subject
const objects = useGameStore((s) => s.gameState?.objects ?? {});
const inspectObject = useUiStore((s) => s.inspectObject);
const dispatch = useGameDispatch();
Expand All @@ -172,7 +176,9 @@ export function YourOverlay({ data }: { data: YourChoiceData }) {
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
>
{/* Card display + selection UI */}
<button onClick={handleConfirm} disabled={!isValid}>Confirm</button>
<button onClick={handleConfirm} disabled={!isValid}>
{t("yourOverlay.confirm")}
</button>
</motion.div>
</AnimatePresence>
);
Expand Down Expand Up @@ -209,6 +215,23 @@ For non-overlay components (new zone display, counter indicators, status badges)
- Subscribe to `useGameStore` for data
- No dispatch needed if read-only

### Phase 2.5 — Internationalize User-Facing Text

Every string the component authors must be a translation key, not a literal. Do this **as you write the component**, not as a cleanup pass.

- [ ] **Pick the namespace by source directory.** A component's namespace is where it lives, not what it's about: `components/draft/*` → `"draft"`, `components/lobby/*` → `"multiplayer"`, in-game overlays (`components/modal/`, `board/`, `combat/`, etc.) → `"game"`. `common` is the implicit default ns (shared buttons like Cancel/Confirm may already live there — reuse before adding).
```tsx
const { t } = useTranslation("game"); // opt into a ns; common is always available
```
- [ ] **Add the key to `client/src/i18n/locales/en/<ns>.json` first.** English is the typing oracle — referencing a key that doesn't exist in `en/` fails type-check. Other locales fall back to English automatically; you do **not** edit `es/fr/de/it/pt` (machine-translated separately).
- Key shape: nested dot paths, `camelCase` leaves, `<componentOrFeature>.<element>` (e.g. `"yourOverlay.confirm"`, `"yourOverlay.title"`).
- [ ] **Plurals via CLDR, not string math.** Use `key_one` / `key_other` + `t(key, { count })`. Never `count === 1 ? "item" : "items"`.
- [ ] **Interpolate engine data raw into a translated template.** The template is chrome (translate it); the values are engine data (leave raw): `t("yourOverlay.summary", { counterType, count })`.
- [ ] **Leave engine/card pass-through untouched.** Card names, Oracle/reminder text, and enum strings (phase, mana type, counter type) are **not** wrapped — they are localized by the content pipeline (`hooks/useEngineCardData.ts`), not `t()`. When in doubt, apply the boundary rule: did the frontend author this sentence, or is it engine data flowing through?
- [ ] **Never call `i18n.changeLanguage` directly.** The preferences store owns the active language: `usePreferencesStore.getState().setLanguage(lng)`.

See `client/src/i18n/README.md` for the full convention set.

### Phase 3 — GamePage Routing

- [ ] **`client/src/pages/GamePage.tsx` — conditional render**
Expand All @@ -234,7 +257,7 @@ If your overlay is a card choice type, integrate into the existing `CardChoiceMo
### Phase 5 — Game Log (if applicable)

- [ ] **`client/src/viewmodel/logFormatting.ts`** — Event formatting
Add a case for your `GameEvent` type to produce a human-readable log string.
Add a case for your `GameEvent` type to produce a human-readable log string. **Translate the template, interpolate engine data raw** — `t("log.yourEvent", { objectId, amount })` where the sentence is chrome and the IDs/amounts are engine data. The log type label is frontend-authored (translate it); the card/object it refers to is not.

- [ ] **`client/src/components/log/LogEntry.tsx`** — Custom rendering (if needed)
Most events use the default text format. Only add custom rendering for events that need icons, card references, or special formatting.
Expand Down Expand Up @@ -288,6 +311,9 @@ If your overlay is a card choice type, integrate into the existing `CardChoiceMo
| Adding client-side visibility logic | Diverges from server-filtered state, multiplayer security hole | Trust the filtered state from the adapter |
| Modifying `gameStore` directly | Bypasses animation pipeline and persistence | Always go through `dispatch()` |
| Not using `AnimatePresence` | Overlay appears/disappears instantly | Wrap in `AnimatePresence` with enter/exit transitions |
| Hardcoded user-facing string in JSX | Untranslatable; breaks 6-locale support | Route frontend-authored text through `t()` (Phase 2.5) |
| Wrapping card/Oracle/enum text in `t()` | Double-localizes engine data; key never resolves | Leave engine pass-through raw; only `t()` frontend-authored text |
| Hand-rolled `count === 1 ? "x" : "xs"` pluralization | Wrong for non-English CLDR rules | `key_one`/`key_other` + `t(key, { count })` |

---

Expand All @@ -310,6 +336,8 @@ test -f client/src/pages/GamePage.tsx && \
test -f client/src/game/dispatch.ts && \
test -f client/src/animation/eventNormalizer.ts && \
test -f client/src/components/modal/CardChoiceModal.tsx && \
test -f client/src/i18n/README.md && \
test -d client/src/i18n/locales/en && \
rg -q "type WaitingFor" client/src/adapter/types.ts && \
rg -q "type GameAction" client/src/adapter/types.ts && \
rg -q "type GameEvent" client/src/adapter/types.ts && \
Expand Down
4 changes: 3 additions & 1 deletion .claude/skills/add-interactive-effect/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ When the continuation is created, parent targets propagate down if the sub-abili
- `CardChoiceModal` → `SearchModal` — filtered card selection from list
- `NamedChoiceModal` — named choices including `CardName`, `NumberRange`, `Labeled`, and `LandType`

- [ ] **Internationalize all frontend-authored text** (titles, prompts, buttons, labels) via `t()` — `const { t } = useTranslation("game")`, with keys added to `client/src/i18n/locales/en/game.json` first. Card names and Oracle text stay raw (content pipeline). Boundary rule and conventions: `client/src/i18n/README.md`. See `$add-frontend-component` Phase 2.5 for the full checklist.

### Phase 7 — Multiplayer State Filtering (if hidden info)

- [ ] **`crates/server-core/src/filter.rs` — `filter_state_for_player()`**
Expand Down Expand Up @@ -247,7 +249,7 @@ The `NamedChoice` system is a well-contained interactive pattern with low blast
| 3 | `crates/engine/src/game/engine.rs` — `ChooseOption` handler | May need custom validation (e.g., NumberRange validates parsed u8 in range) |
| 4 | `client/src/adapter/types.ts` | Already generic (`choice_type: string`, `options: string[]`) — **no change needed** |
| 5 | `client/src/components/modal/NamedChoiceModal.tsx` | Add rendering branch only if the existing button grid / card-name search is insufficient |
| 6 | `client/src/components/modal/NamedChoiceModal.tsx` — `CHOICE_TYPE_LABELS` | Add user-facing label for the new type |
| 6 | `client/src/components/modal/NamedChoiceModal.tsx` — `CHOICE_TYPE_TITLE_KEYS` | Map the new ChoiceType key → an i18n leaf (e.g. `Keyword: "keyword"`). The title renders via `t(\`namedChoice.title.${leaf}\`)`, so also add `namedChoice.title.<leaf>` to `client/src/i18n/locales/en/game.json`. Never hardcode the label. |
| 7 | `crates/engine/src/ai_support/candidates.rs` — `NamedChoice` arm | Already generates one action per option — works for any choice type with populated options |
| 8 | `crates/engine/src/parser/oracle_effect/` | Add parser patterns for the new choice text |
| 9 | `crates/engine/src/game/effects/choose.rs` — tests | Add test for `compute_options()` with new variant |
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/review-engine-plan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Review the plan as an architectural gate. Reject the plan if any required dimens
- Types belong in `types/`.
- Game logic must not leak into frontend or WASM bridge.
- Display formatting must not leak into the engine.
- **i18n boundary:** if the plan adds frontend UI or log text, it must route frontend-authored strings through `t()` (react-i18next, keys in `client/src/i18n/locales/en/<ns>.json`) and leave engine/card pass-through raw. Reject plans that hardcode user-facing chrome strings or that wrap card/Oracle/enum text in `t()`. See `client/src/i18n/README.md`.

5. **Idiomatic Rust**
- Prefer typed enums such as `ControllerRef`, `Comparator`, and `Option<T>` over bool fields.
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/review-impl/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Skip checks CI already enforces:
- The frontend renders engine-provided state; it must not infer game rules or hidden data.
- Check React effect dependencies, unmount cleanup, touch equivalents, mobile scroll containment, and empty/loading/error states.
- Type-check passing is not proof of feature correctness; say when browser verification was not performed.
- **i18n:** Flag frontend-authored user-facing text (titles, labels, buttons, tooltips, placeholders, log templates) hardcoded in JSX instead of routed through `t()`. Conversely, flag engine/card pass-through (card names, Oracle text, interpolated enum strings) that was wrongly wrapped in `t()` — it belongs to the content pipeline, not chrome. Boundary rule: a string gets `t()` iff the frontend authored it (`client/src/i18n/README.md`). Also flag hand-rolled pluralization (`count === 1 ? …`) that should use `key_one`/`key_other`, and any direct `i18n.changeLanguage` call (the preferences store owns language).

### Multiplayer / Transport

Expand Down
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ members = ["crates/*"]
exclude = ["client/src-tauri", "lobby-worker/broker-wasm"]

[workspace.package]
version = "0.1.35"
version = "0.1.36"
license = "MIT OR Apache-2.0"

[workspace.dependencies]
Expand Down
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "phase-rs-client",
"private": true,
"version": "0.1.35",
"version": "0.1.36",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
Loading
Loading