diff --git a/.claude/skills/add-engine-effect/SKILL.md b/.claude/skills/add-engine-effect/SKILL.md index 1d8fb0a4f9..39b816a0ac 100644 --- a/.claude/skills/add-engine-effect/SKILL.md +++ b/.claude/skills/add-engine-effect/SKILL.md @@ -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. @@ -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) diff --git a/.claude/skills/add-frontend-component/SKILL.md b/.claude/skills/add-frontend-component/SKILL.md index a7e6ccb4c7..771c179d8e 100644 --- a/.claude/skills/add-frontend-component/SKILL.md +++ b/.claude/skills/add-frontend-component/SKILL.md @@ -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. @@ -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(); @@ -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 */} - + ); @@ -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/.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, `.` (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** @@ -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. @@ -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 })` | --- @@ -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 && \ diff --git a/.claude/skills/add-interactive-effect/SKILL.md b/.claude/skills/add-interactive-effect/SKILL.md index 8c1a8bd193..f40bfa80b9 100644 --- a/.claude/skills/add-interactive-effect/SKILL.md +++ b/.claude/skills/add-interactive-effect/SKILL.md @@ -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()`** @@ -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.` 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 | diff --git a/.claude/skills/review-engine-plan/SKILL.md b/.claude/skills/review-engine-plan/SKILL.md index 73534c3327..b10e0e39e8 100644 --- a/.claude/skills/review-engine-plan/SKILL.md +++ b/.claude/skills/review-engine-plan/SKILL.md @@ -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/.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` over bool fields. diff --git a/.claude/skills/review-impl/SKILL.md b/.claude/skills/review-impl/SKILL.md index f6a5c79f11..de99d47779 100644 --- a/.claude/skills/review-impl/SKILL.md +++ b/.claude/skills/review-impl/SKILL.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 38cab1bcb7..f6de379be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -520,7 +520,7 @@ dependencies = [ [[package]] name = "draft-core" -version = "0.1.35" +version = "0.1.36" dependencies = [ "engine", "rand 0.9.4", @@ -532,7 +532,7 @@ dependencies = [ [[package]] name = "draft-wasm" -version = "0.1.35" +version = "0.1.36" dependencies = [ "console_error_panic_hook", "draft-core", @@ -598,7 +598,7 @@ dependencies = [ [[package]] name = "engine" -version = "0.1.35" +version = "0.1.36" dependencies = [ "assert_matches", "im", @@ -623,7 +623,7 @@ dependencies = [ [[package]] name = "engine-inventory-gen" -version = "0.1.35" +version = "0.1.36" dependencies = [ "anyhow", "proc-macro2", @@ -636,7 +636,7 @@ dependencies = [ [[package]] name = "engine-wasm" -version = "0.1.35" +version = "0.1.36" dependencies = [ "console_error_panic_hook", "engine", @@ -688,7 +688,7 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "feed-scraper" -version = "0.1.35" +version = "0.1.36" dependencies = [ "clap", "reqwest", @@ -1338,7 +1338,7 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lobby-broker" -version = "0.1.35" +version = "0.1.36" dependencies = [ "engine", "serde", @@ -1432,7 +1432,7 @@ dependencies = [ [[package]] name = "mtgish-import" -version = "0.1.35" +version = "0.1.36" dependencies = [ "anyhow", "engine", @@ -1595,7 +1595,7 @@ dependencies = [ [[package]] name = "phase-ai" -version = "0.1.35" +version = "0.1.36" dependencies = [ "engine", "rand 0.9.4", @@ -1609,7 +1609,7 @@ dependencies = [ [[package]] name = "phase-server" -version = "0.1.35" +version = "0.1.36" dependencies = [ "axum", "clap", @@ -2146,7 +2146,7 @@ dependencies = [ [[package]] name = "seat-reducer" -version = "0.1.35" +version = "0.1.36" dependencies = [ "engine", "phase-ai", @@ -2322,7 +2322,7 @@ dependencies = [ [[package]] name = "server-core" -version = "0.1.35" +version = "0.1.36" dependencies = [ "draft-core", "engine", diff --git a/Cargo.toml b/Cargo.toml index cb1b0fc582..c6286e6a2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/client/package.json b/client/package.json index dbbd2cfafd..7a591a3976 100644 --- a/client/package.json +++ b/client/package.json @@ -1,7 +1,7 @@ { "name": "phase-rs-client", "private": true, - "version": "0.1.35", + "version": "0.1.36", "type": "module", "scripts": { "dev": "vite", diff --git a/client/public/feeds/mtggoldfish-commander.json b/client/public/feeds/mtggoldfish-commander.json index 171243831a..c769029aaa 100644 --- a/client/public/feeds/mtggoldfish-commander.json +++ b/client/public/feeds/mtggoldfish-commander.json @@ -5,16 +5,14 @@ "icon": "G", "format": "commander", "version": 1, - "updated": "2026-06-08T00:00:00Z", + "updated": "2026-06-09T00:00:00Z", "source": "https://www.mtggoldfish.com/metagame/commander", "decks": [ { - "name": "Y'shtola, Night's Blessed", + "name": "Krenko, Mob Boss", "author": "MTGGoldfish", "colors": [ - "W", - "B", - "U" + "R" ], "tags": [ "metagame" @@ -22,576 +20,574 @@ "main": [ { "count": 1, - "name": "Absorb" + "name": "Abrade" }, { "count": 1, - "name": "Alisaie Leveilleur" + "name": "Arms Dealer" }, { "count": 1, - "name": "Alphinaud Leveilleur" + "name": "Boggart Brute" }, { "count": 1, - "name": "Anguished Unmaking" + "name": "Castle Embereth" }, { "count": 1, - "name": "Arcane Sanctum" + "name": "Cathartic Reunion" }, { "count": 1, - "name": "Arcane Signet" + "name": "Cavalcade of Calamity" }, { "count": 1, - "name": "Archaeomancer's Map" + "name": "Comet Storm" }, { "count": 1, - "name": "Archmage Emeritus" + "name": "Dark-Dweller Oracle" }, { "count": 1, - "name": "Ardbert, Warrior of Darkness" + "name": "Destructive Tampering" }, { "count": 1, - "name": "Baleful Strix" + "name": "Dragon Fodder" }, { "count": 1, - "name": "Bender's Waterskin" + "name": "Experimental Frenzy" }, { "count": 1, - "name": "Bloodchief Ascension" + "name": "Fanatical Firebrand" }, { "count": 1, - "name": "Circle of Power" + "name": "Ferocity of the Wilds" }, { "count": 1, - "name": "Command Tower" + "name": "Forgotten Cave" }, { "count": 1, - "name": "Contaminated Aquifer" + "name": "Foundry Street Denizen" }, { "count": 1, - "name": "Crushing Disappointment" + "name": "Frenzied Goblin" }, { "count": 1, - "name": "Curiosity" + "name": "Gempalm Incinerator" }, { "count": 1, - "name": "Deadly Rollick" + "name": "Goblin Arsonist" }, { "count": 1, - "name": "Decanter of Endless Water" + "name": "Goblin Assault" }, { "count": 1, - "name": "Decorum Dissertation" + "name": "Goblin Burrows" }, { "count": 1, - "name": "Defacing Duskmage" + "name": "Goblin Cratermaker" }, { "count": 1, - "name": "Deserted Beach" + "name": "Goblin Dark-Dwellers" }, { "count": 1, - "name": "Disallow" + "name": "Goblin Diplomats" }, { "count": 1, - "name": "Dismember" + "name": "Goblin Fireleaper" }, { "count": 1, - "name": "Drowned Catacomb" + "name": "Goblin Goon" }, { "count": 1, - "name": "Emet-Selch of the Third Seat" + "name": "Goblin Instigator" }, { "count": 1, - "name": "Enduring Tenacity" + "name": "Goblin Matron" }, { "count": 1, - "name": "Exsanguinate" + "name": "Goblin Morningstar" }, { "count": 1, - "name": "Eye of Nidhogg" + "name": "Goblin Motivator" }, { "count": 1, - "name": "Fell the Profane" + "name": "Goblin Oriflamme" }, { "count": 1, - "name": "G'raha Tia, Scion Reborn" + "name": "Goblin Picker" }, { "count": 1, - "name": "Generous Gift" + "name": "Goblin Rally" }, { "count": 1, - "name": "Ghostly Prison" + "name": "Goblin Razerunners" }, { "count": 1, - "name": "Glacial Fortress" + "name": "Goblin Ringleader" }, { "count": 1, - "name": "Godless Shrine" + "name": "Goblin War Party" }, { "count": 1, - "name": "Hallowed Fountain" + "name": "Goblin Wardriver" }, { "count": 1, - "name": "Helm of the Ghastlord" + "name": "Hidden Volcano" }, { "count": 1, - "name": "Hermes, Overseer of Elpis" + "name": "Hordeling Outburst" }, { "count": 1, - "name": "Hullbreaker Horror" + "name": "Impact Tremors" }, { "count": 1, - "name": "Idyllic Beachfront" - }, - { - "count": 4, - "name": "Island" + "name": "Impulsive Pilferer" }, { "count": 1, - "name": "Isolated Chapel" + "name": "Koth, Fire of Resistance" }, { "count": 1, - "name": "Kambal, Consul of Allocation" + "name": "Krenko's Command" }, { "count": 1, - "name": "Lightning Greaves" + "name": "Krenko's Enforcer" }, { "count": 1, - "name": "Lotho, Corrupt Shirriff" + "name": "Krenko, Mob Boss" }, { "count": 1, - "name": "Lyse Hext" + "name": "Krenko, Tin Street Kingpin" }, { "count": 1, - "name": "Mai, Scornful Striker" + "name": "Legion Warboss" }, { "count": 1, - "name": "Mindcrank" + "name": "Lightning Strike" }, { "count": 1, - "name": "Morphic Pool" + "name": "Massive Raid" }, { "count": 1, - "name": "Norn's Annex" + "name": "Mizzium Mortars" }, { "count": 1, - "name": "Ophidian Eye" + "name": "Mogg War Marshal" }, { - "count": 1, - "name": "Overkill" + "count": 33, + "name": "Mountain" }, { "count": 1, - "name": "Papalymo Totolymo" + "name": "Outnumber" }, { "count": 1, - "name": "Phyresis" + "name": "Outpost Siege" }, { - "count": 4, - "name": "Plains" + "count": 1, + "name": "Path of Ancestry" }, { "count": 1, - "name": "Prairie Stream" + "name": "Raid Bombardment" }, { "count": 1, - "name": "Propaganda" + "name": "Rummaging Goblin" }, { "count": 1, - "name": "Queza, Augur of Agonies" + "name": "Runaway Steam-Kin" }, { "count": 1, - "name": "Raffine's Tower" + "name": "Shock" }, { "count": 1, - "name": "Read the Bones" + "name": "Siege-Gang Commander" }, { "count": 1, - "name": "Reliquary Tower" + "name": "Skirk Prospector" }, { "count": 1, - "name": "Rewind" + "name": "Sol Ring" }, { "count": 1, - "name": "Risky Shortcut" + "name": "Squee, Goblin Nabob" }, { "count": 1, - "name": "Roaming Throne" + "name": "Squee, the Immortal" }, { "count": 1, - "name": "Sanguine Bond" + "name": "Tarfire" }, { "count": 1, - "name": "Scheming Silvertongue" + "name": "Thrill of Possibility" }, { "count": 1, - "name": "Sea of Clouds" + "name": "Torch Courier" }, { "count": 1, - "name": "Shattered Sanctum" + "name": "Volley Veteran" }, { "count": 1, - "name": "Sheoldred, the Apocalypse" - }, + "name": "You See a Pair of Goblins" + } + ], + "sideboard": [] + }, + { + "name": "Y'shtola, Night's Blessed", + "author": "MTGGoldfish", + "colors": [ + "W", + "B", + "U" + ], + "tags": [ + "metagame" + ], + "main": [ { "count": 1, - "name": "Shipwreck Marsh" + "name": "Foolish Fate" }, { "count": 1, - "name": "Snuff Out" + "name": "Alisaie Leveilleur" }, { "count": 1, - "name": "Sol Ring" + "name": "Alphinaud Leveilleur" }, { "count": 1, - "name": "Stormscape Familiar" + "name": "Anguished Unmaking" }, { "count": 1, - "name": "Stroke of Midnight" + "name": "Arcane Sanctum" }, { "count": 1, - "name": "Sunken Hollow" + "name": "Arcane Signet" }, { "count": 1, - "name": "Sunlit Marsh" + "name": "Archaeomancer's Map" }, { - "count": 4, - "name": "Swamp" + "count": 1, + "name": "Archmage Emeritus" }, { "count": 1, - "name": "Swiftfoot Boots" + "name": "Ardbert, Warrior of Darkness" }, { "count": 1, - "name": "Swords to Plowshares" + "name": "Baleful Strix" }, { "count": 1, - "name": "Sygg, River Cutthroat" + "name": "Bender's Waterskin" }, { "count": 1, - "name": "Thought Vessel" + "name": "Bloodchief Ascension" }, { "count": 1, - "name": "Undercity Sewers" + "name": "Circle of Power" }, { "count": 1, - "name": "Undermine" + "name": "Command Tower" }, { "count": 1, - "name": "Unwind" + "name": "Contaminated Aquifer" }, { "count": 1, - "name": "Vault of Champions" + "name": "Crushing Disappointment" }, { "count": 1, - "name": "Vindicate" + "name": "Curiosity" }, { "count": 1, - "name": "Vito, Thorn of the Dusk Rose" + "name": "Deadly Rollick" }, { "count": 1, - "name": "Void Rend" + "name": "Decanter of Endless Water" }, { "count": 1, - "name": "Watery Grave" + "name": "Decorum Dissertation" }, { "count": 1, - "name": "Y'shtola, Night's Blessed" + "name": "Defacing Duskmage" }, { "count": 1, - "name": "Aetherflux Reservoir" - } - ], - "sideboard": [] - }, - { - "name": "Krenko, Mob Boss", - "author": "MTGGoldfish", - "colors": [ - "R" - ], - "tags": [ - "metagame" - ], - "main": [ + "name": "Deserted Beach" + }, { "count": 1, - "name": "Goblin Chirurgeon" + "name": "Disallow" }, { "count": 1, - "name": "Skirk Prospector" + "name": "Dismember" }, { "count": 1, - "name": "Battle Cry Goblin" + "name": "Drowned Catacomb" }, { "count": 1, - "name": "Conspicuous Snoop" + "name": "Emet-Selch of the Third Seat" }, { "count": 1, - "name": "Goblin Instigator" + "name": "Enduring Tenacity" }, { "count": 1, - "name": "Goblin Lookout" + "name": "Exsanguinate" }, { "count": 1, - "name": "Goblin Piledriver" + "name": "Eye of Nidhogg" }, { "count": 1, - "name": "Goblin Recruiter" + "name": "Fell the Profane" }, { "count": 1, - "name": "Hexing Squelcher" + "name": "G'raha Tia, Scion Reborn" }, { "count": 1, - "name": "Rundvelt Hordemaster" + "name": "Generous Gift" }, { "count": 1, - "name": "Combat Celebrant" + "name": "Ghostly Prison" }, { "count": 1, - "name": "General Kreat, the Boltbringer" + "name": "Glacial Fortress" }, { "count": 1, - "name": "Goblin Chieftain" + "name": "Godless Shrine" }, { "count": 1, - "name": "Goblin King" + "name": "Hallowed Fountain" }, { "count": 1, - "name": "Goblin Matron" + "name": "Helm of the Ghastlord" }, { "count": 1, - "name": "Goblin Warchief" + "name": "Hermes, Overseer of Elpis" }, { "count": 1, - "name": "Hobgoblin Bandit Lord" + "name": "Hullbreaker Horror" }, { "count": 1, - "name": "Howlsquad Heavy" + "name": "Idyllic Beachfront" }, { - "count": 1, - "name": "Krenko, Tin Street Kingpin" + "count": 4, + "name": "Island" }, { "count": 1, - "name": "Legion Warboss" + "name": "Isolated Chapel" }, { "count": 1, - "name": "Pashalik Mons" + "name": "Kambal, Consul of Allocation" }, { "count": 1, - "name": "Squee, the Immortal" + "name": "Lightning Greaves" }, { "count": 1, - "name": "Treasure Nabber" + "name": "Lotho, Corrupt Shirriff" }, { "count": 1, - "name": "Anger" + "name": "Lyse Hext" }, { "count": 1, - "name": "Beetleback Chief" + "name": "Mai, Scornful Striker" }, { "count": 1, - "name": "Goblin Ringleader" + "name": "Mindcrank" }, { "count": 1, - "name": "Goblin Trashmaster" + "name": "Morphic Pool" }, { "count": 1, - "name": "Krenko, Mob Boss" + "name": "Norn's Annex" }, { "count": 1, - "name": "Battle Squadron" + "name": "Ophidian Eye" }, { "count": 1, - "name": "Brash Taunter" + "name": "Overkill" }, { "count": 1, - "name": "Kiki-Jiki, Mirror Breaker" + "name": "Papalymo Totolymo" }, { "count": 1, - "name": "Siege-Gang Commander" + "name": "Phyresis" }, { - "count": 1, - "name": "Muxus, Goblin Grandee" + "count": 4, + "name": "Plains" }, { "count": 1, - "name": "Brightstone Ritual" + "name": "Prairie Stream" }, { "count": 1, - "name": "Last-Ditch Effort" + "name": "Propaganda" }, { "count": 1, - "name": "Red Elemental Blast" + "name": "Queza, Augur of Agonies" }, { "count": 1, - "name": "Chaos Warp" + "name": "Raffine's Tower" }, { "count": 1, - "name": "Flare of Duplication" + "name": "Read the Bones" }, { "count": 1, - "name": "Big Score" + "name": "Reliquary Tower" }, { "count": 1, - "name": "Faithless Looting" + "name": "Rewind" }, { "count": 1, - "name": "Goblin War Strike" + "name": "Risky Shortcut" }, { "count": 1, - "name": "Vandalblast" + "name": "Roaming Throne" }, { "count": 1, - "name": "Dragon Fodder" + "name": "Sanguine Bond" }, { "count": 1, - "name": "Krenko's Command" + "name": "Scheming Silvertongue" }, { "count": 1, - "name": "Hordeling Outburst" + "name": "Sea of Clouds" }, { "count": 1, - "name": "Empty the Warrens" + "name": "Shattered Sanctum" }, { "count": 1, - "name": "Blasphemous Act" + "name": "Sheoldred, the Apocalypse" }, { "count": 1, - "name": "Dawn-Blessed Pennant" + "name": "Shipwreck Marsh" }, { "count": 1, - "name": "Skullclamp" + "name": "Snuff Out" }, { "count": 1, @@ -599,80 +595,88 @@ }, { "count": 1, - "name": "Blazing Sunsteel" + "name": "Stormscape Familiar" }, { "count": 1, - "name": "Fellwar Stone" + "name": "Stroke of Midnight" }, { "count": 1, - "name": "Lightning Greaves" + "name": "Sunken Hollow" }, { "count": 1, - "name": "Ruby Medallion" + "name": "Sunlit Marsh" }, { - "count": 1, - "name": "Sting, the Glinting Dagger" + "count": 4, + "name": "Swamp" }, { "count": 1, - "name": "Hazoret's Monument" + "name": "Swiftfoot Boots" }, { "count": 1, - "name": "The Fire Crystal" + "name": "Swords to Plowshares" }, { "count": 1, - "name": "Coat of Arms" + "name": "Sygg, River Cutthroat" }, { "count": 1, - "name": "Vanquisher's Banner" + "name": "Thought Vessel" }, { "count": 1, - "name": "Goblin Bombardment" + "name": "Undercity Sewers" }, { "count": 1, - "name": "Impact Tremors" + "name": "Undermine" }, { "count": 1, - "name": "Aggravated Assault" + "name": "Unwind" }, { "count": 1, - "name": "Blood Moon" + "name": "Vault of Champions" }, { "count": 1, - "name": "Fervor" + "name": "Vindicate" }, { "count": 1, - "name": "Goblin War Drums" + "name": "Vito, Thorn of the Dusk Rose" }, { "count": 1, - "name": "Shared Animosity" + "name": "Void Rend" }, { - "count": 34, - "name": "Mountain" + "count": 1, + "name": "Watery Grave" + }, + { + "count": 1, + "name": "Y'shtola, Night's Blessed" + }, + { + "count": 1, + "name": "Aetherflux Reservoir" } ], "sideboard": [] }, { - "name": "Quandrix, the Proof", + "name": "Witherbloom, the Balancer", "author": "MTGGoldfish", "colors": [ - "U", + "B", "G" ], "tags": [ @@ -681,219 +685,227 @@ "main": [ { "count": 1, - "name": "Quandrix Charm" + "name": "Twitching Doll" }, { "count": 1, - "name": "Koma, World-Eater" + "name": "Chatterfang, Squirrel General" }, { "count": 1, - "name": "Ancient Imperiosaur" + "name": "Arbor Elf" }, { "count": 1, - "name": "Planar Engineering" + "name": "Elvish Mystic" }, { "count": 1, - "name": "Mystic Confluence" + "name": "Llanowar Elves" }, { "count": 1, - "name": "Hullbreaker Horror" + "name": "Viscera Seer" }, { "count": 1, - "name": "Imoti, Celebrant of Bounty" + "name": "Blood Artist" }, { "count": 1, - "name": "Rapid Hybridization" + "name": "Bogwater Lumaret" }, { "count": 1, - "name": "Ivy, Gleeful Spellthief" + "name": "Witherbloom Apprentice" }, { "count": 1, - "name": "Talrand, Sky Summoner" + "name": "Zulaport Cutthroat" }, { "count": 1, - "name": "Counterspell" + "name": "Enduring Vitality" }, { "count": 1, - "name": "Skyshroud Claim" + "name": "Sedgemoor Witch" }, { "count": 1, - "name": "Brainsurge" + "name": "Arasta of the Endless Web" }, { "count": 1, - "name": "Quandrix Pledgemage" + "name": "Creakwood Liege" }, { "count": 1, - "name": "Shark Typhoon" + "name": "Hazel of the Rootbloom" }, { "count": 1, - "name": "Proctor's Gaze" + "name": "Pitiless Plunderer" }, { "count": 1, - "name": "Wisdom of Ages" + "name": "Mycoloth" }, { "count": 1, - "name": "Cultivate" + "name": "Tendershoot Dryad" }, { "count": 1, - "name": "Quandrix Command" + "name": "Professor Onyx" }, { "count": 1, - "name": "Archmage of Runes" + "name": "Animist's Awakening" }, { "count": 1, - "name": "Growth Spiral" + "name": "Scatter the Seeds" }, { "count": 1, - "name": "Auroral Procession" + "name": "Scour from Existence" }, { "count": 1, - "name": "Hydro-Channeler" + "name": "For the Common Good" }, { "count": 1, - "name": "Roil Elemental" + "name": "Preposterous Proportions" }, { "count": 1, - "name": "Negate" + "name": "Pest Infestation" }, { "count": 1, - "name": "Eureka Moment" + "name": "Necrotic Hex" }, { "count": 1, - "name": "Double Major" + "name": "Praetor's Counsel" }, { "count": 1, - "name": "Beast Within" + "name": "Chain of Smog" + }, + { + "count": 1, + "name": "Brightcap Badger" }, { "count": 1, - "name": "Aetherize" + "name": "Damnable Pact" }, { "count": 1, - "name": "Rampant Growth" + "name": "Deadly Dispute" }, { "count": 1, - "name": "Into the Story" + "name": "Exsanguinate" }, { "count": 1, - "name": "Planar Genesis" + "name": "Farseek" }, { "count": 1, - "name": "Forceful Denial" + "name": "Nature's Lore" }, { "count": 1, - "name": "Deekah, Fractal Theorist" + "name": "Ophiomancer" }, { "count": 1, - "name": "Fact or Fiction" + "name": "Rampant Growth" }, { "count": 1, - "name": "Apex Devastator" + "name": "Sprout Swarm" }, { "count": 1, - "name": "Turnabout" + "name": "Tear Asunder" }, { "count": 1, - "name": "Llanowar Elves" + "name": "Rise of the Dark Realms" }, { "count": 1, - "name": "Inevitable Betrayal" + "name": "Witherbloom Charm" }, { "count": 1, - "name": "Rashmi, Eternities Crafter" + "name": "Chord of Calling" }, { "count": 1, - "name": "Echocasting Symposium" + "name": "Cultivate" }, { "count": 1, - "name": "Jadzi, Oracle of Arcavios" + "name": "Moseo, Vein's New Dean" }, { "count": 1, - "name": "Praetor's Counsel" + "name": "Ribtruss Roaster" }, { "count": 1, - "name": "Murmuring Mystic" + "name": "Kodama's Reach" }, { "count": 1, - "name": "Time Stretch" + "name": "Pest Rescuer" }, { "count": 1, - "name": "Haughty Djinn" + "name": "Army of the Damned" }, { "count": 1, - "name": "Body of Research" + "name": "Saproling Symbiosis" }, { "count": 1, - "name": "Lier, Disciple of the Drowned" + "name": "Dark Petition" }, { "count": 1, - "name": "Traverse the Outlands" + "name": "Dregs of Sorrow" }, { "count": 1, - "name": "Hermes, Overseer of Elpis" + "name": "Increasing Ambition" }, { "count": 1, - "name": "Omniscience" + "name": "Shamanic Revelation" }, { "count": 1, - "name": "High Fae Trickster" + "name": "Windgrace's Judgment" }, { "count": 1, - "name": "Arcane Signet" + "name": "Beseech the Queen" + }, + { + "count": 1, + "name": "Collective Unconscious" }, { "count": 1, - "name": "Arixmethes, Slumbering Isle" + "name": "Eternal Witness" }, { "count": 1, @@ -901,65 +913,93 @@ }, { "count": 1, - "name": "Archaeomancer" + "name": "Golgari Signet" }, { "count": 1, - "name": "Talisman of Curiosity" + "name": "Talisman of Resilience" }, { "count": 1, - "name": "Dirgur Focusmage" + "name": "Tooth and Nail" }, { "count": 1, - "name": "Chrome Host Seedshark" + "name": "Insidious Roots" }, { "count": 1, - "name": "Berta, Wise Extrapolator" + "name": "Springleaf Parade" }, { "count": 1, - "name": "Kianne, Corrupted Memory" + "name": "Phyrexian Arena" }, { "count": 1, - "name": "Quandrix, the Proof" + "name": "Unbound Flourishing" }, { - "count": 17, - "name": "Island" + "count": 1, + "name": "Command Tower" }, { - "count": 17, - "name": "Forest" + "count": 1, + "name": "Darkbore Pathway" }, { "count": 1, - "name": "Command Tower" + "name": "Llanowar Wastes" }, { "count": 1, - "name": "Sodden Verdure" + "name": "Overgrown Tomb" }, { "count": 1, - "name": "Breeding Pool" + "name": "Reliquary Tower" + }, + { + "count": 1, + "name": "Tainted Wood" + }, + { + "count": 1, + "name": "Temple of Malady" + }, + { + "count": 1, + "name": "Twilight Mire" + }, + { + "count": 1, + "name": "Vernal Fen" + }, + { + "count": 1, + "name": "Viridescent Bog" + }, + { + "count": 12, + "name": "Forest" + }, + { + "count": 13, + "name": "Swamp" }, { "count": 1, - "name": "Dreamroot Cascade" + "name": "Witherbloom, the Balancer" } ], "sideboard": [] }, { - "name": "Witherbloom, the Balancer", + "name": "Quandrix, the Proof", "author": "MTGGoldfish", "colors": [ - "B", - "G" + "G", + "U" ], "tags": [ "metagame" @@ -967,321 +1007,309 @@ "main": [ { "count": 1, - "name": "Twitching Doll" + "name": "Quandrix, the Proof" }, { "count": 1, - "name": "Chatterfang, Squirrel General" + "name": "Flooded Grove" }, { "count": 1, - "name": "Arbor Elf" + "name": "Yavimaya Coast" }, { - "count": 1, - "name": "Elvish Mystic" + "count": 11, + "name": "Forest" }, { - "count": 1, - "name": "Llanowar Elves" + "count": 17, + "name": "Island" }, { "count": 1, - "name": "Viscera Seer" + "name": "Tangled Islet" }, { "count": 1, - "name": "Blood Artist" + "name": "Vineglimmer Snarl" }, { "count": 1, - "name": "Bogwater Lumaret" + "name": "Exotic Orchard" }, { "count": 1, - "name": "Witherbloom Apprentice" + "name": "Mystic Sanctuary" }, { "count": 1, - "name": "Zulaport Cutthroat" + "name": "Temple of the False God" }, { "count": 1, - "name": "Enduring Vitality" + "name": "Inevitable Betrayal" }, { "count": 1, - "name": "Sedgemoor Witch" + "name": "Ancestral Vision" }, { "count": 1, - "name": "Arasta of the Endless Web" + "name": "Sol Ring" }, { "count": 1, - "name": "Creakwood Liege" + "name": "Llanowar Elves" }, { "count": 1, - "name": "Hazel of the Rootbloom" + "name": "Preordain" }, { "count": 1, - "name": "Pitiless Plunderer" + "name": "Brainstorm" }, { "count": 1, - "name": "Mycoloth" + "name": "Unable to Scream" }, { "count": 1, - "name": "Tendershoot Dryad" + "name": "Serum Visions" }, { "count": 1, - "name": "Professor Onyx" + "name": "Fellwar Stone" }, { "count": 1, - "name": "Animist's Awakening" + "name": "Baral, Chief of Compliance" }, { "count": 1, - "name": "Dark Ritual" + "name": "Boomerang" }, { "count": 1, - "name": "Scour from Existence" + "name": "Farseek" }, { "count": 1, - "name": "For the Common Good" + "name": "Rampant Growth" }, { "count": 1, - "name": "Preposterous Proportions" + "name": "Reality Shift" }, { "count": 1, - "name": "Pest Infestation" + "name": "Growth Spiral" }, { "count": 1, - "name": "Necrotic Hex" + "name": "Explore" }, { "count": 1, - "name": "Praetor's Counsel" + "name": "Get Out" }, { "count": 1, - "name": "Chain of Smog" + "name": "Naturalize" }, { "count": 1, - "name": "Brightcap Badger" + "name": "Sapphire Medallion" }, { "count": 1, - "name": "Damnable Pact" + "name": "Resculpt" }, { "count": 1, - "name": "Deadly Dispute" + "name": "Return to Nature" }, { "count": 1, - "name": "Exsanguinate" + "name": "Nature's Lore" }, { "count": 1, - "name": "Farseek" + "name": "Shared Roots" }, { "count": 1, - "name": "Nature's Lore" + "name": "Arcane Signet" }, { "count": 1, - "name": "Ophiomancer" + "name": "Simic Signet" }, { "count": 1, - "name": "Rampant Growth" + "name": "Tribute to the Wild" }, { "count": 1, - "name": "Sprout Swarm" + "name": "Amphibian Downpour" }, { "count": 1, - "name": "Tear Asunder" + "name": "Archmage's Charm" }, { "count": 1, - "name": "Rise of the Dark Realms" + "name": "Cultivate" }, { "count": 1, - "name": "Witherbloom Charm" + "name": "Cycle of Renewal" }, { "count": 1, - "name": "Chord of Calling" + "name": "Brainsurge" }, { "count": 1, - "name": "Cultivate" + "name": "Harrow" }, { "count": 1, - "name": "Moseo, Vein's New Dean" + "name": "Beast Within" }, { "count": 1, - "name": "Genesis Wave" + "name": "Haughty Djinn" }, { "count": 1, - "name": "Kodama's Reach" + "name": "Entish Restoration" }, { "count": 1, - "name": "Culling Ritual" + "name": "Stock Up" }, { "count": 1, - "name": "Empty the Pits" + "name": "Archmage Emeritus" }, { "count": 1, - "name": "Saproling Symbiosis" + "name": "Leyline of Anticipation" }, { "count": 1, - "name": "Dark Petition" + "name": "Map the Frontier" }, { "count": 1, - "name": "Dregs of Sorrow" + "name": "Consuming Tide" }, { "count": 1, - "name": "Increasing Ambition" + "name": "Eureka Moment" }, { "count": 1, - "name": "Shamanic Revelation" + "name": "Naru Meha, Master Wizard" }, { "count": 1, - "name": "Windgrace's Judgment" + "name": "Wash Out" }, { "count": 1, - "name": "Beseech the Queen" + "name": "The Water Crystal" }, { "count": 1, - "name": "Collective Unconscious" + "name": "Venser, Shaper Savant" }, { "count": 1, - "name": "Eternal Witness" + "name": "Peregrine Drake" }, { "count": 1, - "name": "Sol Ring" + "name": "Embrace the Paradox" }, { "count": 1, - "name": "Golgari Signet" + "name": "Urban Evolution" }, { "count": 1, - "name": "Talisman of Resilience" + "name": "Imoti, Celebrant of Bounty" }, { "count": 1, - "name": "Tooth and Nail" + "name": "Gush" }, { "count": 1, - "name": "Insidious Roots" + "name": "Lorien Revealed" }, { "count": 1, - "name": "Springleaf Parade" + "name": "Devastation Tide" }, { "count": 1, - "name": "Phyrexian Arena" + "name": "Step Between Worlds" }, { "count": 1, - "name": "Unbound Flourishing" + "name": "Natural Reclamation" }, { "count": 1, - "name": "Command Tower" + "name": "Aether Gale" }, { "count": 1, - "name": "Darkbore Pathway" + "name": "Rampaging Baloths" }, { "count": 1, - "name": "Llanowar Wastes" + "name": "Part the Waterveil" }, { "count": 1, - "name": "Overgrown Tomb" + "name": "Alrund's Epiphany" }, { "count": 1, - "name": "Reliquary Tower" + "name": "Temporal Mastery" }, { "count": 1, - "name": "Tainted Wood" - }, - { - "count": 1, - "name": "Temple of Malady" + "name": "Wisdom of Ages" }, { "count": 1, - "name": "Twilight Mire" + "name": "Aminatou's Augury" }, { "count": 1, - "name": "Vernal Fen" + "name": "Treasure Cruise" }, { "count": 1, - "name": "Viridescent Bog" - }, - { - "count": 12, - "name": "Forest" - }, - { - "count": 13, - "name": "Swamp" + "name": "Dig Through Time" }, { "count": 1, - "name": "Witherbloom, the Balancer" + "name": "Temporal Trespass" } ], "sideboard": [] }, { - "name": "Kinnan, Bonder Prodigy", + "name": "Prismari, the Inspiration", "author": "MTGGoldfish", "colors": [ "U", - "G" + "R" ], "tags": [ "metagame" @@ -1289,1144 +1317,1128 @@ "main": [ { "count": 1, - "name": "Amphibian Downpour" + "name": "Hellkite Courser" }, { "count": 1, - "name": "Ancient Tomb" + "name": "Geode Golem" }, { "count": 1, - "name": "Arcane Signet" + "name": "The Dawning Archaic" }, { "count": 1, - "name": "Barkchannel Pathway" + "name": "Third Path Iconoclast" }, { "count": 1, - "name": "Basalt Monolith" + "name": "Dragonspeaker Shaman" }, { "count": 1, - "name": "Birds of Paradise" + "name": "Runaway Steam-Kin" }, { "count": 1, - "name": "Borne Upon a Wind" + "name": "Goldspan Dragon" }, { "count": 1, - "name": "Boseiju, Who Endures" + "name": "Ragavan, Nimble Pilferer" }, { "count": 1, - "name": "Breeding Pool" + "name": "Dirgur Focusmage" }, { "count": 1, - "name": "Cephalid Coliseum" + "name": "Galazeth Prismari" }, { "count": 1, - "name": "Chain of Vapor" + "name": "Hexing Squelcher" }, { "count": 1, - "name": "Chord of Calling" + "name": "Snapcaster Mage" }, { "count": 1, - "name": "Chrome Mox" + "name": "Sprite Dragon" }, { "count": 1, - "name": "City of Brass" + "name": "Opt" }, { "count": 1, - "name": "Clever Impersonator" + "name": "Prismari Command" }, { "count": 1, - "name": "Command Tower" + "name": "Prismari Charm" }, { "count": 1, - "name": "Commandeer" + "name": "Brainstorm" }, { "count": 1, - "name": "Consecrated Sphinx" + "name": "Snap" }, { "count": 1, - "name": "Copy Artifact" + "name": "Gut Shot" }, { "count": 1, - "name": "Copy Enchantment" + "name": "Borne Upon a Wind" }, { "count": 1, - "name": "Disrupting Shoal" + "name": "High Tide" }, { "count": 1, - "name": "Dizzy Spell" + "name": "Dig Through Time" }, { "count": 1, - "name": "Dramatic Reversal" + "name": "Impulse" }, { "count": 1, - "name": "Drift of Phantasms" + "name": "Unexpected Windfall" }, { "count": 1, - "name": "Elvish Mystic" + "name": "Consider" }, { "count": 1, - "name": "Elvish Spirit Guide" + "name": "Abjure" }, { "count": 1, - "name": "Endurance" + "name": "Turn Aside" }, { "count": 1, - "name": "Enduring Vitality" + "name": "Wash Away" }, { "count": 1, - "name": "Faerie Mastermind" + "name": "Disruption Protocol" }, { "count": 1, - "name": "Fellwar Stone" + "name": "Familiar's Ruse" }, { "count": 1, - "name": "Fierce Guardianship" + "name": "Arcane Denial" }, { "count": 1, - "name": "Finale of Devastation" + "name": "Mana Drain" }, { "count": 1, - "name": "Flash Photography" + "name": "Rapturous Moment" }, { "count": 1, - "name": "Flesh Duplicate" + "name": "Splatter Technique" }, { "count": 1, - "name": "Flooded Strand" + "name": "Mathemagics" }, { "count": 1, - "name": "Flusterstorm" + "name": "Time Warp" }, { "count": 1, - "name": "Force of Negation" + "name": "Crackle with Power" }, { "count": 1, - "name": "Force of Will" + "name": "Merchant Scroll" }, { "count": 1, - "name": "Forest" + "name": "Fireball" }, { "count": 1, - "name": "Fyndhorn Elves" + "name": "Preordain" }, { "count": 1, - "name": "Gemstone Caverns" + "name": "Stock Up" }, { "count": 1, - "name": "Gene Pollinator" + "name": "Imposing Grandeur" }, { "count": 1, - "name": "Grim Monolith" + "name": "Ponder" }, { "count": 1, - "name": "Hidden Strings" + "name": "Time Spiral" }, { "count": 1, - "name": "High Fae Trickster" + "name": "Echo of Eons" }, { "count": 1, - "name": "Hullbreaker Horror" + "name": "Grapeshot" }, { "count": 1, - "name": "Hydroelectric Specimen" + "name": "Timetwister" }, { "count": 1, - "name": "Imposter Mech" + "name": "Commit // Memory" }, { "count": 1, - "name": "Invasion of Ikoria" + "name": "Campfire" }, { "count": 1, - "name": "Island" + "name": "The Immortal Sun" }, { "count": 1, - "name": "Llanowar Elves" + "name": "Sapphire Medallion" }, { "count": 1, - "name": "Lotus Petal" + "name": "Resonating Lute" }, { "count": 1, - "name": "Mana Confluence" + "name": "The Fire Crystal" }, { "count": 1, - "name": "Mana Vault" + "name": "Doubling Cube" }, { "count": 1, - "name": "Mental Misstep" + "name": "Levitating Statue" }, { "count": 1, - "name": "Mindbreak Trap" + "name": "Arcane Signet" }, { "count": 1, - "name": "Mirage Mirror" + "name": "Thought Vessel" }, { "count": 1, - "name": "Mirrormade" + "name": "Tablet of Discovery" }, { "count": 1, - "name": "Misdirection" + "name": "Commander's Sphere" }, { "count": 1, - "name": "Misty Rainforest" + "name": "Fellwar Stone" }, { "count": 1, - "name": "Mockingbird" + "name": "Ruby Medallion" }, { "count": 1, - "name": "Mox Amber" + "name": "Mindsplice Apparatus" }, { "count": 1, - "name": "Mox Diamond" + "name": "Propaganda" }, { "count": 1, - "name": "Mox Opal" + "name": "Omniscience" }, { "count": 1, - "name": "Mystic Remora" + "name": "Case of the Ransacked Lab" }, { "count": 1, - "name": "Mystical Tutor" + "name": "Cavern of Souls" }, { "count": 1, - "name": "Nature's Rhythm" + "name": "Command Beacon" }, { "count": 1, - "name": "Nezahal, Primal Tide" + "name": "Reliquary Tower" }, { "count": 1, - "name": "Otawara, Soaring City" + "name": "Mystic Sanctuary" }, { "count": 1, - "name": "Pact of Negation" + "name": "Mistrise Village" }, { "count": 1, - "name": "Phyrexian Metamorph" + "name": "Myriad Landscape" }, { "count": 1, - "name": "Polluted Delta" + "name": "Prismari Campus" }, { "count": 1, - "name": "Rejuvenating Springs" + "name": "Thundering Falls" }, { "count": 1, - "name": "Rhystic Study" + "name": "Cascade Bluffs" }, { "count": 1, - "name": "Scalding Tarn" + "name": "Gemstone Caverns" }, { "count": 1, - "name": "Sink into Stupor" + "name": "Volcanic Island" }, { "count": 1, - "name": "Snapback" + "name": "Training Center" }, { "count": 1, - "name": "Sol Ring" + "name": "Scorched Geyser" }, { "count": 1, - "name": "Springleaf Drum" + "name": "Scalding Tarn" }, { "count": 1, - "name": "Subtlety" + "name": "Exotic Orchard" }, { "count": 1, - "name": "Swan Song" + "name": "Frostboil Snarl" }, { "count": 1, - "name": "Talisman of Curiosity" + "name": "Stormcarved Coast" }, { "count": 1, - "name": "Tarnished Citadel" + "name": "Coastal Peak" }, { "count": 1, - "name": "The Cabbage Merchant" + "name": "Reflecting Pool" }, { "count": 1, - "name": "The Unagi of Kyoshi Island" + "name": "Command Tower" }, { - "count": 1, - "name": "Thrasios, Triton Hero" + "count": 9, + "name": "Island" }, { - "count": 1, - "name": "Transmute Artifact" + "count": 5, + "name": "Mountain" }, { "count": 1, - "name": "Trophy Mage" - }, + "name": "Prismari, the Inspiration" + } + ], + "sideboard": [] + }, + { + "name": "Edgar Markov", + "author": "MTGGoldfish", + "colors": [ + "W", + "R", + "B" + ], + "tags": [ + "metagame" + ], + "main": [ { "count": 1, - "name": "Tropical Island" + "name": "Swords to Plowshares" }, { "count": 1, - "name": "Valley Floodcaller" + "name": "Path to Exile" }, { "count": 1, - "name": "Veil of Summer" + "name": "Foreboding Ruins" }, { "count": 1, - "name": "Verdant Catacombs" + "name": "Sol Ring" }, { "count": 1, - "name": "Wan Shi Tong, Librarian" + "name": "Arcane Signet" }, { "count": 1, - "name": "Waterlogged Grove" + "name": "Generous Gift" }, { "count": 1, - "name": "Whir of Invention" + "name": "Lethal Scheme" }, { "count": 1, - "name": "Windswept Heath" + "name": "Blasphemous Act" }, { "count": 1, - "name": "Wooded Foothills" + "name": "Exotic Orchard" }, { "count": 1, - "name": "Worldly Tutor" + "name": "Path of Ancestry" }, { "count": 1, - "name": "Yavimaya Coast" + "name": "Command Tower" }, { "count": 1, - "name": "Kinnan, Bonder Prodigy" - } - ], - "sideboard": [] - }, - { - "name": "Prismari, the Inspiration", - "author": "MTGGoldfish", - "colors": [ - "R", - "U" - ], - "tags": [ - "metagame" - ], - "main": [ - { - "count": 1, - "name": "Bulk Up" + "name": "Unclaimed Territory" }, { "count": 1, - "name": "Traumatic Critique" + "name": "Battlefield Forge" }, { "count": 1, - "name": "Temple of Epiphany" + "name": "Caves of Koilos" }, { "count": 1, - "name": "Prismari Campus" + "name": "Tainted Field" }, { "count": 1, - "name": "Path of Ancestry" + "name": "Voldaren Estate" }, { "count": 1, - "name": "Expressive Iteration" + "name": "Dragonskull Summit" }, { "count": 1, - "name": "Study Hall" + "name": "Isolated Chapel" }, { "count": 1, - "name": "Spectacle Summit" + "name": "Vault of the Archangel" }, { "count": 1, - "name": "Dirgur Focusmage" + "name": "Fetid Heath" }, { "count": 1, - "name": "Manaform Hellkite" + "name": "Smoldering Marsh" }, { "count": 1, - "name": "Shivan Reef" + "name": "Secluded Courtyard" }, { "count": 1, - "name": "Consult the Star Charts" + "name": "Nomad Outpost" }, { "count": 1, - "name": "Blazing Firesinger" + "name": "Canyon Slough" }, { "count": 1, - "name": "Cascade Bluffs" + "name": "Shineshadow Snarl" }, { "count": 1, - "name": "Temple of the False God" + "name": "Sulfurous Springs" }, { "count": 1, - "name": "Archmage Emeritus" + "name": "Stroke of Midnight" }, { "count": 1, - "name": "Shark Typhoon" + "name": "Tainted Peak" }, { "count": 1, - "name": "Sulfur Falls" + "name": "Edgar Markov" }, { "count": 1, - "name": "Command Tower" + "name": "Clifftop Retreat" }, { - "count": 1, - "name": "Opt" + "count": 4, + "name": "Plains" }, { - "count": 1, - "name": "Tablet of Discovery" + "count": 4, + "name": "Mountain" }, { - "count": 1, - "name": "An Offer You Can't Refuse" + "count": 9, + "name": "Swamp" }, { "count": 1, - "name": "Goblin Glasswright" + "name": "Ruinous Ultimatum" }, { "count": 1, - "name": "Abstract Paintmage" + "name": "Village Rites" }, { "count": 1, - "name": "Rewind" + "name": "Corrupted Conviction" }, { "count": 1, - "name": "Stock Up" + "name": "Vampire Cutthroat" }, { "count": 1, - "name": "Resonating Lute" + "name": "Stromkirk Noble" }, { "count": 1, - "name": "Emeritus of Ideation" + "name": "Pulse Tracker" }, { "count": 1, - "name": "Rapturous Moment" + "name": "Master of Dark Rites" }, { "count": 1, - "name": "Flashback" + "name": "Knight of the Ebon Legion" }, { "count": 1, - "name": "Big Score" + "name": "Indulgent Aristocrat" }, { "count": 1, - "name": "Creative Technique" + "name": "Gas Guzzler" }, { "count": 1, - "name": "Ferrous Lake" + "name": "Legion Lieutenant" }, { "count": 1, - "name": "Volcanic Torrent" + "name": "Dusk Legion Zealot" }, { "count": 1, - "name": "The Emperor of Palamecia" + "name": "Cruel Celebrant" }, { "count": 1, - "name": "Master the Way" + "name": "Cordial Vampire" }, { "count": 1, - "name": "Prismari Charm" + "name": "Blood Artist" }, { "count": 1, - "name": "Rousing Refrain" + "name": "Voldaren Epicure" }, { "count": 1, - "name": "Pinnacle Monk" + "name": "Viscera Seer" }, { "count": 1, - "name": "Beacon of Tomorrows" + "name": "Vicious Conquistador" }, { "count": 1, - "name": "Reliquary Tower" + "name": "Florian, Voldaren Scion" }, { "count": 1, - "name": "Arcane Signet" + "name": "Susurian Voidborn" }, { "count": 1, - "name": "Teach by Example" + "name": "Stromkirk Captain" }, { "count": 1, - "name": "Prismari Command" + "name": "Ruthless Lawbringer" }, { "count": 1, - "name": "Fellwar Stone" + "name": "Qarsi Revenant" }, { "count": 1, - "name": "Abrade" + "name": "Preacher of the Schism" }, { "count": 1, - "name": "Sol Ring" + "name": "Mirror Entity" }, { "count": 1, - "name": "Seize the Spoils" + "name": "Markov Baron" }, { "count": 1, - "name": "Coastal Peak" + "name": "Drana, Liberator of Malakir" }, { "count": 1, - "name": "Comet Storm" + "name": "Clavileno, First of the Blessed" }, { "count": 1, - "name": "Sanar, Unfinished Genius" + "name": "Captivating Vampire" }, { "count": 1, - "name": "Blasphemous Act" + "name": "Vengeful Bloodwitch" }, { "count": 1, - "name": "Crackle with Power" + "name": "Vampire Socialite" }, { "count": 1, - "name": "Terramorphic Expanse" + "name": "Vampire Gourmand" }, { "count": 1, - "name": "Talisman of Creativity" + "name": "Welcoming Vampire" }, { "count": 1, - "name": "Colorstorm Stallion" + "name": "Falkenrath Noble" }, { "count": 1, - "name": "Furygale Flocking" + "name": "Sanctum Seeker" }, { "count": 1, - "name": "Scorched Geyser" + "name": "Champion of Dusk" }, { "count": 1, - "name": "Storm-Kiln Artist" + "name": "Malakir Bloodwitch" }, { "count": 1, - "name": "Stormcatch Mentor" + "name": "Bartolome del Presidio" }, { "count": 1, - "name": "Harmonized Trio" + "name": "Bloodthrone Vampire" }, { "count": 1, - "name": "Fabled Passage" + "name": "Goblin Bombardment" }, { "count": 1, - "name": "Mana Geyser" + "name": "Skullclamp" }, { "count": 1, - "name": "Dualcaster Mage" + "name": "Patchwork Banner" }, { "count": 1, - "name": "Snap" + "name": "Deadly Dispute" }, { "count": 1, - "name": "Frantic Search" + "name": "Plumb the Forbidden" }, { "count": 1, - "name": "Refute" + "name": "Fracture" }, { "count": 1, - "name": "Leitmotif Composer" + "name": "Vindicate" }, { "count": 1, - "name": "Jaya's Immolating Inferno" + "name": "Despark" }, { "count": 1, - "name": "Zaffai and the Tempests" + "name": "Invasion of New Capenna" }, { "count": 1, - "name": "Flare of Duplication" + "name": "Night's Whisper" }, { "count": 1, - "name": "Electro, Assaulting Battery" + "name": "Stromkirk Condemned" }, { "count": 1, - "name": "Hall of Oracles" + "name": "Crossway Troublemakers" }, { "count": 1, - "name": "Mystic Sanctuary" + "name": "Gathering Stone" }, { "count": 1, - "name": "River's Rebuke" - }, + "name": "Rite of Oblivion" + } + ], + "sideboard": [] + }, + { + "name": "Kinnan, Bonder Prodigy", + "author": "MTGGoldfish", + "colors": [ + "U", + "G" + ], + "tags": [ + "metagame" + ], + "main": [ { "count": 1, - "name": "Exotic Orchard" + "name": "Amphibian Downpour" }, { "count": 1, - "name": "Dig Through Time" + "name": "Ancient Tomb" }, { "count": 1, - "name": "Frostboil Snarl" + "name": "Arcane Signet" }, { "count": 1, - "name": "Turbulent Springs" + "name": "Barkchannel Pathway" }, { "count": 1, - "name": "Restless Spire" + "name": "Basalt Monolith" }, { "count": 1, - "name": "Decanter of Endless Water" + "name": "Birds of Paradise" }, { "count": 1, - "name": "Chaos Warp" + "name": "Borne Upon a Wind" }, { "count": 1, - "name": "Replication Technique" + "name": "Boseiju, Who Endures" }, { "count": 1, - "name": "Arcane Denial" + "name": "Breeding Pool" }, { "count": 1, - "name": "Molten Tributary" + "name": "Cephalid Coliseum" }, { "count": 1, - "name": "Sundering Eruption" + "name": "Chain of Vapor" }, { "count": 1, - "name": "Counterspell" + "name": "Chord of Calling" }, { "count": 1, - "name": "Sunbird's Invocation" + "name": "Chrome Mox" }, { "count": 1, - "name": "Thousand-Year Storm" - }, - { - "count": 5, - "name": "Mountain" - }, - { - "count": 5, - "name": "Island" + "name": "City of Brass" }, { "count": 1, - "name": "Prismari, the Inspiration" - } - ], - "sideboard": [] - }, - { - "name": "Edgar Markov", - "author": "MTGGoldfish", - "colors": [ - "W", - "R", - "B" - ], - "tags": [ - "metagame" - ], - "main": [ - { - "count": 1, - "name": "Swords to Plowshares" + "name": "Clever Impersonator" }, { "count": 1, - "name": "Path to Exile" + "name": "Command Tower" }, { "count": 1, - "name": "Foreboding Ruins" + "name": "Commandeer" }, { "count": 1, - "name": "Sol Ring" + "name": "Consecrated Sphinx" }, { "count": 1, - "name": "Arcane Signet" + "name": "Copy Artifact" }, { "count": 1, - "name": "Generous Gift" + "name": "Copy Enchantment" }, { "count": 1, - "name": "Lethal Scheme" + "name": "Disrupting Shoal" }, { "count": 1, - "name": "Blasphemous Act" + "name": "Dizzy Spell" }, { "count": 1, - "name": "Exotic Orchard" + "name": "Dramatic Reversal" }, { "count": 1, - "name": "Path of Ancestry" + "name": "Drift of Phantasms" }, { "count": 1, - "name": "Command Tower" + "name": "Elvish Mystic" }, { "count": 1, - "name": "Unclaimed Territory" + "name": "Elvish Spirit Guide" }, { "count": 1, - "name": "Battlefield Forge" + "name": "Endurance" }, { "count": 1, - "name": "Caves of Koilos" + "name": "Enduring Vitality" }, { "count": 1, - "name": "Tainted Field" + "name": "Faerie Mastermind" }, { "count": 1, - "name": "Voldaren Estate" + "name": "Fellwar Stone" }, { "count": 1, - "name": "Dragonskull Summit" + "name": "Fierce Guardianship" }, { "count": 1, - "name": "Isolated Chapel" + "name": "Finale of Devastation" }, { "count": 1, - "name": "Vault of the Archangel" + "name": "Flash Photography" }, { "count": 1, - "name": "Fetid Heath" + "name": "Flesh Duplicate" }, { "count": 1, - "name": "Smoldering Marsh" + "name": "Flooded Strand" }, { "count": 1, - "name": "Secluded Courtyard" + "name": "Flusterstorm" }, { "count": 1, - "name": "Nomad Outpost" + "name": "Force of Negation" }, { "count": 1, - "name": "Canyon Slough" + "name": "Force of Will" }, { "count": 1, - "name": "Shineshadow Snarl" + "name": "Forest" }, { "count": 1, - "name": "Sulfurous Springs" + "name": "Fyndhorn Elves" }, { "count": 1, - "name": "Stroke of Midnight" + "name": "Gemstone Caverns" }, { "count": 1, - "name": "Tainted Peak" + "name": "Gene Pollinator" }, { "count": 1, - "name": "Edgar Markov" + "name": "Grim Monolith" }, { "count": 1, - "name": "Clifftop Retreat" + "name": "Hidden Strings" }, { - "count": 4, - "name": "Plains" + "count": 1, + "name": "High Fae Trickster" }, { - "count": 4, - "name": "Mountain" + "count": 1, + "name": "Hullbreaker Horror" }, { - "count": 9, - "name": "Swamp" + "count": 1, + "name": "Hydroelectric Specimen" }, { "count": 1, - "name": "Ruinous Ultimatum" + "name": "Imposter Mech" }, { "count": 1, - "name": "Village Rites" + "name": "Invasion of Ikoria" }, { "count": 1, - "name": "Corrupted Conviction" + "name": "Island" }, { "count": 1, - "name": "Vampire Cutthroat" + "name": "Llanowar Elves" }, { "count": 1, - "name": "Stromkirk Noble" + "name": "Lotus Petal" }, { "count": 1, - "name": "Pulse Tracker" + "name": "Mana Confluence" }, { "count": 1, - "name": "Master of Dark Rites" + "name": "Mana Vault" }, { "count": 1, - "name": "Knight of the Ebon Legion" + "name": "Mental Misstep" }, { "count": 1, - "name": "Indulgent Aristocrat" + "name": "Mindbreak Trap" }, { "count": 1, - "name": "Gas Guzzler" + "name": "Mirage Mirror" }, { "count": 1, - "name": "Legion Lieutenant" + "name": "Mirrormade" }, { "count": 1, - "name": "Dusk Legion Zealot" + "name": "Misdirection" }, { "count": 1, - "name": "Cruel Celebrant" + "name": "Misty Rainforest" }, { "count": 1, - "name": "Cordial Vampire" + "name": "Mockingbird" }, { "count": 1, - "name": "Blood Artist" + "name": "Mox Amber" }, { "count": 1, - "name": "Voldaren Epicure" + "name": "Mox Diamond" }, { "count": 1, - "name": "Viscera Seer" + "name": "Mox Opal" }, { "count": 1, - "name": "Vicious Conquistador" + "name": "Mystic Remora" }, { "count": 1, - "name": "Florian, Voldaren Scion" + "name": "Mystical Tutor" }, { "count": 1, - "name": "Susurian Voidborn" + "name": "Nature's Rhythm" }, { "count": 1, - "name": "Stromkirk Captain" + "name": "Nezahal, Primal Tide" }, { "count": 1, - "name": "Ruthless Lawbringer" + "name": "Otawara, Soaring City" }, { "count": 1, - "name": "Qarsi Revenant" + "name": "Pact of Negation" }, { "count": 1, - "name": "Preacher of the Schism" + "name": "Phyrexian Metamorph" }, { "count": 1, - "name": "Mirror Entity" + "name": "Polluted Delta" }, { "count": 1, - "name": "Markov Baron" + "name": "Rejuvenating Springs" }, { "count": 1, - "name": "Drana, Liberator of Malakir" + "name": "Rhystic Study" }, { "count": 1, - "name": "Clavileno, First of the Blessed" + "name": "Scalding Tarn" }, { "count": 1, - "name": "Captivating Vampire" + "name": "Sink into Stupor" }, { "count": 1, - "name": "Vengeful Bloodwitch" + "name": "Snapback" }, { "count": 1, - "name": "Vampire Socialite" + "name": "Sol Ring" }, { "count": 1, - "name": "Vampire Gourmand" + "name": "Springleaf Drum" }, { "count": 1, - "name": "Welcoming Vampire" + "name": "Subtlety" }, { "count": 1, - "name": "Falkenrath Noble" + "name": "Swan Song" }, { "count": 1, - "name": "Sanctum Seeker" + "name": "Talisman of Curiosity" }, { "count": 1, - "name": "Champion of Dusk" + "name": "Tarnished Citadel" }, { "count": 1, - "name": "Malakir Bloodwitch" + "name": "The Cabbage Merchant" }, { "count": 1, - "name": "Bartolome del Presidio" + "name": "The Unagi of Kyoshi Island" }, { "count": 1, - "name": "Bloodthrone Vampire" + "name": "Thrasios, Triton Hero" }, { "count": 1, - "name": "Goblin Bombardment" + "name": "Transmute Artifact" }, { "count": 1, - "name": "Skullclamp" + "name": "Trophy Mage" }, { "count": 1, - "name": "Patchwork Banner" + "name": "Tropical Island" }, { "count": 1, - "name": "Deadly Dispute" + "name": "Valley Floodcaller" }, { "count": 1, - "name": "Plumb the Forbidden" + "name": "Veil of Summer" }, { "count": 1, - "name": "Fracture" + "name": "Verdant Catacombs" }, { "count": 1, - "name": "Vindicate" + "name": "Wan Shi Tong, Librarian" }, { "count": 1, - "name": "Despark" + "name": "Waterlogged Grove" }, { "count": 1, - "name": "Invasion of New Capenna" + "name": "Whir of Invention" }, { "count": 1, - "name": "Night's Whisper" + "name": "Windswept Heath" }, { "count": 1, - "name": "Stromkirk Condemned" + "name": "Wooded Foothills" }, { "count": 1, - "name": "Crossway Troublemakers" + "name": "Worldly Tutor" }, { "count": 1, - "name": "Gathering Stone" + "name": "Yavimaya Coast" }, { "count": 1, - "name": "Rite of Oblivion" + "name": "Kinnan, Bonder Prodigy" } ], "sideboard": [] @@ -2436,8 +2448,8 @@ "author": "MTGGoldfish", "colors": [ "R", - "G", - "W" + "W", + "G" ], "tags": [ "metagame" @@ -2445,15 +2457,7 @@ "main": [ { "count": 1, - "name": "Guardian Project" - }, - { - "count": 1, - "name": "Branching Evolution" - }, - { - "count": 1, - "name": "Elemental Teachings" + "name": "Earthbending Lesson" }, { "count": 1, @@ -2461,27 +2465,23 @@ }, { "count": 1, - "name": "Mightform Harmonizer" - }, - { - "count": 1, - "name": "Horizon Explorer" + "name": "Path to Redemption" }, { "count": 1, - "name": "Cracked Earth Technique" + "name": "Earth Kingdom Jailer" }, { "count": 1, - "name": "Bumi, Eclectic Earthbender" + "name": "Kyoshi Battle Fan" }, { "count": 1, - "name": "Haru, Hidden Talent" + "name": "Fated Firepower" }, { "count": 1, - "name": "Mind Stone" + "name": "Yip Yip!" }, { "count": 1, @@ -2489,27 +2489,7 @@ }, { "count": 1, - "name": "Master's Guidance" - }, - { - "count": 1, - "name": "Selesnya Cluestone" - }, - { - "count": 1, - "name": "Swords to Plowshares" - }, - { - "count": 1, - "name": "Genesis Wave" - }, - { - "count": 1, - "name": "Avatar Kyoshi, Earthbender" - }, - { - "count": 1, - "name": "Zuran Orb" + "name": "Haru, Hidden Talent" }, { "count": 1, @@ -2517,123 +2497,123 @@ }, { "count": 1, - "name": "Avenger of Zendikar" + "name": "Fervor" }, { "count": 1, - "name": "Earth Rumble" + "name": "Sandbenders' Storm" }, { "count": 1, - "name": "Boros Cluestone" + "name": "Bitter Work" }, { "count": 1, - "name": "Crystalline Armor" + "name": "Energybending" }, { "count": 1, - "name": "Omnath, Locus of Rage" + "name": "Team Avatar" }, { "count": 1, - "name": "Sazh's Chocobo" + "name": "Leaves from the Vine" }, { "count": 1, - "name": "Moraug, Fury of Akoum" + "name": "Shared Roots" }, { "count": 1, - "name": "Garruk's Uprising" + "name": "Earth Rumble" }, { "count": 1, - "name": "Sabotender" + "name": "Aang's Iceberg" }, { "count": 1, - "name": "Arcane Signet" + "name": "Fancy Footwork" }, { "count": 1, - "name": "Bumi, Unleashed" + "name": "Earthbender Ascension" }, { "count": 1, - "name": "Earthbender Ascension" + "name": "Raucous Audience" }, { "count": 1, - "name": "Ashaya, Soul of the Wild" + "name": "Aang's Journey" }, { "count": 1, - "name": "The Legend of Kyoshi" + "name": "Seismic Sense" }, { "count": 1, - "name": "Gruul Cluestone" + "name": "Rebellious Captives" }, { "count": 1, - "name": "Teleportation Circle" + "name": "Origin of Metalbending" }, { "count": 1, - "name": "Ouroboroid" + "name": "Twin Blades" }, { "count": 1, - "name": "The Boulder, Ready to Rumble" + "name": "Enter the Avatar State" }, { "count": 1, - "name": "Aang's Journey" + "name": "The Earth King" }, { "count": 1, - "name": "Sol Ring" + "name": "Earth Kingdom General" }, { "count": 1, - "name": "Thought Vessel" + "name": "Rocky Rebuke" }, { "count": 1, - "name": "Sandbenders' Storm" + "name": "Pillar Launch" }, { "count": 1, - "name": "Cycle of Renewal" + "name": "The Cave of Two Lovers" }, { "count": 1, - "name": "Rocco, Cabaretti Caterer" + "name": "Rockalanche" }, { "count": 1, - "name": "Earth Kingdom General" + "name": "Bumi, Unleashed" }, { "count": 1, - "name": "Toph, Earthbending Master" + "name": "Cycle of Renewal" }, { "count": 1, - "name": "Terrasymbiosis" + "name": "Bumi, King of Three Trials" }, { "count": 1, - "name": "Bitter Work" + "name": "Toph, Hardheaded Teacher" }, { "count": 1, - "name": "Blossoming Sands" + "name": "Great Divide Guide" }, { "count": 1, - "name": "Clifftop Retreat" + "name": "Razor Rings" }, { "count": 1, @@ -2641,139 +2621,127 @@ }, { "count": 1, - "name": "Rogue's Passage" + "name": "Secret Tunnel" }, { "count": 1, - "name": "Command Tower" + "name": "Kyoshi Village" }, { "count": 1, - "name": "Vibrant Cityscape" + "name": "Sun-Blessed Peak" }, { "count": 1, - "name": "Stomping Ground" + "name": "White Lotus Hideout" }, { "count": 1, - "name": "Secret Tunnel" + "name": "Omashu City" }, { "count": 1, - "name": "Kyoshi Village" - }, - { - "count": 7, - "name": "Forest" - }, - { - "count": 6, - "name": "Plains" - }, - { - "count": 7, - "name": "Mountain" + "name": "Jasmine Dragon Tea Shop" }, { "count": 1, - "name": "Toph, the First Metalbender" + "name": "Rumble Arena" }, { "count": 1, - "name": "Annie Joins Up" + "name": "Treetop Village" }, { "count": 1, - "name": "Sphere Grid" + "name": "Fire Nation Palace" }, { "count": 1, - "name": "The Earth Crystal" + "name": "Abandoned Air Temple" }, { - "count": 1, - "name": "Path to Exile" + "count": 10, + "name": "Mountain" }, { - "count": 1, - "name": "Beast Within" + "count": 10, + "name": "Plains" }, { - "count": 1, - "name": "Darksteel Citadel" + "count": 11, + "name": "Forest" }, { "count": 1, - "name": "Lotus Field" + "name": "Toph, the First Metalbender" }, { "count": 1, - "name": "Fabled Passage" + "name": "Thriving Heath" }, { "count": 1, - "name": "Hour of Revelation" + "name": "Thriving Grove" }, { "count": 1, - "name": "Bootleggers' Stash" + "name": "Thriving Bluff" }, { "count": 1, - "name": "Titania, Protector of Argoth" + "name": "Crystalline Armor" }, { "count": 1, - "name": "Chromatic Lantern" + "name": "Cracked Earth Technique" }, { "count": 1, - "name": "Sleeper Dart" + "name": "The Boulder, Ready to Rumble" }, { "count": 1, - "name": "Tannuk, Memorial Ensign" + "name": "Bosco, Just a Bear" }, { "count": 1, - "name": "Nature's Lore" + "name": "Kyoshi Island Plaza" }, { "count": 1, - "name": "Liquimetal Torque" + "name": "Earthshape" }, { "count": 1, - "name": "Eumidian Terrabotanist" + "name": "Avatar Kyoshi" }, { "count": 1, - "name": "Evolution Sage" + "name": "Bumi, Eclectic Earthbender" }, { "count": 1, - "name": "All Will Be One" + "name": "The Legend of Kyoshi" }, { "count": 1, - "name": "Mountain Valley" + "name": "Tale of Katara and Toph" }, { "count": 1, - "name": "Earthshape" + "name": "Solid Ground" }, { "count": 1, - "name": "Canopy Vista" + "name": "Swampbenders" }, { "count": 1, - "name": "Cinder Glade" + "name": "Master's Guidance" }, { "count": 1, - "name": "Radiant Summit" + "name": "Inspiring Call" } ], "sideboard": [] @@ -3097,12 +3065,11 @@ "sideboard": [] }, { - "name": "Kaalia of the Vast", + "name": "The Scarab God", "author": "MTGGoldfish", "colors": [ - "B", - "W", - "R" + "U", + "B" ], "tags": [ "metagame" @@ -3110,79 +3077,27 @@ "main": [ { "count": 1, - "name": "Ancient Copper Dragon" - }, - { - "count": 1, - "name": "Ancient Tomb" - }, - { - "count": 1, - "name": "Angel of Despair" - }, - { - "count": 1, - "name": "Angel of Serenity" - }, - { - "count": 1, - "name": "Angelic Arbiter" - }, - { - "count": 1, - "name": "Anguished Unmaking" - }, - { - "count": 1, - "name": "Animate Dead" - }, - { - "count": 1, - "name": "Arcane Signet" - }, - { - "count": 1, - "name": "Archfiend of Depravity" + "name": "Lord of the Undead" }, { "count": 1, - "name": "Archfiend of Despair" + "name": "Sunken Ruins" }, { "count": 1, - "name": "Arena of Glory" + "name": "Mind Grind" }, { "count": 1, - "name": "Arid Mesa" - }, - { - "count": 1, - "name": "Aurelia, the Warleader" - }, - { - "count": 1, - "name": "Avacyn, Angel of Hope" - }, - { - "count": 1, - "name": "Badlands" - }, - { - "count": 1, - "name": "Balefire Dragon" - }, - { - "count": 1, - "name": "Blasphemous Act" + "name": "Phyrexian Altar" }, { "count": 1, - "name": "Blood Crypt" + "name": "Thespian's Stage" }, { "count": 1, - "name": "Bloodstained Mire" + "name": "Roaming Throne" }, { "count": 1, @@ -3190,11 +3105,11 @@ }, { "count": 1, - "name": "Caves of Koilos" + "name": "Bloodletter of Aclazotz" }, { "count": 1, - "name": "Clifftop Retreat" + "name": "Demonic Tutor" }, { "count": 1, @@ -3202,314 +3117,291 @@ }, { "count": 1, - "name": "Damn" - }, - { - "count": 1, - "name": "Dark Ritual" - }, - { - "count": 1, - "name": "Deadly Rollick" - }, - { - "count": 1, - "name": "Deflecting Swat" - }, - { - "count": 1, - "name": "Demonic Counsel" - }, - { - "count": 1, - "name": "Demonic Tutor" - }, - { - "count": 1, - "name": "Dragon Tempest" + "name": "Death Baron" }, { "count": 1, - "name": "Dragonskull Summit" + "name": "Diregraf Captain" }, { "count": 1, - "name": "Drakuseth, Maw of Flames" + "name": "Diregraf Colossus" }, { "count": 1, - "name": "Drana and Linvala" + "name": "Drowned Catacomb" }, { "count": 1, - "name": "Enlightened Tutor" + "name": "Ghoulcaller Gisa" }, { "count": 1, - "name": "Exotic Orchard" + "name": "Wonder" }, { "count": 1, - "name": "Farewell" + "name": "The Ghoul, Gunslinger" }, { "count": 1, - "name": "Flawless Maneuver" + "name": "Memory Erosion" }, { "count": 1, - "name": "Gisela, Blade of Goldnight" + "name": "Geralf, the Fleshwright" }, { "count": 1, - "name": "Godless Shrine" + "name": "Stitcher Geralf" }, { "count": 1, - "name": "Grand Abolisher" + "name": "Gravecrawler" }, { "count": 1, - "name": "Hellkite Tyrant" + "name": "Gray Merchant of Asphodel" }, { - "count": 1, - "name": "Isolated Chapel" + "count": 10, + "name": "Island" }, { "count": 1, - "name": "Isshin, Two Heavens as One" + "name": "Liliana of the Dark Realms" }, { "count": 1, - "name": "Jeska's Will" + "name": "Liliana, Dreadhorde General" }, { "count": 1, - "name": "Kaalia, Zenith Seeker" + "name": "Profane Tutor" }, { "count": 1, - "name": "Land Tax" + "name": "Buried Alive" }, { "count": 1, - "name": "Liesa, Forgotten Archangel" + "name": "Singularity Rupture" }, { "count": 1, - "name": "Lightning Greaves" + "name": "Empty the Laboratory" }, { "count": 1, - "name": "Lord of the Void" + "name": "Morphic Pool" }, { "count": 1, - "name": "Marsh Flats" + "name": "Nykthos, Shrine to Nyx" }, { "count": 1, - "name": "Master of Cruelties" + "name": "Nyx Lotus" }, { "count": 1, - "name": "Minas Tirith" + "name": "Phyrexian Tower" }, { "count": 1, - "name": "Mortify" + "name": "Sol Ring" }, { "count": 1, - "name": "Mother of Runes" + "name": "Phenax, God of Deception" }, { "count": 1, - "name": "Mountain" + "name": "Corpse Harvester" }, { - "count": 1, - "name": "Night's Whisper" + "count": 12, + "name": "Swamp" }, { "count": 1, - "name": "Nomad Outpost" + "name": "Undead Warchief" }, { "count": 1, - "name": "Orzhov Signet" + "name": "Unholy Grotto" }, { "count": 1, - "name": "Path to Exile" + "name": "Victimize" }, { "count": 1, - "name": "Plains" + "name": "Zombie Master" }, { "count": 1, - "name": "Plateau" + "name": "Relentless Dead" }, { "count": 1, - "name": "Rakdos, Patron of Chaos" + "name": "Wilhelt, the Rotcleaver" }, { "count": 1, - "name": "Razaketh, the Foulblooded" + "name": "Vizier of the Scorpion" }, { "count": 1, - "name": "Read the Bones" + "name": "Gleaming Overseer" }, { "count": 1, - "name": "Reanimate" + "name": "Cleaver Skaab" }, { "count": 1, - "name": "Reconnaissance" + "name": "Maddening Cacophony" }, { "count": 1, - "name": "Reliquary Tower" + "name": "Blasphemous Edict" }, { "count": 1, - "name": "Reya Dawnbringer" + "name": "Mindcrank" }, { "count": 1, - "name": "Rogue's Passage" + "name": "Bontu's Monument" }, { "count": 1, - "name": "Ruinous Ultimatum" + "name": "Bloodchief Ascension" }, { "count": 1, - "name": "Rune-Scarred Demon" + "name": "Jet Medallion" }, { "count": 1, - "name": "Sacred Foundry" + "name": "Psychic Spiral" }, { "count": 1, - "name": "Savai Triome" + "name": "Vault 12: The Necropolis" }, { "count": 1, - "name": "Scrubland" + "name": "Dread Summons" }, { "count": 1, - "name": "Sephara, Sky's Blade" + "name": "Black Market" }, { "count": 1, - "name": "Serra's Emissary" + "name": "Counterspell" }, { "count": 1, - "name": "Shattered Sanctum" + "name": "Training Grounds" }, { "count": 1, - "name": "Shizo, Death's Storehouse" + "name": "Dreadhorde Invasion" }, { "count": 1, - "name": "Smoldering Marsh" + "name": "Psychic Corrosion" }, { "count": 1, - "name": "Smothering Tithe" + "name": "Grave Pact" }, { "count": 1, - "name": "Sol Ring" + "name": "Necroduality" }, { "count": 1, - "name": "Sundown Pass" + "name": "Shipwreck Marsh" }, { "count": 1, - "name": "Swamp" + "name": "Cavern of Souls" }, { "count": 1, - "name": "Swiftfoot Boots" + "name": "Liliana's Standard Bearer" }, { "count": 1, - "name": "Swords to Plowshares" + "name": "Grim Servant" }, { "count": 1, - "name": "Teferi's Protection" + "name": "Myriad Landscape" }, { "count": 1, - "name": "Temple of Malice" + "name": "Paradox Haze" }, { "count": 1, - "name": "Temple of Silence" + "name": "Zombie Apocalypse" }, { "count": 1, - "name": "Temple of Triumph" + "name": "Rise of the Dread Marn" }, { "count": 1, - "name": "Terminate" + "name": "Ayara, First of Locthwain" }, { "count": 1, - "name": "Terror of the Peaks" + "name": "Tormod, the Desecrator" }, { "count": 1, - "name": "The One Ring" + "name": "Archghoul of Thraben" }, { "count": 1, - "name": "Twinflame Tyrant" + "name": "Syr Konrad, the Grim" }, { "count": 1, - "name": "Urborg, Tomb of Yawgmoth" + "name": "Cryptbreaker" }, { "count": 1, - "name": "Valgavoth, Terror Eater" + "name": "Gisa and Geralf" }, { "count": 1, - "name": "Vampiric Tutor" + "name": "Teval's Judgment" }, { "count": 1, - "name": "Vault of the Archangel" + "name": "Screeching Scorchbeast" }, { "count": 1, - "name": "Vilis, Broker of Blood" + "name": "Rooftop Storm" }, { "count": 1, - "name": "Whispersilk Cloak" - }, + "name": "The Scarab God" + } + ], + "sideboard": [ { "count": 1, - "name": "Kaalia of the Vast" + "name": "Lim-Dul the Necromancer" } - ], - "sideboard": [] + ] } ] } \ No newline at end of file diff --git a/client/public/feeds/mtggoldfish-modern.json b/client/public/feeds/mtggoldfish-modern.json index 453a47abf7..4add7ed3dd 100644 --- a/client/public/feeds/mtggoldfish-modern.json +++ b/client/public/feeds/mtggoldfish-modern.json @@ -5,7 +5,7 @@ "icon": "G", "format": "modern", "version": 1, - "updated": "2026-06-08T00:00:00Z", + "updated": "2026-06-09T00:00:00Z", "source": "https://www.mtggoldfish.com/metagame/modern", "decks": [ { @@ -167,129 +167,129 @@ "main": [ { "count": 4, - "name": "Ruby Medallion" - }, - { - "count": 4, - "name": "Ral, Monsoon Mage" + "name": "Wrenn's Resolve" }, { - "count": 2, - "name": "Artist's Talent" + "count": 1, + "name": "Commercial District" }, { - "count": 2, - "name": "Wooded Foothills" + "count": 1, + "name": "Elegant Parlor" }, { - "count": 2, - "name": "Flashback" + "count": 4, + "name": "Ruby Medallion" }, { "count": 2, - "name": "Scalding Tarn" + "name": "Glimpse the Impossible" }, { "count": 4, - "name": "Pyretic Ritual" + "name": "Ral, Monsoon Mage" }, { "count": 2, - "name": "Valakut Awakening" + "name": "Artist's Talent" }, { "count": 2, - "name": "Glimpse the Impossible" + "name": "Flashback" + }, + { + "count": 4, + "name": "Desperate Ritual" }, { "count": 1, - "name": "Sacred Foundry" + "name": "Grapeshot" }, { - "count": 4, - "name": "Mountain" + "count": 1, + "name": "Gemstone Caverns" }, { "count": 4, - "name": "Reckless Impulse" + "name": "Manamorphose" }, { "count": 2, - "name": "Wish" + "name": "Scalding Tarn" }, { "count": 4, - "name": "Wrenn's Resolve" + "name": "Pyretic Ritual" }, { - "count": 1, - "name": "Gemstone Caverns" + "count": 3, + "name": "Past in Flames" }, { "count": 1, - "name": "Commercial District" + "name": "Sacred Foundry" }, { "count": 2, "name": "Bloodstained Mire" }, { - "count": 1, - "name": "Elegant Parlor" + "count": 2, + "name": "Wooded Foothills" }, { - "count": 3, - "name": "Arid Mesa" + "count": 4, + "name": "Mountain" }, { - "count": 3, - "name": "Past in Flames" + "count": 1, + "name": "Sunbaked Canyon" }, { - "count": 1, - "name": "Grapeshot" + "count": 2, + "name": "Valakut Awakening" }, { - "count": 4, - "name": "Manamorphose" + "count": 3, + "name": "Arid Mesa" }, { - "count": 1, - "name": "Sunbaked Canyon" + "count": 2, + "name": "Wish" }, { "count": 4, - "name": "Desperate Ritual" + "name": "Reckless Impulse" } ], "sideboard": [ { - "count": 1, - "name": "Past in Flames" - }, - { - "count": 4, - "name": "Orim's Chant" + "count": 2, + "name": "Vexing Bauble" }, { "count": 1, - "name": "Grapeshot" + "name": "Empty the Warrens" }, { "count": 1, - "name": "Empty the Warrens" + "name": "Grapeshot" }, { "count": 4, - "name": "Prismatic Ending" + "name": "Orim's Chant" }, { - "count": 2, - "name": "Vexing Bauble" + "count": 1, + "name": "Past in Flames" }, { "count": 2, "name": "Wear // Tear" + }, + { + "count": 4, + "name": "Prismatic Ending" } ] }, @@ -1088,141 +1088,138 @@ ] }, { - "name": "Eldrazi Ramp", + "name": "Grixis Reanimator", "author": "MTGGoldfish", "colors": [ - "R", - "G" + "B", + "U", + "R" ], "tags": [ "metagame" ], "main": [ { - "count": 1, - "name": "Commercial District" + "count": 3, + "name": "Unearth" }, { - "count": 4, - "name": "Sowing Mycospawn" + "count": 1, + "name": "Raucous Theater" }, { - "count": 3, - "name": "Devourer of Destiny" + "count": 1, + "name": "Thundering Falls" }, { - "count": 4, - "name": "Kozilek's Command" + "count": 1, + "name": "Undercity Sewers" }, { "count": 4, - "name": "Malevolent Rumble" + "name": "Emperor of Bones" }, { "count": 4, - "name": "Ugin's Labyrinth" + "name": "Psychic Frog" }, { "count": 1, - "name": "Sire of Seven Deaths" + "name": "Sink into Stupor" }, { - "count": 2, - "name": "Ugin, Eye of the Storms" + "count": 4, + "name": "Abhorrent Oculus" }, { "count": 2, - "name": "Icetill Explorer" + "name": "Quantum Riddler" }, { "count": 4, - "name": "Talisman of Impulse" + "name": "Thoughtseize" }, { "count": 1, - "name": "Ghost Quarter" + "name": "Scalding Tarn" }, { "count": 4, - "name": "Utopia Sprawl" + "name": "Faithless Looting" }, { "count": 1, - "name": "Bojuka Bog" + "name": "Blood Crypt" }, { - "count": 2, - "name": "Ancient Stirrings" + "count": 4, + "name": "Polluted Delta" }, { "count": 4, - "name": "Eldrazi Temple" + "name": "Bloodstained Mire" }, { - "count": 1, - "name": "Cavern of Souls" + "count": 4, + "name": "Fatal Push" }, { "count": 2, - "name": "Stomping Ground" + "name": "Spell Pierce" }, { - "count": 4, - "name": "Wooded Foothills" + "count": 1, + "name": "Steam Vents" }, { "count": 1, - "name": "Sanctum of Ugin" + "name": "Watery Grave" }, { - "count": 2, - "name": "Kozilek's Return" + "count": 1, + "name": "Island" }, { - "count": 1, - "name": "World Breaker" + "count": 2, + "name": "Swamp" }, { - "count": 3, - "name": "Emrakul, the Promised End" + "count": 4, + "name": "Archon of Cruelty" }, { - "count": 3, - "name": "Forest" + "count": 4, + "name": "Persist" }, { "count": 2, - "name": "Unholy Heat" + "name": "Otherworldly Gaze" } ], "sideboard": [ { - "count": 2, - "name": "Fade from History" + "count": 4, + "name": "Consign to Memory" }, { "count": 2, - "name": "The Stone Brain" - }, - { - "count": 3, "name": "Vexing Bauble" }, { "count": 2, - "name": "Trinisphere" + "name": "Pyroclasm" }, { - "count": 2, - "name": "Nature's Claim" + "count": 3, + "name": "Leyline of the Void" }, { "count": 2, - "name": "Blasphemous Act" + "name": "Damping Sphere" }, { "count": 2, - "name": "Grafdigger's Cage" + "name": "Mystical Dispute" } ] }, @@ -1370,138 +1367,141 @@ ] }, { - "name": "Grixis Reanimator", + "name": "Eldrazi Ramp", "author": "MTGGoldfish", "colors": [ - "B", - "U", - "R" + "R", + "G" ], "tags": [ "metagame" ], "main": [ { - "count": 3, - "name": "Unearth" + "count": 1, + "name": "Commercial District" }, { - "count": 1, - "name": "Raucous Theater" + "count": 4, + "name": "Sowing Mycospawn" }, { - "count": 1, - "name": "Thundering Falls" + "count": 3, + "name": "Devourer of Destiny" }, { - "count": 1, - "name": "Undercity Sewers" + "count": 4, + "name": "Kozilek's Command" }, { "count": 4, - "name": "Emperor of Bones" + "name": "Malevolent Rumble" }, { "count": 4, - "name": "Psychic Frog" + "name": "Ugin's Labyrinth" }, { "count": 1, - "name": "Sink into Stupor" + "name": "Sire of Seven Deaths" }, { - "count": 4, - "name": "Abhorrent Oculus" + "count": 2, + "name": "Ugin, Eye of the Storms" }, { "count": 2, - "name": "Quantum Riddler" + "name": "Icetill Explorer" }, { "count": 4, - "name": "Thoughtseize" + "name": "Talisman of Impulse" }, { "count": 1, - "name": "Scalding Tarn" + "name": "Ghost Quarter" }, { "count": 4, - "name": "Faithless Looting" + "name": "Utopia Sprawl" }, { "count": 1, - "name": "Blood Crypt" + "name": "Bojuka Bog" }, { - "count": 4, - "name": "Polluted Delta" + "count": 2, + "name": "Ancient Stirrings" }, { "count": 4, - "name": "Bloodstained Mire" + "name": "Eldrazi Temple" }, { - "count": 4, - "name": "Fatal Push" + "count": 1, + "name": "Cavern of Souls" }, { "count": 2, - "name": "Spell Pierce" + "name": "Stomping Ground" }, { - "count": 1, - "name": "Steam Vents" + "count": 4, + "name": "Wooded Foothills" }, { "count": 1, - "name": "Watery Grave" + "name": "Sanctum of Ugin" }, { - "count": 1, - "name": "Island" + "count": 2, + "name": "Kozilek's Return" }, { - "count": 2, - "name": "Swamp" + "count": 1, + "name": "World Breaker" }, { - "count": 4, - "name": "Archon of Cruelty" + "count": 3, + "name": "Emrakul, the Promised End" }, { - "count": 4, - "name": "Persist" + "count": 3, + "name": "Forest" }, { "count": 2, - "name": "Otherworldly Gaze" + "name": "Unholy Heat" } ], "sideboard": [ { - "count": 4, - "name": "Consign to Memory" + "count": 2, + "name": "Fade from History" }, { "count": 2, + "name": "The Stone Brain" + }, + { + "count": 3, "name": "Vexing Bauble" }, { "count": 2, - "name": "Pyroclasm" + "name": "Trinisphere" }, { - "count": 3, - "name": "Leyline of the Void" + "count": 2, + "name": "Nature's Claim" }, { "count": 2, - "name": "Damping Sphere" + "name": "Blasphemous Act" }, { "count": 2, - "name": "Mystical Dispute" + "name": "Grafdigger's Cage" } ] } diff --git a/client/public/feeds/mtggoldfish-pioneer.json b/client/public/feeds/mtggoldfish-pioneer.json index f2a1cbae24..d9d9fd37a4 100644 --- a/client/public/feeds/mtggoldfish-pioneer.json +++ b/client/public/feeds/mtggoldfish-pioneer.json @@ -5,7 +5,7 @@ "icon": "G", "format": "pioneer", "version": 1, - "updated": "2026-06-08T00:00:00Z", + "updated": "2026-06-09T00:00:00Z", "source": "https://www.mtggoldfish.com/metagame/pioneer", "decks": [ { @@ -803,288 +803,288 @@ ] }, { - "name": "Abzan Greasefang", + "name": "Mono-Red Prowess", "author": "MTGGoldfish", "colors": [ - "G", - "W", - "B" + "R" ], "tags": [ "metagame" ], "main": [ - { - "count": 3, - "name": "Bitter Triumph" - }, - { - "count": 1, - "name": "Forest" - }, { "count": 4, - "name": "Temple Garden" + "name": "Burst Lightning" }, { "count": 1, - "name": "Takenuma, Abandoned Mire" + "name": "Den of the Bugbear" }, { "count": 4, - "name": "Cache Grab" - }, - { - "count": 1, - "name": "Swamp" + "name": "Emberheart Challenger" }, { - "count": 1, - "name": "Plains" + "count": 4, + "name": "Kumano Faces Kakkazan" }, { - "count": 2, - "name": "Overlord of the Balemurk" + "count": 3, + "name": "Robber of the Rich" }, { "count": 4, - "name": "Esika's Chariot" - }, - { - "count": 1, - "name": "Lush Portico" + "name": "Monastery Swiftspear" }, { "count": 4, - "name": "Greasefang, Okiba Boss" + "name": "Monstrous Rage" }, { "count": 1, - "name": "Godless Shrine" + "name": "Mountain" }, { - "count": 1, - "name": "Emptiness" + "count": 2, + "name": "Ramunap Ruins" }, { "count": 4, - "name": "Parhelion II" + "name": "Reckless Rage" }, { - "count": 3, - "name": "Darkbore Pathway" + "count": 2, + "name": "Screaming Nemesis" }, { "count": 1, - "name": "Mana Confluence" + "name": "Sokenzan, Crucible of Defiance" }, { "count": 3, - "name": "Concealed Courtyard" - }, - { - "count": 1, - "name": "Boseiju, Who Endures" + "name": "Soul-Scar Mage" }, { "count": 3, - "name": "Blooming Marsh" + "name": "Sunspine Lynx" }, { "count": 1, - "name": "Thundering Broodwagon" + "name": "Mountain" }, { "count": 3, - "name": "Witherbloom Command" - }, - { - "count": 4, - "name": "Thoughtseize" + "name": "Mountain" }, { "count": 1, - "name": "Shadowy Backstreet" - }, - { - "count": 2, - "name": "Formidable Speaker" + "name": "Mountain" }, { - "count": 1, - "name": "Sheoldred, the Apocalypse" + "count": 9, + "name": "Mountain" }, { - "count": 1, - "name": "Skysovereign, Consul Flagship" + "count": 2, + "name": "Screaming Nemesis" }, { "count": 4, - "name": "Raffine's Informant" + "name": "Mutavault" } ], "sideboard": [ { - "count": 1, - "name": "Damping Sphere" - }, - { - "count": 1, - "name": "Ashiok, Dream Render" - }, - { - "count": 3, - "name": "Fatal Push" + "count": 2, + "name": "Flowstone Infusion" }, { "count": 2, - "name": "Duress" + "name": "Scorching Shot" }, { - "count": 1, - "name": "Knight of Autumn" + "count": 3, + "name": "Pyroclasm" }, { "count": 2, - "name": "Abrupt Decay" + "name": "Maelstrom Artisan" }, { - "count": 1, - "name": "Tear Asunder" + "count": 4, + "name": "Magebane Lizard" }, { "count": 1, - "name": "Unlicensed Hearse" + "name": "The Legend of Roku" }, { "count": 1, - "name": "Vanishing Verse" - }, - { - "count": 2, - "name": "Temporary Lockdown" + "name": "Sunspine Lynx" } ] }, { - "name": "Mono-Red Prowess", + "name": "Abzan Greasefang", "author": "MTGGoldfish", "colors": [ - "R" + "G", + "W", + "B" ], "tags": [ "metagame" ], "main": [ { - "count": 4, - "name": "Burst Lightning" + "count": 3, + "name": "Bitter Triumph" }, { "count": 1, - "name": "Den of the Bugbear" + "name": "Forest" }, { "count": 4, - "name": "Emberheart Challenger" + "name": "Temple Garden" + }, + { + "count": 1, + "name": "Takenuma, Abandoned Mire" }, { "count": 4, - "name": "Kumano Faces Kakkazan" + "name": "Cache Grab" }, { - "count": 3, - "name": "Robber of the Rich" + "count": 1, + "name": "Swamp" + }, + { + "count": 1, + "name": "Plains" + }, + { + "count": 2, + "name": "Overlord of the Balemurk" }, { "count": 4, - "name": "Monastery Swiftspear" + "name": "Esika's Chariot" + }, + { + "count": 1, + "name": "Lush Portico" }, { "count": 4, - "name": "Monstrous Rage" + "name": "Greasefang, Okiba Boss" }, { "count": 1, - "name": "Mountain" + "name": "Godless Shrine" }, { - "count": 2, - "name": "Ramunap Ruins" + "count": 1, + "name": "Emptiness" }, { "count": 4, - "name": "Reckless Rage" + "name": "Parhelion II" }, { - "count": 2, - "name": "Screaming Nemesis" + "count": 3, + "name": "Darkbore Pathway" }, { "count": 1, - "name": "Sokenzan, Crucible of Defiance" + "name": "Mana Confluence" }, { "count": 3, - "name": "Soul-Scar Mage" + "name": "Concealed Courtyard" + }, + { + "count": 1, + "name": "Boseiju, Who Endures" }, { "count": 3, - "name": "Sunspine Lynx" + "name": "Blooming Marsh" }, { "count": 1, - "name": "Mountain" + "name": "Thundering Broodwagon" }, { "count": 3, - "name": "Mountain" + "name": "Witherbloom Command" }, { - "count": 1, - "name": "Mountain" + "count": 4, + "name": "Thoughtseize" }, { - "count": 9, - "name": "Mountain" + "count": 1, + "name": "Shadowy Backstreet" }, { "count": 2, - "name": "Screaming Nemesis" + "name": "Formidable Speaker" + }, + { + "count": 1, + "name": "Sheoldred, the Apocalypse" + }, + { + "count": 1, + "name": "Skysovereign, Consul Flagship" }, { "count": 4, - "name": "Mutavault" + "name": "Raffine's Informant" } ], "sideboard": [ { - "count": 2, - "name": "Flowstone Infusion" + "count": 1, + "name": "Damping Sphere" }, { - "count": 2, - "name": "Scorching Shot" + "count": 1, + "name": "Ashiok, Dream Render" }, { "count": 3, - "name": "Pyroclasm" + "name": "Fatal Push" }, { "count": 2, - "name": "Maelstrom Artisan" + "name": "Duress" }, { - "count": 4, - "name": "Magebane Lizard" + "count": 1, + "name": "Knight of Autumn" + }, + { + "count": 2, + "name": "Abrupt Decay" }, { "count": 1, - "name": "The Legend of Roku" + "name": "Tear Asunder" }, { "count": 1, - "name": "Sunspine Lynx" + "name": "Unlicensed Hearse" + }, + { + "count": 1, + "name": "Vanishing Verse" + }, + { + "count": 2, + "name": "Temporary Lockdown" } ] }, @@ -1391,12 +1391,11 @@ ] }, { - "name": "Sultai Scapeshift", + "name": "Selesnya Angels", "author": "MTGGoldfish", "colors": [ - "B", - "G", - "U" + "W", + "G" ], "tags": [ "metagame" @@ -1404,169 +1403,97 @@ "main": [ { "count": 4, - "name": "Aftermath Analyst" - }, - { - "count": 2, - "name": "Swamp" - }, - { - "count": 1, - "name": "Emeritus of Ideation" - }, - { - "count": 1, - "name": "Takenuma, Abandoned Mire" - }, - { - "count": 4, - "name": "Lotus Field" + "name": "Bishop of Wings" }, { "count": 4, - "name": "Arboreal Grazer" + "name": "Brushland" }, { "count": 4, - "name": "Stock Up" - }, - { - "count": 1, - "name": "Boseiju, Who Endures" - }, - { - "count": 3, - "name": "Port of Karfell" - }, - { - "count": 1, - "name": "Ipnu Rivulet" + "name": "Collected Company" }, { "count": 3, - "name": "Crumbling Vestige" + "name": "Elvish Mystic" }, { "count": 4, - "name": "Spelunking" + "name": "Razorverge Thicket" }, { "count": 4, - "name": "Forest" - }, - { - "count": 1, - "name": "Thassa's Oracle" + "name": "Giada, Font of Hope" }, { "count": 4, - "name": "Scapeshift" + "name": "Hushwood Verge" }, { "count": 1, - "name": "Island" + "name": "Inspiring Overseer" }, { "count": 4, - "name": "Breeding Pool" + "name": "Kayla's Reconstruction" }, { - "count": 4, - "name": "Fabled Passage" - }, - { - "count": 4, - "name": "The Wandering Minstrel" - }, - { - "count": 4, - "name": "Hedge Maze" - }, - { - "count": 1, - "name": "Mirrorpool" - }, - { - "count": 1, - "name": "Otawara, Soaring City" + "count": 3, + "name": "Llanowar Elves" }, { "count": 1, - "name": "Emeritus of Abundance" + "name": "Plains" }, { - "count": 1, - "name": "Arid Archway" + "count": 4, + "name": "Resplendent Angel" }, { "count": 4, - "name": "Growth Spiral" + "name": "Temple Garden" }, { "count": 4, - "name": "Lumra, Bellow of the Woods" + "name": "Righteous Valkyrie" }, { "count": 3, - "name": "Castle Garenbrig" - }, - { - "count": 4, - "name": "Formidable Speaker" + "name": "Skyclave Apparition" }, { - "count": 1, - "name": "Vibrance" + "count": 2, + "name": "Plains" }, { - "count": 1, - "name": "Wistfulness" + "count": 4, + "name": "Plains" }, { - "count": 1, - "name": "Cavern of Souls" + "count": 3, + "name": "Enduring Innocence" } ], "sideboard": [ { - "count": 2, - "name": "Aether Gust" - }, - { - "count": 1, - "name": "Tranquil Frillback" - }, - { - "count": 1, - "name": "Yorion, Sky Nomad" - }, - { - "count": 1, - "name": "Wistfulness" + "count": 3, + "name": "Archon of Emeria" }, { "count": 3, - "name": "Culling Ritual" + "name": "High Noon" }, { "count": 2, - "name": "Unable to Scream" - }, - { - "count": 1, - "name": "Unable to Scream" - }, - { - "count": 1, - "name": "Bonny Pall, Clearcutter" + "name": "Get Lost" }, { - "count": 1, - "name": "Emrakul, the Promised End" + "count": 4, + "name": "Seam Rip" }, { - "count": 2, - "name": "Languish" + "count": 3, + "name": "Rest in Peace" } ] } diff --git a/client/public/feeds/mtggoldfish-standard.json b/client/public/feeds/mtggoldfish-standard.json index 78868d1267..65533b6348 100644 --- a/client/public/feeds/mtggoldfish-standard.json +++ b/client/public/feeds/mtggoldfish-standard.json @@ -5,15 +5,15 @@ "icon": "G", "format": "standard", "version": 1, - "updated": "2026-06-08T00:00:00Z", + "updated": "2026-06-09T00:00:00Z", "source": "https://www.mtggoldfish.com/metagame/standard", "decks": [ { "name": "Selesnya Landfall", "author": "MTGGoldfish", "colors": [ - "W", - "G" + "G", + "W" ], "tags": [ "metagame" @@ -24,11 +24,11 @@ "name": "Erode" }, { - "count": 1, + "count": 2, "name": "Surrak, Elusive Hunter" }, { - "count": 2, + "count": 1, "name": "Lumbering Worldwagon" }, { @@ -39,10 +39,6 @@ "count": 4, "name": "Earthbender Ascension" }, - { - "count": 2, - "name": "Temple Garden" - }, { "count": 7, "name": "Forest" @@ -51,6 +47,10 @@ "count": 4, "name": "Sazh's Chocobo" }, + { + "count": 1, + "name": "Temple Garden" + }, { "count": 2, "name": "Icetill Explorer" @@ -67,14 +67,14 @@ "count": 4, "name": "Badgermole Cub" }, - { - "count": 4, - "name": "Fabled Passage" - }, { "count": 2, "name": "Dyadrine, Synthesis Amalgam" }, + { + "count": 4, + "name": "Fabled Passage" + }, { "count": 4, "name": "Hushwood Verge" @@ -84,7 +84,7 @@ "name": "Mightform Harmonizer" }, { - "count": 3, + "count": 4, "name": "Escape Tunnel" }, { @@ -111,11 +111,11 @@ }, { "count": 1, - "name": "Restoration Magic" + "name": "Kutzil, Malamet Exemplar" }, { "count": 1, - "name": "Kutzil, Malamet Exemplar" + "name": "Ba Sing Se" }, { "count": 2, @@ -140,7 +140,6 @@ "author": "MTGGoldfish", "colors": [ "U", - "G", "R" ], "tags": [ @@ -148,21 +147,13 @@ ], "main": [ { - "count": 6, + "count": 7, "name": "Island" }, - { - "count": 1, - "name": "Willowrush Verge" - }, { "count": 4, "name": "Opt" }, - { - "count": 4, - "name": "Elusive Otter" - }, { "count": 4, "name": "Stormchaser's Talent" @@ -180,32 +171,28 @@ "name": "Colorstorm Stallion" }, { - "count": 1, - "name": "Breeding Pool" + "count": 3, + "name": "Secret Identity" }, { - "count": 4, + "count": 3, "name": "Flow State" }, { "count": 1, - "name": "Impractical Joke" + "name": "Wild Ride" }, { "count": 2, - "name": "Secret Identity" - }, - { - "count": 1, - "name": "Wild Ride" + "name": "Multiversal Passage" }, { "count": 1, - "name": "Multiversal Passage" + "name": "Into the Flood Maw" }, { - "count": 1, - "name": "Spell Pierce" + "count": 3, + "name": "Drake Hatcher" }, { "count": 4, @@ -223,6 +210,10 @@ "count": 4, "name": "Sleight of Hand" }, + { + "count": 2, + "name": "Vibrant Outburst" + }, { "count": 4, "name": "Slickshot Show-Off" @@ -238,40 +229,36 @@ "name": "Slagstorm" }, { - "count": 3, + "count": 2, "name": "Get Out" }, { - "count": 1, - "name": "Sear" - }, - { - "count": 1, - "name": "Ghost Vacuum" - }, - { - "count": 1, + "count": 2, "name": "Roaring Furnace // Steaming Sauna" }, { - "count": 2, + "count": 1, "name": "Into the Flood Maw" }, { "count": 2, - "name": "Ral, Crackling Wit" + "name": "Spell Pierce" }, { "count": 1, - "name": "Soul-Guide Lantern" + "name": "Drake Hatcher" }, { "count": 1, - "name": "Spell Pierce" + "name": "Ral, Crackling Wit" }, { - "count": 1, - "name": "Broadside Barrage" + "count": 2, + "name": "Soul-Guide Lantern" + }, + { + "count": 2, + "name": "Sunspine Lynx" } ] }, @@ -739,260 +726,260 @@ ] }, { - "name": "Izzet Lessons", + "name": "Dimir Excruciator", "author": "MTGGoldfish", "colors": [ - "U", - "R" + "B", + "U" ], "tags": [ "metagame" ], "main": [ { - "count": 6, - "name": "Island" + "count": 3, + "name": "Doomsday Excruciator" }, { "count": 4, - "name": "Gran-Gran" + "name": "Restless Reef" }, { - "count": 2, - "name": "Three Steps Ahead" + "count": 4, + "name": "Deceit" }, { "count": 3, - "name": "Consult the Star Charts" + "name": "Duress" }, { - "count": 2, - "name": "Agna Qel'a" + "count": 9, + "name": "Swamp" }, { "count": 2, - "name": "Boomerang Basics" + "name": "Winternight Stories" }, { - "count": 4, - "name": "Combustion Technique" + "count": 3, + "name": "Bitter Triumph" }, { - "count": 2, - "name": "Mountain" + "count": 4, + "name": "Superior Spider-Man" }, { "count": 2, - "name": "Iroh's Demonstration" + "name": "Cavern of Souls" }, { - "count": 4, - "name": "Firebending Lesson" + "count": 1, + "name": "Multiversal Passage" }, { - "count": 4, - "name": "Monument to Endurance" + "count": 3, + "name": "Day of Black Sun" }, { - "count": 1, - "name": "Spell Pierce" + "count": 2, + "name": "Undercity Sewers" }, { - "count": 2, - "name": "Abandon Attachments" + "count": 1, + "name": "Deadly Cover-Up" }, { "count": 4, - "name": "Accumulate Wisdom" + "name": "Requiting Hex" }, { - "count": 2, - "name": "It'll Quench Ya!" + "count": 3, + "name": "Stock Up" }, { "count": 4, - "name": "Riverpyre Verge" + "name": "Watery Grave" }, { - "count": 4, - "name": "Artist's Talent" + "count": 1, + "name": "Strategic Betrayal" }, { "count": 4, - "name": "Steam Vents" + "name": "Gloomlake Verge" }, { - "count": 4, - "name": "Spirebluff Canal" + "count": 1, + "name": "Emeritus of Ideation" + }, + { + "count": 2, + "name": "Insatiable Avarice" } ], "sideboard": [ { - "count": 2, - "name": "Stormchaser's Talent" - }, - { - "count": 2, - "name": "Negate" + "count": 1, + "name": "Strategic Betrayal" }, { "count": 1, - "name": "Boomerang Basics" + "name": "Decorum Dissertation" }, { "count": 2, - "name": "Ral, Crackling Wit" + "name": "Qarsi Revenant" }, { - "count": 2, - "name": "Quantum Riddler" + "count": 1, + "name": "Ghost Vacuum" }, { "count": 3, - "name": "Flashfreeze" + "name": "Oildeep Gearhulk" }, { "count": 1, - "name": "Spell Pierce" + "name": "Duress" }, { - "count": 1, - "name": "Spell Snare" + "count": 2, + "name": "Quantum Riddler" }, { - "count": 1, - "name": "Broadside Barrage" + "count": 2, + "name": "Flashfreeze" + }, + { + "count": 2, + "name": "Sunderflock" } ] }, { - "name": "Dimir Excruciator", + "name": "Izzet Lessons", "author": "MTGGoldfish", "colors": [ - "B", - "U" + "U", + "R" ], "tags": [ "metagame" ], "main": [ { - "count": 3, - "name": "Doomsday Excruciator" + "count": 6, + "name": "Island" }, { "count": 4, - "name": "Restless Reef" + "name": "Gran-Gran" }, { - "count": 4, - "name": "Deceit" + "count": 2, + "name": "Three Steps Ahead" }, { "count": 3, - "name": "Duress" - }, - { - "count": 9, - "name": "Swamp" + "name": "Consult the Star Charts" }, { "count": 2, - "name": "Winternight Stories" + "name": "Agna Qel'a" }, { - "count": 3, - "name": "Bitter Triumph" + "count": 2, + "name": "Boomerang Basics" }, { "count": 4, - "name": "Superior Spider-Man" + "name": "Combustion Technique" }, { "count": 2, - "name": "Cavern of Souls" + "name": "Mountain" }, { - "count": 1, - "name": "Multiversal Passage" + "count": 2, + "name": "Iroh's Demonstration" }, { - "count": 3, - "name": "Day of Black Sun" + "count": 4, + "name": "Firebending Lesson" }, { - "count": 2, - "name": "Undercity Sewers" + "count": 4, + "name": "Monument to Endurance" }, { "count": 1, - "name": "Deadly Cover-Up" + "name": "Spell Pierce" }, { - "count": 4, - "name": "Requiting Hex" + "count": 2, + "name": "Abandon Attachments" }, { - "count": 3, - "name": "Stock Up" + "count": 4, + "name": "Accumulate Wisdom" }, { - "count": 4, - "name": "Watery Grave" + "count": 2, + "name": "It'll Quench Ya!" }, { - "count": 1, - "name": "Strategic Betrayal" + "count": 4, + "name": "Riverpyre Verge" }, { "count": 4, - "name": "Gloomlake Verge" + "name": "Artist's Talent" }, { - "count": 1, - "name": "Emeritus of Ideation" + "count": 4, + "name": "Steam Vents" }, { - "count": 2, - "name": "Insatiable Avarice" + "count": 4, + "name": "Spirebluff Canal" } ], "sideboard": [ { - "count": 1, - "name": "Strategic Betrayal" + "count": 2, + "name": "Stormchaser's Talent" + }, + { + "count": 2, + "name": "Negate" }, { "count": 1, - "name": "Decorum Dissertation" + "name": "Boomerang Basics" }, { "count": 2, - "name": "Qarsi Revenant" + "name": "Ral, Crackling Wit" }, { - "count": 1, - "name": "Ghost Vacuum" + "count": 2, + "name": "Quantum Riddler" }, { "count": 3, - "name": "Oildeep Gearhulk" + "name": "Flashfreeze" }, { "count": 1, - "name": "Duress" - }, - { - "count": 2, - "name": "Quantum Riddler" + "name": "Spell Pierce" }, { - "count": 2, - "name": "Flashfreeze" + "count": 1, + "name": "Spell Snare" }, { - "count": 2, - "name": "Sunderflock" + "count": 1, + "name": "Broadside Barrage" } ] }, diff --git a/client/src-tauri/Cargo.toml b/client/src-tauri/Cargo.toml index 5f8dc06d39..833fd953d0 100644 --- a/client/src-tauri/Cargo.toml +++ b/client/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "phase-tauri" -version = "0.1.35" +version = "0.1.36" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/client/src-tauri/tauri.conf.json b/client/src-tauri/tauri.conf.json index 1fc1442fcf..dddd3ac273 100644 --- a/client/src-tauri/tauri.conf.json +++ b/client/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/config.schema.json", "productName": "Phase", - "version": "0.1.35", + "version": "0.1.36", "identifier": "rs.phase.app", "build": { "frontendDist": "../dist", diff --git a/client/src/adapter/__tests__/draftPodAdapter.test.ts b/client/src/adapter/__tests__/draftPodAdapter.test.ts index 9c12fe1ba0..f33ef87a01 100644 --- a/client/src/adapter/__tests__/draftPodAdapter.test.ts +++ b/client/src/adapter/__tests__/draftPodAdapter.test.ts @@ -14,6 +14,7 @@ import type { DraftPodHostEvent } from "../draftPodHostAdapter"; import { DraftPodGuestAdapter } from "../draftPodGuestAdapter"; import type { DraftPodGuestEvent } from "../draftPodGuestAdapter"; import type { DraftPlayerView } from "../draft-adapter"; +import { loadDraftHostSession } from "../../services/draftPersistence"; // ── Mocks ────────────────────────────────────────────────────────────── @@ -41,7 +42,7 @@ const mockHostRequestPause = vi.fn(); const mockHostRequestResume = vi.fn(); const mockHostDispose = vi.fn(); const mockHostTerminateDraft = vi.fn(async () => {}); -const mockHostRestoreFromPersisted = vi.fn(async () => null); +const mockHostRestoreFromPersisted = vi.fn(async (): Promise => null); vi.mock("../p2p-draft-host", () => ({ P2PDraftHost: vi.fn().mockImplementation(() => ({ @@ -220,6 +221,40 @@ describe("DraftPodHostAdapter", () => { expect(mockHostStartDraft).toHaveBeenCalledOnce(); }); + it("restores MatchInProgress host sessions without falling back to drafting", async () => { + vi.mocked(loadDraftHostSession).mockResolvedValue({ + persistenceId: "draft-1", + roomCode: "ABCDE", + kind: "Premier", + podSize: 8, + hostDisplayName: "Host", + tournamentFormat: "Swiss", + podPolicy: "Competitive", + seatTokens: { 0: "host" }, + seatNames: { 0: "Host" }, + kickedTokens: [], + draftStarted: true, + draftCode: "ABCDE", + draftSessionJson: "{}", + setPoolJson: "{}", + }); + const restoredView = mockView("MatchInProgress"); + mockHostRestoreFromPersisted.mockResolvedValue(restoredView); + + await adapter.initialize({ + setPoolJson: "{}", + kind: "Premier", + podSize: 8, + hostDisplayName: "Host", + tournamentFormat: "Swiss", + podPolicy: "Competitive", + persistenceId: "draft-1", + }); + + expect(adapter.status).toBe("matchInProgress"); + expect(events).toContainEqual({ type: "viewUpdated", view: restoredView }); + }); + it("delegates submitPick and returns view", async () => { await adapter.initialize({ setPoolJson: "{}", @@ -307,6 +342,21 @@ describe("DraftPodHostAdapter", () => { hostEventHandler({ type: "allDecksSubmitted" }); expect(adapter.status).toBe("pairing"); + + hostEventHandler({ + type: "bo3ChoosePlayDraw", + matchId: "match-1", + gameNumber: 2, + score: { p0_wins: 0, p1_wins: 1, draws: 0 }, + timerMs: 10_000, + }); + expect(events).toContainEqual({ + type: "bo3ChoosePlayDraw", + matchId: "match-1", + gameNumber: 2, + score: { p0_wins: 0, p1_wins: 1, draws: 0 }, + timerMs: 10_000, + }); }); it("cleans up on dispose", async () => { diff --git a/client/src/adapter/draft-adapter.ts b/client/src/adapter/draft-adapter.ts index 4c7b341215..863a1a16bc 100644 --- a/client/src/adapter/draft-adapter.ts +++ b/client/src/adapter/draft-adapter.ts @@ -299,9 +299,9 @@ export class DraftAdapter { ); } - async replaceSeatWithBot(seat: number): Promise { + async replaceSeatWithBot(seat: number, name?: string): Promise { return this.applyActionAndGetHostView( - JSON.stringify({ type: "ReplaceSeatWithBot", data: { seat } }), + JSON.stringify({ type: "ReplaceSeatWithBot", data: { seat, name } }), ); } } diff --git a/client/src/adapter/draftPodGuestAdapter.ts b/client/src/adapter/draftPodGuestAdapter.ts index c9f4bd81a9..8fa0bb2156 100644 --- a/client/src/adapter/draftPodGuestAdapter.ts +++ b/client/src/adapter/draftPodGuestAdapter.ts @@ -300,12 +300,21 @@ export class DraftPodGuestAdapter { case "Deckbuilding": if (this._status !== "deckbuilding") this.setStatus("deckbuilding"); break; + case "Pairing": + case "RoundComplete": + break; case "MatchInProgress": if (this._status !== "matchInProgress") this.setStatus("matchInProgress"); break; case "Complete": if (this._status !== "complete") this.setStatus("complete"); break; + case "Lobby": + if (this._status !== "lobby") this.setStatus("lobby"); + break; + case "Paused": + case "Abandoned": + break; } } diff --git a/client/src/adapter/draftPodHostAdapter.ts b/client/src/adapter/draftPodHostAdapter.ts index 0429eb0c5f..bf0bf89cb0 100644 --- a/client/src/adapter/draftPodHostAdapter.ts +++ b/client/src/adapter/draftPodHostAdapter.ts @@ -44,6 +44,8 @@ export type DraftPodHostEvent = | { type: "draftComplete" } | { type: "deckSubmitted"; seatIndex: number } | { type: "allDecksSubmitted" } + | { type: "draftPaused"; reason: string } + | { type: "draftResumed" } | { type: "seatJoined"; seatIndex: number; displayName: string } | { type: "seatReconnected"; seatIndex: number } | { type: "seatDisconnected"; seatIndex: number } @@ -53,6 +55,22 @@ export type DraftPodHostEvent = | { type: "matchResultReceived"; matchId: string; winnerSeat: number | null } | { type: "roundAdvanced"; newRound: number } | { type: "timerExpired" } + | { + type: "bo3SideboardPrompt"; + matchId: string; + gameNumber: number; + score: MatchScore; + loserSeat: number | null; + timerMs: number; + } + | { + type: "bo3ChoosePlayDraw"; + matchId: string; + gameNumber: number; + score: MatchScore; + timerMs: number; + } + | { type: "bo3GameStart"; matchId: string; gameNumber: number; firstPlayerSeat: number } | { type: "bo3SideboardPromptSent"; matchId: string } | { type: "bo3BothSideboardsSubmitted"; matchId: string } | { type: "bo3GameStarted"; matchId: string; gameNumber: number } @@ -60,6 +78,28 @@ export type DraftPodHostEvent = type DraftPodHostEventListener = (event: DraftPodHostEvent) => void; +function hostStatusForView(view: DraftPlayerView): DraftPodHostStatus { + switch (view.status) { + case "Lobby": + return "lobby"; + case "Drafting": + case "Paused": + return "drafting"; + case "Deckbuilding": + return "deckbuilding"; + case "Pairing": + return "pairing"; + case "MatchInProgress": + return "matchInProgress"; + case "RoundComplete": + return "roundComplete"; + case "Complete": + return "complete"; + case "Abandoned": + return "error"; + } +} + export interface DraftPodHostConfig { setPoolJson: string; kind: "Premier" | "Traditional"; @@ -174,9 +214,7 @@ export class DraftPodHostAdapter { if (persisted) { const view = await host.restoreFromPersisted(persisted); if (view) { - this.setStatus( - view.status === "Deckbuilding" ? "deckbuilding" : "drafting", - ); + this.setStatus(hostStatusForView(view)); this.emit({ type: "viewUpdated", view }); } } @@ -257,6 +295,12 @@ export class DraftPodHostAdapter { this.setStatus("pairing"); this.emit({ type: "allDecksSubmitted" }); break; + case "draftPaused": + this.emit({ type: "draftPaused", reason: event.reason }); + break; + case "draftResumed": + this.emit({ type: "draftResumed" }); + break; case "error": this.emit({ type: "error", message: event.message }); break; @@ -290,6 +334,33 @@ export class DraftPodHostAdapter { case "bo3GameStarted": this.emit({ type: "bo3GameStarted", matchId: event.matchId, gameNumber: event.gameNumber }); break; + case "bo3SideboardPrompt": + this.emit({ + type: "bo3SideboardPrompt", + matchId: event.matchId, + gameNumber: event.gameNumber, + score: event.score, + loserSeat: event.loserSeat, + timerMs: event.timerMs, + }); + break; + case "bo3ChoosePlayDraw": + this.emit({ + type: "bo3ChoosePlayDraw", + matchId: event.matchId, + gameNumber: event.gameNumber, + score: event.score, + timerMs: event.timerMs, + }); + break; + case "bo3GameStart": + this.emit({ + type: "bo3GameStart", + matchId: event.matchId, + gameNumber: event.gameNumber, + firstPlayerSeat: event.firstPlayerSeat, + }); + break; } } diff --git a/client/src/adapter/p2p-draft-host.ts b/client/src/adapter/p2p-draft-host.ts index 36663c8cef..0d234cd559 100644 --- a/client/src/adapter/p2p-draft-host.ts +++ b/client/src/adapter/p2p-draft-host.ts @@ -27,6 +27,7 @@ import { clearDraftHostSession, type PersistedDraftHostSession, } from "../services/draftPersistence"; +import { assignAvatarForSeat } from "../services/playerAvatars"; // ── Types ────────────────────────────────────────────────────────────── @@ -54,6 +55,8 @@ export type DraftHostEvent = | { type: "draftComplete" } | { type: "deckSubmitted"; seatIndex: number } | { type: "allDecksSubmitted" } + | { type: "draftPaused"; reason: string } + | { type: "draftResumed" } | { type: "error"; message: string } | { type: "viewUpdated"; view: DraftPlayerView } | { type: "pairingsGenerated"; round: number; pairings: PairingView[] } @@ -61,6 +64,22 @@ export type DraftHostEvent = | { type: "matchResultReceived"; matchId: string; winnerSeat: number | null } | { type: "roundAdvanced"; newRound: number } | { type: "timerExpired" } + | { + type: "bo3SideboardPrompt"; + matchId: string; + gameNumber: number; + score: MatchScore; + loserSeat: number | null; + timerMs: number; + } + | { + type: "bo3ChoosePlayDraw"; + matchId: string; + gameNumber: number; + score: MatchScore; + timerMs: number; + } + | { type: "bo3GameStart"; matchId: string; gameNumber: number; firstPlayerSeat: number } | { type: "bo3SideboardPromptSent"; matchId: string } | { type: "bo3BothSideboardsSubmitted"; matchId: string } | { type: "bo3GameStarted"; matchId: string; gameNumber: number }; @@ -96,6 +115,14 @@ function deckPayload(mainDeck: string[], sideboard: string[]): DraftDeckPayload return { main_deck: mainDeck, sideboard, commander: [] }; } +function hashStringToSeed(value: string): number { + let hash = 5381; + for (let i = 0; i < value.length; i++) { + hash = ((hash * 33) ^ value.charCodeAt(i)) | 0; + } + return hash >>> 0; +} + function sideboardFromPool( session: ExportedDraftSession, seat: number, @@ -134,6 +161,7 @@ export class P2PDraftHost { private draftStarted = false; private draftCode = ""; + private draftSeed: number | null = null; private activePodSize: number; private hostConnectionUnsub: (() => void) | null = null; private paused = false; @@ -146,6 +174,8 @@ export class P2PDraftHost { // Server backup upload state (D-08) private backupEndpoint: string | null = null; private picksSinceLastBackup = 0; + private persistQueue = Promise.resolve(); + private persistenceClosed = false; private static readonly BACKUP_INTERVAL_PICKS = 5; constructor( @@ -322,6 +352,9 @@ export class P2PDraftHost { view, draftCode: this.draftCode, }); + if (view.status === "MatchInProgress") { + await this.dispatchMatchLaunchesForSeat(view, seat!); + } } catch (err) { console.error("[P2PDraftHost] reconnect view failed:", err); } @@ -343,6 +376,7 @@ export class P2PDraftHost { if (this.disconnectedSeats.size === 0 && this.paused) { this.paused = false; this.broadcastToGuests({ type: "draft_resumed" }); + this.emit({ type: "draftResumed" }); } } @@ -403,6 +437,9 @@ export class P2PDraftHost { async startDraft(botFillEmptySeats = true): Promise { if (this.draftStarted) return; + const seed = Math.floor(Math.random() * 0xffffffff); + this.draftSeed = seed; + const draftCode = `draft-${seed.toString(16).padStart(8, "0")}`; const seats: MultiplayerSeatDescriptor[] = []; for (let i = 0; i < this.podSize; i++) { const displayName = this.seatNames.get(i); @@ -413,15 +450,13 @@ export class P2PDraftHost { display_name: displayName, }); } else if (botFillEmptySeats) { - seats.push({ type: "Bot", name: this.botNameForSeat(i) }); + seats.push({ type: "Bot", name: this.botNameForSeat(i, seed) }); } } if (seats.length < 2) { throw new Error("Need at least two seats to start a pod draft"); } - const seed = Math.floor(Math.random() * 0xffffffff); - const draftCode = `draft-${seed.toString(16).padStart(8, "0")}`; await this.adapter.createMultiplayerDraft( this.setPoolJson, seats, @@ -648,6 +683,7 @@ export class P2PDraftHost { if (!this.paused) { this.paused = true; this.broadcastToGuests({ type: "draft_paused", reason: "Player disconnected" }); + this.emit({ type: "draftPaused", reason: "Player disconnected" }); } this.emit({ type: "seatDisconnected", seatIndex: seat }); @@ -757,8 +793,8 @@ export class P2PDraftHost { return this.seatNames.get(seat) === undefined && !this.guestSessions.has(seat); } - private botNameForSeat(seat: number): string { - return `AI player ${seat + 1}`; + private botNameForSeat(seat: number, seed: number): string { + return assignAvatarForSeat(this.podSize, seat, seed)?.name ?? `Seat ${seat + 1}`; } // ── Match coordination ──────────────────────────────────────────────── @@ -770,17 +806,41 @@ export class P2PDraftHost { async generatePairings(round: number): Promise { try { const view = await this.adapter.generatePairings(round); + const launchablePairings = view.pairings.filter((pairing) => + pairing.round === round && + (pairing.status === "Pending" || pairing.status === "InProgress") + ); + + for (const pairing of launchablePairings) { + if ( + this.isBotSeatFromView(view, pairing.seat_a) && + this.isBotSeatFromView(view, pairing.seat_b) + ) { + await this.dispatchMatchLaunch(pairing, view); + } + } - for (const pairing of view.pairings) { + const postBotView = await this.adapter.getViewForSeat(0); + for (const pairing of postBotView.pairings) { if (pairing.round !== round) continue; if (pairing.status !== "Pending" && pairing.status !== "InProgress") continue; + if ( + this.isBotSeatFromView(postBotView, pairing.seat_a) && + this.isBotSeatFromView(postBotView, pairing.seat_b) + ) { + continue; + } - await this.dispatchMatchLaunch(pairing, view); + await this.dispatchMatchLaunch(pairing, postBotView); } + const latestView = await this.adapter.getViewForSeat(0); + // Broadcast updated views await this.broadcastViews(); - this.emit({ type: "pairingsGenerated", round, pairings: view.pairings }); + this.persistSession(); + this.emit({ type: "pairingsGenerated", round, pairings: latestView.pairings }); + this.emit({ type: "viewUpdated", view: latestView }); } catch (err) { const message = err instanceof Error ? err.message : String(err); this.emit({ type: "error", message: `Failed to generate pairings: ${message}` }); @@ -872,6 +932,16 @@ export class P2PDraftHost { }); } + private async dispatchMatchLaunchesForSeat(view: DraftPlayerView, seat: number): Promise { + for (const pairing of view.pairings) { + if (pairing.round !== view.current_round) continue; + if (pairing.status !== "Pending" && pairing.status !== "InProgress") continue; + if (pairing.seat_a !== seat && pairing.seat_b !== seat) continue; + + await this.dispatchMatchLaunch(pairing, view); + } + } + private isBotSeatFromView(view: DraftPlayerView, seat: number): boolean { return view.seats.find((s) => s.seat_index === seat)?.is_bot ?? this.isBotSeat(seat); } @@ -926,14 +996,12 @@ export class P2PDraftHost { // Broadcast updated views with new standings await this.broadcastViews(); + this.persistSession(); + this.emit({ type: "viewUpdated", view }); // Check if the reducer auto-advanced (Competitive mode) - if (view.status === "RoundComplete" || view.status === "Complete") { - const hostView = await this.adapter.getViewForSeat(0); - this.emit({ type: "viewUpdated", view: hostView }); - if (view.status === "Complete") { - void this.cleanupServerBackup(); - } + if (view.status === "Complete") { + void this.cleanupServerBackup(); } } catch (err) { const message = err instanceof Error ? err.message : String(err); @@ -962,8 +1030,10 @@ export class P2PDraftHost { */ async replaceSeatWithBot(seat: number): Promise { try { - await this.adapter.replaceSeatWithBot(seat); + const seed = this.draftSeed ?? hashStringToSeed(this.draftCode || this.roomCode || "draft"); + await this.adapter.replaceSeatWithBot(seat, this.botNameForSeat(seat, seed)); await this.broadcastViews(); + this.persistSession(); } catch (err) { const message = err instanceof Error ? err.message : String(err); this.emit({ type: "error", message: `Failed to replace seat ${seat}: ${message}` }); @@ -1111,8 +1181,39 @@ export class P2PDraftHost { private sendToSeat(seat: number, msg: DraftP2PMessage): void { if (seat === 0) { // Host is seat 0 — emit event directly instead of sending over network - if (msg.type === "draft_match_start") { - this.emit({ type: "matchStart", launch: msg.launch }); + switch (msg.type) { + case "draft_match_start": + this.emit({ type: "matchStart", launch: msg.launch }); + break; + case "draft_bo3_sideboard_prompt": + this.emit({ + type: "bo3SideboardPrompt", + matchId: msg.matchId, + gameNumber: msg.gameNumber, + score: msg.score, + loserSeat: msg.loserSeat, + timerMs: msg.timerMs, + }); + break; + case "draft_bo3_play_draw_prompt": + this.emit({ + type: "bo3ChoosePlayDraw", + matchId: msg.matchId, + gameNumber: msg.gameNumber, + score: msg.score, + timerMs: msg.timerMs, + }); + break; + case "draft_bo3_game_start": + this.emit({ + type: "bo3GameStart", + matchId: msg.matchId, + gameNumber: msg.gameNumber, + firstPlayerSeat: msg.firstPlayerSeat, + }); + break; + default: + break; } return; } @@ -1152,6 +1253,7 @@ export class P2PDraftHost { this.clearActiveTimer(); this.paused = true; this.broadcastToGuests({ type: "draft_paused", reason: "Paused by host" }); + this.emit({ type: "draftPaused", reason: "Paused by host" }); } } @@ -1159,6 +1261,7 @@ export class P2PDraftHost { if (this.paused && this.disconnectedSeats.size === 0) { this.paused = false; this.broadcastToGuests({ type: "draft_resumed" }); + this.emit({ type: "draftResumed" }); // Restart timer if still in drafting phase if (this.draftStarted && this.podPolicy === "Competitive") { void (async () => { @@ -1176,12 +1279,14 @@ export class P2PDraftHost { // ── Persistence (P2P-05) ────────────────────────────────────────── private persistSession(): void { - if (!this.persistenceId) return; - void (async () => { + if (!this.persistenceId || this.persistenceClosed) return; + this.persistQueue = this.persistQueue.then(async () => { try { + if (this.persistenceClosed) return; const sessionJson = this.draftStarted ? await this.adapter.exportSession() : null; + if (this.persistenceClosed) return; const snapshot: PersistedDraftHostSession = { persistenceId: this.persistenceId!, @@ -1211,7 +1316,7 @@ export class P2PDraftHost { } catch (err) { console.warn("[P2PDraftHost] persist failed:", err); } - })(); + }); } /** @@ -1265,6 +1370,7 @@ export class P2PDraftHost { } this.draftStarted = session.draftStarted; this.draftCode = session.draftCode; + this.draftSeed = hashStringToSeed(session.draftCode || this.roomCode || "draft"); if (session.draftSessionJson) { const view = await this.adapter.importSession(session.draftSessionJson, 2); @@ -1282,6 +1388,14 @@ export class P2PDraftHost { if (this.disconnectedSeats.size > 0) { this.paused = true; + this.emit({ type: "draftPaused", reason: "Waiting for players to reconnect" }); + } + + if (view.status === "MatchInProgress") { + await this.dispatchMatchLaunchesForSeat(view, 0); + } else if (view.status === "Pairing" && view.pairings.length === 0) { + await this.generatePairings(view.current_round + 1); + return this.adapter.getViewForSeat(0); } return view; @@ -1311,8 +1425,10 @@ export class P2PDraftHost { for (const session of this.guestSessions.values()) { await session.send({ type: "draft_host_left", reason: "Host left the draft" }); } + this.persistenceClosed = true; + await this.persistQueue; if (this.persistenceId) { - void clearDraftHostSession(this.persistenceId); + await clearDraftHostSession(this.persistenceId); } void this.cleanupServerBackup(); this.dispose(); diff --git a/client/src/animation/types.ts b/client/src/animation/types.ts index 53fb2b6f90..ef6b559c8f 100644 --- a/client/src/animation/types.ts +++ b/client/src/animation/types.ts @@ -17,17 +17,9 @@ export type PacingCategory = "effects" | "combat" | "banners"; export const PACING_CATEGORIES: readonly PacingCategory[] = ["effects", "combat", "banners"] as const; -export const PACING_LABELS: Record = { - effects: "Effect Pacing", - combat: "Combat Pacing", - banners: "Banner Pacing", -}; - -export const PACING_DESCRIPTIONS: Record = { - effects: "Spell casts, zone changes, deaths, life changes, counters, tap/untap.", - combat: "Combat damage timing — how long blockers and attackers linger before damage resolves.", - banners: "Turn-start banner display.", -}; +// Per-category labels/descriptions are frontend-authored display text and are +// translated at the render site via `t("pacing.labels.")` / +// `t("pacing.descriptions.")` (settings namespace). export const PACING_DEFAULT = 1.0; export const PACING_MIN = 0; diff --git a/client/src/components/card/CardPreview.tsx b/client/src/components/card/CardPreview.tsx index 092707caa8..10e04d69c1 100644 --- a/client/src/components/card/CardPreview.tsx +++ b/client/src/components/card/CardPreview.tsx @@ -142,7 +142,7 @@ function CardPreviewInner({ const defaultFaceIndex = faceIndex ?? (isTransformed ? 1 : 0); // Battlefield path: route through oracle_id when the engine attached one. // Deck-builder path: `obj` is null, so we keep the name-based fallback. - const { src, isLoading, isRotated } = useCardImage(cardName, { + const { src, isLoading, isRotated, isFlip } = useCardImage(cardName, { size: "normal", faceIndex: defaultFaceIndex, isToken, @@ -180,8 +180,12 @@ function CardPreviewInner({ }; }, []); + // Kamigawa flip cards print both halves in one image, the alternate half + // rotated 180°. There's no second face to fetch, so Ctrl spins the same image + // 180° (flip180) instead of swapping faces the way DFC/MDFC do (showOtherFace). + const flip180 = !isMobile && ctrlHeld && isFlip; // On desktop, Ctrl swaps to the other face (back face normally, front face if transformed) - const showOtherFace = !isMobile && ctrlHeld && backFaceName != null; + const showOtherFace = !isMobile && ctrlHeld && backFaceName != null && !isFlip; // Fetch other face image when Ctrl is held (hook must always be called, but with empty // string when not needed so useCardImage short-circuits without a network request). // Battlefield path: the back_face's printed_ref carries the other face's @@ -345,9 +349,12 @@ function CardPreviewInner({ isLoading={activeLoading} src={activeSrc} isRotated={activeRotated} - backFaceHint={backFaceName != null && !showOtherFace - ? (isTransformed ? t("preview.holdCtrlFront") : t("preview.holdCtrlBack")) - : null} + flip180={flip180} + backFaceHint={isFlip + ? (flip180 ? null : t("preview.holdCtrlFlip")) + : backFaceName != null && !showOtherFace + ? (isTransformed ? t("preview.holdCtrlFront") : t("preview.holdCtrlBack")) + : null} altAvailable={Boolean(frontParseDetails || engineFrontFace)} debugObjectId={showDebugId && inspectedObjectId != null ? inspectedObjectId : null} /> @@ -373,7 +380,8 @@ function MobilePreviewOverlay({ sourcePrinting?: SourcePrinting; layout?: "modal" | "compact"; }) { - const { src, isRotated } = useCardImage(cardName, { + const { t } = useTranslation("game"); + const { src, isRotated, isFlip } = useCardImage(cardName, { size: "normal", faceIndex, oracleId: obj?.printed_ref?.oracle_id, @@ -381,6 +389,11 @@ function MobilePreviewOverlay({ sourcePrinting, }); + // Mobile has no Ctrl key, so a Kamigawa flip card's 180° spin is a tap toggle + // (desktop holds Ctrl). Only the full-screen modal layout can host the button — + // the compact peek dismisses on any tap via document-level capture listeners. + const [flipped, setFlipped] = useState(false); + // Compact layout: dismiss on the next tap or scroll anywhere, so no separate // dismiss gesture is needed. Listeners attach on a deferred tick so the very // tap that opened the preview doesn't immediately close it. Capture phase so @@ -449,8 +462,17 @@ function MobilePreviewOverlay({ draggable={false} className={isRotated ? "absolute left-1/2 top-1/2 h-[min(84vw,420px)] w-[min(60vw,300px)] -translate-x-1/2 -translate-y-1/2 rotate-90 object-cover" - : "max-h-[calc(100dvh-2rem)] max-w-full object-contain"} + : `max-h-[calc(100dvh-2rem)] max-w-full object-contain${isFlip ? " transition-transform duration-200" : ""}${flipped ? " rotate-180" : ""}`} /> + {isFlip && ( + + )} )} @@ -468,6 +490,7 @@ function CardImagePreview({ isLoading, src, isRotated, + flip180, backFaceHint, altAvailable, mobileMode, @@ -482,6 +505,7 @@ function CardImagePreview({ isLoading: boolean; src: string | null; isRotated: boolean; + flip180?: boolean; backFaceHint: string | null; altAvailable: boolean; mobileMode?: boolean; @@ -508,7 +532,7 @@ function CardImagePreview({ ? mobileMode ? "absolute left-1/2 top-1/2 h-[min(56vw,420px)] w-[min(40vw,300px)] -translate-x-1/2 -translate-y-1/2 rotate-90 object-cover" : "absolute left-1/2 top-1/2 h-[clamp(308px,36.4vw,661px)] w-[clamp(220px,26vw,472px)] max-h-[80vh] max-w-[42vw] -translate-x-1/2 -translate-y-1/2 rotate-90 object-cover" - : `${frameClass} object-cover`; + : `${frameClass} object-cover transition-transform duration-200${flip180 ? " rotate-180" : ""}`; // Use effective spell cost from engine if available (reflects alt costs, reductions), // otherwise fall back to printed mana cost. When the user holds Ctrl to view the diff --git a/client/src/components/chrome/GameMenu.tsx b/client/src/components/chrome/GameMenu.tsx index 2807ae8aec..c40cd7b5aa 100644 --- a/client/src/components/chrome/GameMenu.tsx +++ b/client/src/components/chrome/GameMenu.tsx @@ -5,8 +5,9 @@ import { useTranslation } from "react-i18next"; import { ConnectionDot } from "../multiplayer/ConnectionDot.tsx"; import { FullscreenButton } from "./FullscreenButton.tsx"; import { VolumeControl } from "./VolumeControl.tsx"; -import { clearGame } from "../../stores/gameStore.ts"; +import { clearGame, useGameStore } from "../../stores/gameStore.ts"; import { useDraftStore } from "../../stores/draftStore.ts"; +import { useMultiplayerDraftStore } from "../../stores/multiplayerDraftStore.ts"; import { useCardDataMeta } from "../../hooks/useCardDataMeta.ts"; interface GameMenuProps { @@ -44,6 +45,7 @@ export function GameMenu({ const menuRef = useRef(null); const cardDataMeta = useCardDataMeta(); const isDraft = searchParams.get("source") === "draft" && !!searchParams.get("draftId"); + const isDraftPodMatch = searchParams.get("mode") === "draft-match"; useEffect(() => { if (!open) return; @@ -151,6 +153,21 @@ export function GameMenu({ clearGame(gameId); navigate("/draft/quick?resume=1"); }); + } else if (isDraftPodMatch) { + const adapter = useGameStore.getState().adapter as { + sendConcede?: () => void | Promise; + } | null; + void adapter?.sendConcede?.(); + void useMultiplayerDraftStore + .getState() + .reportActiveMatchConcession() + .then(() => { + clearGame(gameId); + navigate("/draft-pod"); + }) + .catch((err) => { + console.error("[GameMenu] failed to report draft pod concession:", err); + }); } else { clearGame(gameId); navigate("/"); @@ -158,7 +175,7 @@ export function GameMenu({ }} /> { setOpen(false); if (isDraft) { @@ -166,6 +183,8 @@ export function GameMenu({ clearGame(gameId); navigate("/draft/quick?resume=1"); }); + } else if (isDraftPodMatch) { + navigate("/draft-pod"); } else { navigate("/"); } diff --git a/client/src/components/chrome/HostControlTile.tsx b/client/src/components/chrome/HostControlTile.tsx index d7c736c8c0..52744bcabb 100644 --- a/client/src/components/chrome/HostControlTile.tsx +++ b/client/src/components/chrome/HostControlTile.tsx @@ -148,7 +148,7 @@ function SeatRow({ > {AI_DIFFICULTIES.map((difficulty) => ( ))} diff --git a/client/src/components/chrome/ScreenChrome.tsx b/client/src/components/chrome/ScreenChrome.tsx index 9d2a8c2637..c00e5ffb6d 100644 --- a/client/src/components/chrome/ScreenChrome.tsx +++ b/client/src/components/chrome/ScreenChrome.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { motion } from "framer-motion"; +import { useTranslation } from "react-i18next"; import { usePreferencesStore } from "../../stores/preferencesStore"; import { menuButtonClass } from "../menu/buttonStyles"; @@ -54,6 +55,7 @@ export function ScreenChrome({ settingsOpen, onSettingsOpenChange, }: ScreenChromeProps) { + const { t } = useTranslation(); const language = usePreferencesStore((s) => s.language); const [internalShowSettings, setInternalShowSettings] = useState(false); const isSettingsControlled = settingsOpen !== undefined; @@ -81,8 +83,8 @@ export function ScreenChrome({ whileHover={{ y: -1 }} whileTap={{ scale: 0.98 }} onClick={onBack} - aria-label="Back" - title="Back" + aria-label={t("chrome.back")} + title={t("chrome.back")} > @@ -103,8 +105,8 @@ export function ScreenChrome({ whileHover={{ y: -1 }} whileTap={{ scale: 0.98 }} onClick={() => setShowSettings(true)} - aria-label={`Language (${language.toUpperCase()}) — open settings`} - title={`Language: ${language.toUpperCase()}`} + aria-label={t("chrome.languageSettings", { lang: language.toUpperCase() })} + title={t("chrome.languageTitle", { lang: language.toUpperCase() })} > @@ -118,8 +120,8 @@ export function ScreenChrome({ whileHover={{ y: -1 }} whileTap={{ scale: 0.98 }} onClick={() => setShowSettings(true)} - aria-label="Settings" - title="Settings" + aria-label={t("chrome.settings")} + title={t("chrome.settings")} > diff --git a/client/src/components/deck-builder/CardSearch.tsx b/client/src/components/deck-builder/CardSearch.tsx index 92d2d7c154..e36ba92b26 100644 --- a/client/src/components/deck-builder/CardSearch.tsx +++ b/client/src/components/deck-builder/CardSearch.tsx @@ -1,11 +1,7 @@ import { useState, useRef, useCallback, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { - searchScryfall, - buildScryfallQuery, - scryfallLegalityKey, - type ScryfallCard, -} from "../../services/scryfall"; +import { scryfallLegalityKey, type ScryfallCard } from "../../services/scryfall"; +import { searchCards } from "../../services/engineRuntime"; import type { GameFormat } from "../../adapter/types"; import { FORMAT_REGISTRY } from "../../data/formatRegistry"; import { useSetList } from "../../hooks/useSetList"; @@ -122,28 +118,23 @@ export function CardSearch({ return; } - const query = buildScryfallQuery({ - text: searchText || undefined, - colors: colors.length > 0 ? colors : undefined, - type: type || undefined, - cmcMax: cmc, - sets, - format: browseFormat === "all" ? undefined : scryfallLegalityKey(browseFormat), - }); - - if (!query) { - onResults([], 0); - setResultCount(null); - return; - } - + // AbortController is the staleness guard: a newer search aborts this one + // so its results never overwrite the latest. The search itself runs + // locally through the engine (no network, no signal to pass). const controller = new AbortController(); abortRef.current = controller; setLoading(true); setError(null); try { - const { cards, total } = await searchScryfall(query, controller.signal); + const { cards, total } = await searchCards({ + text: searchText || undefined, + colors: colors.length > 0 ? colors : undefined, + type: type || undefined, + cmcMax: cmc, + sets, + legalFormat: browseFormat === "all" ? undefined : scryfallLegalityKey(browseFormat), + }); if (!controller.signal.aborted) { onResults(cards, total); setResultCount(total); diff --git a/client/src/components/deck-builder/CommanderPanel.tsx b/client/src/components/deck-builder/CommanderPanel.tsx index 1cb3e0e3ee..fb8cba1bc4 100644 --- a/client/src/components/deck-builder/CommanderPanel.tsx +++ b/client/src/components/deck-builder/CommanderPanel.tsx @@ -7,6 +7,7 @@ import { getColorIdentityViolations, getSingletonViolations, } from "./commanderUtils"; +import { mouseHoverPreview } from "./hoverPreview"; const WUBRG_COLORS = ["W", "U", "B", "R", "G"] as const; @@ -26,6 +27,7 @@ interface CommanderPanelProps { isCommanderEligible: (name: string) => boolean; onSetCommander: (cardName: string) => void; onRemoveCommander: (cardName: string) => void; + onCardHover?: (cardName: string | null) => void; } @@ -37,6 +39,7 @@ export function CommanderPanel({ isCommanderEligible, onSetCommander, onRemoveCommander, + onCardHover, }: CommanderPanelProps) { const { t } = useTranslation("deck-builder"); const identity = getCombinedColorIdentity(commanders, cardDataCache); @@ -70,6 +73,7 @@ export function CommanderPanel({ return (
@@ -113,6 +117,7 @@ export function CommanderPanel({
diff --git a/client/src/components/deck-builder/StatsPanel.tsx b/client/src/components/deck-builder/StatsPanel.tsx index ad419f9fac..98d1cecc4b 100644 --- a/client/src/components/deck-builder/StatsPanel.tsx +++ b/client/src/components/deck-builder/StatsPanel.tsx @@ -6,6 +6,7 @@ import { FORMAT_REGISTRY } from "../../data/formatRegistry"; import type { BracketEstimate, CommanderBracket } from "../../types/bracket"; import { ManaCurve } from "./ManaCurve"; import { BracketAuditPanel } from "./BracketAuditPanel"; +import { BracketPicker } from "./BracketPicker"; const LEGALITY_STYLES: Record = { legal: "bg-emerald-600/70 text-emerald-100", @@ -41,6 +42,7 @@ interface StatsPanelProps { isCommander: boolean; estimate: BracketEstimate | null; manualBracket: CommanderBracket | null; + onBracketChange: (bracket: CommanderBracket | null) => void; auditEmptyReason?: "not-commander" | "no-commander" | "unsupported"; onCardClick: (cardName: string) => void; } @@ -52,6 +54,7 @@ export function StatsPanel({ isCommander, estimate, manualBracket, + onBracketChange, auditEmptyReason, onCardClick, }: StatsPanelProps) { @@ -65,12 +68,23 @@ export function StatsPanel({ return (
{isCommander && ( - +
+ {/* The bracket picker lives beside the audit it's compared against, so + setting a bracket and seeing the deck's estimated tier (and any + mismatch) read as one unit. Both are Commander-only. */} +
+ + {t("toolbar.bracket")} + + +
+ +
)}
diff --git a/client/src/components/draft/BotIndicator.tsx b/client/src/components/draft/BotIndicator.tsx new file mode 100644 index 0000000000..c05dd8bdc6 --- /dev/null +++ b/client/src/components/draft/BotIndicator.tsx @@ -0,0 +1,21 @@ +interface BotIndicatorProps { + label: string; + size?: "sm" | "md"; +} + +export function BotIndicator({ label, size = "md" }: BotIndicatorProps) { + const boxSize = size === "sm" ? "h-3.5 w-3.5" : "h-4 w-4"; + return ( + + + + + + + + ); +} diff --git a/client/src/components/draft/DraftPodLobby.tsx b/client/src/components/draft/DraftPodLobby.tsx index 7d43a13f22..2f49b49fa7 100644 --- a/client/src/components/draft/DraftPodLobby.tsx +++ b/client/src/components/draft/DraftPodLobby.tsx @@ -23,6 +23,7 @@ import type { SeatPublicView } from "../../adapter/draft-adapter"; import { menuButtonClass } from "../menu/buttonStyles"; import { useMultiplayerDraftStore } from "../../stores/multiplayerDraftStore"; import { useDraftPodStore } from "../../stores/draftPodStore"; +import { BotIndicator } from "./BotIndicator"; // ── Seat Card ───────────────────────────────────────────────────────── @@ -50,6 +51,7 @@ function SeatCard({ ? t("lobby.botSeat") : t("lobby.waitingSeat") : seat.display_name; + const botLabel = t("lobby.botSeat"); const borderColor = isLocalSeat ? "border-emerald-400/40" @@ -96,6 +98,7 @@ function SeatCard({ > {seatLabel} + {seat.is_bot && }
{/* Role badge */} diff --git a/client/src/components/draft/HostControls.tsx b/client/src/components/draft/HostControls.tsx index 7d445f5e2d..2d02357f27 100644 --- a/client/src/components/draft/HostControls.tsx +++ b/client/src/components/draft/HostControls.tsx @@ -1,10 +1,21 @@ +import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router"; import { useMultiplayerDraftStore } from "../../stores/multiplayerDraftStore"; +import { useDraftPodStore } from "../../stores/draftPodStore"; import { menuButtonClass } from "../menu/buttonStyles"; const EMPTY_SEATS: Array<{ seat_index: number; display_name: string; is_bot: boolean; connected: boolean }> = []; +function winnerChoiceClass(selected: boolean): string { + return menuButtonClass({ + tone: selected ? "emerald" : "neutral", + size: "xs", + className: "min-w-0 flex-1 justify-center px-2", + }); +} + // ── Component ─────────────────────────────────────────────────────────── /** @@ -13,6 +24,8 @@ const EMPTY_SEATS: Array<{ seat_index: number; display_name: string; is_bot: boo */ export function HostControls() { const { t } = useTranslation("draft"); + const navigate = useNavigate(); + const [endingDraft, setEndingDraft] = useState(false); const role = useMultiplayerDraftStore((s) => s.role); const phase = useMultiplayerDraftStore((s) => s.phase); const podPolicy = useMultiplayerDraftStore((s) => s.view?.pod_policy); @@ -24,6 +37,8 @@ export function HostControls() { const overrideMatchResult = useMultiplayerDraftStore( (s) => s.overrideMatchResult, ); + const leave = useMultiplayerDraftStore((s) => s.leave); + const resetPod = useDraftPodStore((s) => s.reset); const replaceSeatWithBot = useMultiplayerDraftStore( (s) => s.replaceSeatWithBot, ); @@ -37,18 +52,42 @@ export function HostControls() { podPolicy === "Casual" && phase === "roundComplete"; const showOverride = podPolicy === "Casual" && - phase === "matchInProgress" && + (phase === "matchInProgress" || phase === "roundComplete") && pairings.length > 0; const humanSeats = seats.filter((s) => !s.is_bot); const showKickReplace = humanSeats.length > 0 && (phase === "matchInProgress" || phase === "roundComplete"); + const showEndDraft = ![ + "idle", + "connecting", + "complete", + "error", + "kicked", + "hostLeft", + ].includes(phase); + + const handleEndDraft = async () => { + if (endingDraft) return; + if (!window.confirm(t("hostControls.endDraftConfirm"))) return; + + setEndingDraft(true); + try { + await leave(false); + resetPod(); + navigate("/"); + } catch (err) { + console.error("[HostControls] failed to end draft:", err); + setEndingDraft(false); + } + }; if ( !showPauseResume && !showAdvanceRound && !showOverride && - !showKickReplace + !showKickReplace && + !showEndDraft ) return null; @@ -83,32 +122,46 @@ export function HostControls() { {/* Override match result — Casual mode, during matches */} {showOverride && ( -
+
{t("hostControls.overrideResult")}
- {pairings - .filter((p) => p.status !== "Complete") - .map((p) => ( -
- - {t("hostControls.versusPair", { a: p.name_a, b: p.name_b })} - - - + + {t("standings.versus")} + + +
+
- {p.name_b.split(" ")[0]} - + {p.match_id} - {p.status} +
- ))} + ); + })}
)} @@ -127,6 +180,21 @@ export function HostControls() { ))}
)} + + {showEndDraft && ( + + )}
); } diff --git a/client/src/components/draft/SeatStatusRing.tsx b/client/src/components/draft/SeatStatusRing.tsx index cbde788aee..f46fe3e808 100644 --- a/client/src/components/draft/SeatStatusRing.tsx +++ b/client/src/components/draft/SeatStatusRing.tsx @@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next"; import type { SeatPublicView } from "../../adapter/draft-adapter"; import { useMultiplayerDraftStore } from "../../stores/multiplayerDraftStore"; +import { BotIndicator } from "./BotIndicator"; const EMPTY_SEATS: SeatPublicView[] = []; @@ -30,6 +31,7 @@ interface SeatBadgeProps { function SeatBadge({ seat, isLocal }: SeatBadgeProps) { const { t } = useTranslation("draft"); + const botLabel = t("lobby.botSeat"); const borderColor = isLocal ? "border-emerald-400/40" : PICK_STATUS_BORDER[seat.pick_status]; @@ -42,6 +44,7 @@ function SeatBadge({ seat, isLocal }: SeatBadgeProps) { {seat.display_name || t("seat.label", { number: seat.seat_index + 1 })} + {seat.is_bot && } ); } diff --git a/client/src/components/draft/SetSelector.tsx b/client/src/components/draft/SetSelector.tsx index aac497192a..d176dcc004 100644 --- a/client/src/components/draft/SetSelector.tsx +++ b/client/src/components/draft/SetSelector.tsx @@ -40,7 +40,11 @@ export function SetSelector({ onStartDraft }: SetSelectorProps) { const [sets, setSets] = useState>([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + // `null` = no error. `{ detail }` carries a technical message; `detail` + // undefined means "use the generic translated fallback". Translation happens + // at render so the load effect never closes over `t` (avoids re-fetch on + // language change). + const [error, setError] = useState<{ detail?: string } | null>(null); useEffect(() => { let cancelled = false; @@ -71,7 +75,7 @@ export function SetSelector({ onStartDraft }: SetSelectorProps) { setSets(entries); } catch (err) { if (!cancelled) { - setError(err instanceof Error ? err.message : "Failed to load sets"); + setError({ detail: err instanceof Error ? err.message : undefined }); } } finally { if (!cancelled) setLoading(false); @@ -123,7 +127,9 @@ export function SetSelector({ onStartDraft }: SetSelectorProps) { {error && ( -
{error}
+
+ {error.detail ?? t("setSelector.loadFailed")} +
)} {!loading && !error && sets.length === 0 && ( diff --git a/client/src/components/draft/StandingsTable.tsx b/client/src/components/draft/StandingsTable.tsx index 9feb8ea351..ae904734ad 100644 --- a/client/src/components/draft/StandingsTable.tsx +++ b/client/src/components/draft/StandingsTable.tsx @@ -20,6 +20,40 @@ function formatGwp(entry: StandingEntry): string { return `${(computeGwp(entry) * 100).toFixed(0)}%`; } +function podiumRowClass(rank: number): string { + switch (rank) { + case 0: + return "bg-amber-400/[0.08] text-amber-50"; + case 1: + return "bg-slate-200/[0.07] text-slate-50"; + case 2: + return "bg-orange-400/[0.07] text-orange-50"; + default: + return ""; + } +} + +function podiumBadgeClass(rank: number): string { + const base = + "inline-flex h-6 min-w-6 items-center justify-center rounded-full border px-1.5 text-xs font-semibold tabular-nums"; + switch (rank) { + case 0: + return `${base} border-amber-300/40 bg-amber-300/18 text-amber-100`; + case 1: + return `${base} border-slate-200/40 bg-slate-200/14 text-slate-100`; + case 2: + return `${base} border-orange-300/40 bg-orange-300/16 text-orange-100`; + default: + return `${base} border-white/10 bg-white/[0.04] text-white/45`; + } +} + +function localPlayerRowClass(isLocal: boolean): string { + return isLocal + ? "bg-emerald-400/[0.08] [box-shadow:inset_3px_0_0_rgba(52,211,153,0.75)]" + : ""; +} + // ── Component ─────────────────────────────────────────────────────────── /** Swiss tournament standings sorted by match wins (GWP tiebreaker), with current round pairings and live game scores. */ @@ -59,11 +93,16 @@ export function StandingsTable() { {sorted.map((entry, i) => ( - {i + 1} + + {i + 1} + {entry.display_name} {entry.match_wins}-{entry.match_losses} diff --git a/client/src/components/menu/AiDifficultyDropdown.tsx b/client/src/components/menu/AiDifficultyDropdown.tsx index 304e54945c..39eda43f95 100644 --- a/client/src/components/menu/AiDifficultyDropdown.tsx +++ b/client/src/components/menu/AiDifficultyDropdown.tsx @@ -1,10 +1,6 @@ import { useTranslation } from "react-i18next"; -import { - AI_DIFFICULTIES, - getAiDifficultyLabel, - type AIDifficulty, -} from "../../constants/ai"; +import { AI_DIFFICULTIES, type AIDifficulty } from "../../constants/ai"; interface AiDifficultyDropdownProps { difficulty: AIDifficulty; @@ -29,7 +25,9 @@ export function AiDifficultyDropdown({ diff --git a/client/src/components/menu/AiOpponentConfig.tsx b/client/src/components/menu/AiOpponentConfig.tsx index e731612a53..b11ec8a031 100644 --- a/client/src/components/menu/AiOpponentConfig.tsx +++ b/client/src/components/menu/AiOpponentConfig.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import type { GameFormat, MatchType } from "../../adapter/types"; -import { AI_DIFFICULTIES, getAiDifficultyLabel, type AIDifficulty } from "../../constants/ai"; +import { AI_DIFFICULTIES, type AIDifficulty } from "../../constants/ai"; import type { AiDeckCandidate } from "../../services/aiDeckCatalog"; import { useAiDeckCatalog } from "../../services/aiDeckCatalog"; import { @@ -260,7 +260,7 @@ function AiSeatPanel({ const summaryDeck = isRandom ? t("aiOpponent.deckRandomCount", { count: filteredDecks.length }) : (selectedCandidate?.name ?? t("aiOpponent.deckRandom")); - const summaryDifficulty = getAiDifficultyLabel(seat.difficulty); + const summaryDifficulty = t(`aiDifficulty.levels.${seat.difficulty}`); const body = (
@@ -295,7 +295,7 @@ function AiSeatPanel({ > {AI_DIFFICULTIES.map((item) => ( ))} diff --git a/client/src/components/menu/LoadGameStateModal.tsx b/client/src/components/menu/LoadGameStateModal.tsx index 9aa0a7e0ce..c9201d9025 100644 --- a/client/src/components/menu/LoadGameStateModal.tsx +++ b/client/src/components/menu/LoadGameStateModal.tsx @@ -58,7 +58,7 @@ export function LoadGameStateModal({ open, onClose, onLoaded }: LoadGameStateMod try { handleParsed(await readImportFile(file)); } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Failed to read file"); + setError(err instanceof Error ? err.message : t("loadGameState.readFailed")); } }; diff --git a/client/src/components/menu/MyDecks.tsx b/client/src/components/menu/MyDecks.tsx index dfc9bb7ccd..8a142be61a 100644 --- a/client/src/components/menu/MyDecks.tsx +++ b/client/src/components/menu/MyDecks.tsx @@ -1093,7 +1093,7 @@ export function MyDecks({ > {FORMAT_FILTERS.map(({ key, label }) => ( ))} diff --git a/client/src/components/modal/CardChoiceModal.tsx b/client/src/components/modal/CardChoiceModal.tsx index 423aee2548..049b51fde0 100644 --- a/client/src/components/modal/CardChoiceModal.tsx +++ b/client/src/components/modal/CardChoiceModal.tsx @@ -1069,7 +1069,9 @@ function OutsideGameModal({ data }: { data: OutsideGameChoice["data"] }) { const selectedCount = Math.min(selectedCounts.get(key) ?? 0, entry.count); const isSelected = selectedCount > 0; const sourceLabel = - entry.source.type === "FaceUpExile" ? "From exile" : "From sideboard"; + entry.source.type === "FaceUpExile" + ? t("outsideGame.fromExile") + : t("outsideGame.fromSideboard"); return ( + + +
+ )}
{data.eligible.map((target) => { const key = targetKey(target); diff --git a/client/src/components/modal/targetRef.ts b/client/src/components/modal/targetRef.ts index 6264d2db3e..a7b9d2762e 100644 --- a/client/src/components/modal/targetRef.ts +++ b/client/src/components/modal/targetRef.ts @@ -1,4 +1,4 @@ -import type { GameObject, TargetRef } from "../../adapter/types.ts"; +import type { GameObject, PlayerId, TargetRef } from "../../adapter/types.ts"; import { getPlayerDisplayName } from "../../stores/multiplayerStore.ts"; export function targetLabel( @@ -15,3 +15,18 @@ export function targetKey(target: TargetRef): string { if ("Object" in target) return `obj-${target.Object}`; return `player-${target.Player}`; } + +/** Filters targets down to those on a player's own side: permanents they control + * plus their own player target. Used by chooser quick-selects so a player can + * pick "my side" in one click instead of deselecting every opponent target. */ +export function filterTargetsByController( + targets: TargetRef[], + objects: Record | undefined, + playerId: PlayerId, +): TargetRef[] { + return targets.filter((target) => + "Object" in target + ? objects?.[String(target.Object)]?.controller === playerId + : target.Player === playerId, + ); +} diff --git a/client/src/components/settings/PreferencesModal.tsx b/client/src/components/settings/PreferencesModal.tsx index 15368f16f2..3d962dd3e5 100644 --- a/client/src/components/settings/PreferencesModal.tsx +++ b/client/src/components/settings/PreferencesModal.tsx @@ -15,8 +15,6 @@ import { ANIMATION_SPEED_STEP, PACING_CATEGORIES, PACING_DEFAULT, - PACING_DESCRIPTIONS, - PACING_LABELS, PACING_MAX, PACING_MIN, PACING_STEP, @@ -52,6 +50,7 @@ const LANGUAGE_OPTIONS: { value: SupportedLng; label: string }[] = [ { value: "de", label: "Deutsch" }, { value: "it", label: "Italiano" }, { value: "pt", label: "Português" }, + { value: "pl", label: "Polski" }, ]; const CARD_SIZES: CardSizePreference[] = ["small", "medium", "large"]; @@ -286,6 +285,7 @@ export function PreferencesModal({ options={CARD_SIZES} value={cardSize} onChange={setCardSize} + renderLabel={(opt) => t(`gameplay.cardSizeOptions.${opt}`)} /> @@ -294,6 +294,7 @@ export function PreferencesModal({ options={LOG_DEFAULTS} value={logDefaultState} onChange={setLogDefaultState} + renderLabel={(opt) => t(`gameplay.logDefaultOptions.${opt}`)} /> @@ -354,6 +355,7 @@ export function PreferencesModal({ options={VFX_QUALITIES} value={vfxQuality} onChange={setVfxQuality} + renderLabel={(opt) => t(`visual.vfxQualityOptions.${opt}`)} /> @@ -874,8 +876,8 @@ function PacingSection({ {PACING_CATEGORIES.map((category) => ( ({ options, value, onChange, + renderLabel, }: { options: T[]; value: T; onChange: (v: T) => void; + /** Maps a raw option value to its translated, display-ready label. */ + renderLabel: (opt: T) => string; }) { return (
@@ -1109,13 +1114,13 @@ function SegmentedControl({ ))}
diff --git a/client/src/components/ui/LanguageFlag.tsx b/client/src/components/ui/LanguageFlag.tsx index 2376f883e5..098525efd3 100644 --- a/client/src/components/ui/LanguageFlag.tsx +++ b/client/src/components/ui/LanguageFlag.tsx @@ -92,6 +92,16 @@ function FlagPT({ className }: { className?: string }) { ); } +function FlagPL({ className }: { className?: string }) { + // White over red, horizontal halves. + return ( + + ); +} + export function LanguageFlag({ lng, className }: { lng: SupportedLng; className?: string }) { // Exhaustive over SupportedLng — a new language without a flag is a compile error. switch (lng) { @@ -107,5 +117,7 @@ export function LanguageFlag({ lng, className }: { lng: SupportedLng; className? return ; case "pt": return ; + case "pl": + return ; } } diff --git a/client/src/constants/ai.ts b/client/src/constants/ai.ts index d9d9ee7119..29d4a1b6ef 100644 --- a/client/src/constants/ai.ts +++ b/client/src/constants/ai.ts @@ -1,15 +1,13 @@ +// `id` is the engine difficulty enum; display labels are translated at render +// via `t("aiDifficulty.levels.")` (menu namespace) — not stored here. export const AI_DIFFICULTIES = [ - { id: "VeryEasy", label: "Very Easy", shortLabel: "Very Easy" }, - { id: "Easy", label: "Easy", shortLabel: "Easy" }, - { id: "Medium", label: "Medium", shortLabel: "Medium" }, - { id: "Hard", label: "Hard", shortLabel: "Hard" }, - { id: "VeryHard", label: "Very Hard", shortLabel: "Very Hard" }, + { id: "VeryEasy" }, + { id: "Easy" }, + { id: "Medium" }, + { id: "Hard" }, + { id: "VeryHard" }, ] as const; export type AIDifficulty = (typeof AI_DIFFICULTIES)[number]["id"]; export const DEFAULT_AI_DIFFICULTY: AIDifficulty = "Medium"; - -export function getAiDifficultyLabel(difficulty: string): string { - return AI_DIFFICULTIES.find((item) => item.id === difficulty)?.label ?? difficulty; -} diff --git a/client/src/hooks/useCardImage.ts b/client/src/hooks/useCardImage.ts index 1656a9e3d5..44e0ccd62e 100644 --- a/client/src/hooks/useCardImage.ts +++ b/client/src/hooks/useCardImage.ts @@ -7,6 +7,7 @@ import { fetchTokenImageUrl, findPrintingById, getCardPrintings, + isCardImageFlipLayoutSync, isCardImageRotatedSync, resolveFaceIndexSync, resolveOracleIdSync, @@ -53,6 +54,9 @@ interface UseCardImageResult { src: string | null; isLoading: boolean; isRotated: boolean; + /** True for Kamigawa-style flip cards (`layout: "flip"`), whose alternate half + * is the same image rotated 180°. The preview uses this to enable Ctrl-spin. */ + isFlip: boolean; } interface MemoryCacheEntry { @@ -314,6 +318,7 @@ export function useCardImage( const [src, setSrc] = useState(null); const [isRotated, setIsRotated] = useState(false); + const [isFlip, setIsFlip] = useState(false); const [isLoading, setIsLoading] = useState(true); const [, setArtCacheTick] = useState(0); @@ -382,6 +387,7 @@ export function useCardImage( if (overrideUrl) { setSrc(overrideUrl); setIsRotated(isCardImageRotatedSync(resolvedOracleId, cardName)); + setIsFlip(isCardImageFlipLayoutSync(resolvedOracleId, cardName)); setIsLoading(false); return; } @@ -389,6 +395,7 @@ export function useCardImage( if (!cardName && !oracleId) { setSrc(null); setIsRotated(false); + setIsFlip(false); setIsLoading(false); return; } @@ -418,11 +425,13 @@ export function useCardImage( if (!cancelled) { setSrc(imageAsset?.src || null); setIsRotated(imageAsset?.isRotated ?? false); + setIsFlip(isCardImageFlipLayoutSync(resolvedOracleId, cardName)); setIsLoading(false); } } catch { if (!cancelled) { setIsRotated(false); + setIsFlip(false); setIsLoading(false); } } @@ -453,5 +462,5 @@ export function useCardImage( size, ]); - return { src, isLoading, isRotated }; + return { src, isLoading, isRotated, isFlip }; } diff --git a/client/src/hooks/usePhaseInfo.ts b/client/src/hooks/usePhaseInfo.ts index 34eb6fe77a..34d571226d 100644 --- a/client/src/hooks/usePhaseInfo.ts +++ b/client/src/hooks/usePhaseInfo.ts @@ -1,3 +1,5 @@ +import { useTranslation } from "react-i18next"; + import type { Phase } from "../adapter/types.ts"; import { useGameStore } from "../stores/gameStore.ts"; import { usePlayerId } from "./usePlayerId.ts"; @@ -74,29 +76,25 @@ const COMBAT_PHASES = new Set([ "EndCombat", ]); -const NEXT_PHASE_LABELS: Partial> = { +// Maps the current phase to the next one the "advance" button moves to. The +// display name is translated via `phaseName.`; this stays an enum map so +// the label routes through i18n rather than carrying hardcoded English. +const NEXT_PHASE: Partial> = { Untap: "Upkeep", Upkeep: "Draw", - Draw: "Main Phase 1", - PreCombatMain: "Begin Combat", - BeginCombat: "Declare Attackers", - DeclareAttackers: "Declare Blockers", - DeclareBlockers: "Combat Damage", - CombatDamage: "End Combat", - EndCombat: "Main Phase 2", - PostCombatMain: "End Step", + Draw: "PreCombatMain", + PreCombatMain: "BeginCombat", + BeginCombat: "DeclareAttackers", + DeclareAttackers: "DeclareBlockers", + DeclareBlockers: "CombatDamage", + CombatDamage: "EndCombat", + EndCombat: "PostCombatMain", + PostCombatMain: "End", End: "Cleanup", }; -function getAdvanceLabel(phase: Phase, hasStackItems: boolean, isMyTurn: boolean): string { - if (hasStackItems) return "Resolve"; - if (!isMyTurn) return "Pass Priority"; - - const nextPhaseLabel = NEXT_PHASE_LABELS[phase]; - return nextPhaseLabel ? `To ${nextPhaseLabel}` : "Pass Priority"; -} - export function usePhaseInfo(): PhaseInfo { + const { t } = useTranslation("game"); const phase = useGameStore((s) => s.gameState?.phase ?? "Untap"); const stackLength = useGameStore((s) => s.gameState?.stack.length ?? 0); const activePlayer = useGameStore((s) => s.gameState?.active_player ?? 0); @@ -106,8 +104,18 @@ export function usePhaseInfo(): PhaseInfo { const displayKey = PHASE_TO_DISPLAY[phase]; const currentOrder = DISPLAY_ORDER[displayKey]; const isCombatPhase = COMBAT_PHASES.has(phase); - const advanceLabel = getAdvanceLabel(phase, stackLength > 0, isMyTurn); - const nextPhaseLabel = NEXT_PHASE_LABELS[phase] ?? null; + + const nextPhase = NEXT_PHASE[phase]; + const nextPhaseLabel = nextPhase ? t(`phaseName.${nextPhase}`) : null; + + let advanceLabel: string; + if (stackLength > 0) { + advanceLabel = t("advance.resolve"); + } else if (!isMyTurn || !nextPhaseLabel) { + advanceLabel = t("advance.passPriority"); + } else { + advanceLabel = t("advance.toPhase", { phase: nextPhaseLabel }); + } return { displayKey, diff --git a/client/src/i18n/README.md b/client/src/i18n/README.md index c89fc1f036..318be9042b 100644 --- a/client/src/i18n/README.md +++ b/client/src/i18n/README.md @@ -57,3 +57,9 @@ Namespaces: `common` (default), `menu`, `game`, `deck-builder`, `draft`, use `usePreferencesStore.getState().setLanguage(lng)`. - English (`en`) is the typing oracle: add a key to `en/.json` **before** referencing it, or it won't type-check. Other locales fall back to English. +- Encoding: catalogs are **UTF-8 with literal accented characters** (write + `"Wähle"` directly — never `\u`-escape sequences) so translations stay + human-readable in diffs. Save files as UTF-8 with no BOM. +- Every other locale must carry the **exact same keys** as `en` — no missing + translations, no orphans. `resources.test.ts` enforces both key parity and + UTF-8 encoding across all catalogs (runs in CI + Tilt `test-frontend`). diff --git a/client/src/i18n/locales/de/common.json b/client/src/i18n/locales/de/common.json index f54727bd6a..408cd90b20 100644 --- a/client/src/i18n/locales/de/common.json +++ b/client/src/i18n/locales/de/common.json @@ -198,5 +198,11 @@ }, "turnBanner": { "turn": "Zug {{number}}" + }, + "chrome": { + "back": "Zurück", + "settings": "Einstellungen", + "languageSettings": "Sprache ({{lang}}) — Einstellungen öffnen", + "languageTitle": "Sprache: {{lang}}" } } diff --git a/client/src/i18n/locales/de/draft.json b/client/src/i18n/locales/de/draft.json index 10897ab61b..269eb69f97 100644 --- a/client/src/i18n/locales/de/draft.json +++ b/client/src/i18n/locales/de/draft.json @@ -39,7 +39,9 @@ "overrideResult": "Ergebnis überschreiben", "versusPair": "{{a}} gegen {{b}}", "kickReplace": "Rauswerfen + Ersetzen", - "replaceWithBot": "{{name}} durch Bot ersetzen" + "replaceWithBot": "{{name}} durch Bot ersetzen", + "endDraft": "Draft beenden", + "endDraftConfirm": "Diesen Draft fuer alle beenden? Das kann nicht rueckgaengig gemacht werden." }, "manaCurve": { "title": "Manakurve" @@ -60,7 +62,8 @@ "botDifficulty": "Bot-Schwierigkeit", "chooseSet": "Wähle ein Set", "noPools": "Keine Draft-Pools verfügbar. Führe zuerst die Draft-Datenpipeline aus.", - "setIconAlt": "{{name}} Set-Symbol" + "setIconAlt": "{{name}} Set-Symbol", + "loadFailed": "Sets konnten nicht geladen werden" }, "pack": { "confirmPick": "Pick bestätigen", diff --git a/client/src/i18n/locales/de/game.json b/client/src/i18n/locales/de/game.json index 0e630c2558..a8f19634ce 100644 --- a/client/src/i18n/locales/de/game.json +++ b/client/src/i18n/locales/de/game.json @@ -8,6 +8,8 @@ "preview": { "holdCtrlBack": "Strg halten für Rückseite", "holdCtrlFront": "Strg halten für Vorderseite", + "holdCtrlFlip": "Strg halten zum Umdrehen", + "flip": "Umdrehen", "altParsedAbilities": "Alt: eingelesene Fähigkeiten", "debugId": "ID: {{id}}", "engineParse": "Engine-Parse", @@ -307,7 +309,9 @@ "statHand": "Hand", "statCreatures": "Kreaturen", "statLands": "Länder", - "statOther": "Sonstige" + "statOther": "Sonstige", + "compactHud": "Kompaktes Gegner-HUD. Reduziert jeden Gegner auf eine einzelne schmale Zeile (Name + Leben); tippe auf einen Gegner, um sein gesamtes Spielfeld anzuzeigen.", + "expandHud": "Gegner-HUD erweitern. Zeigt die Handkarten und die Spielfeldaufschlüsselung jedes Gegners." }, "combat": { "attackAll": "Alle angreifen", @@ -380,7 +384,9 @@ "nounTarget": "Ziel", "confirmTap": "Tappen bestätigen ({{selected}}/{{count}})", "keepCurrentTargets": "Aktuelle Ziele behalten", - "skip": "Überspringen" + "skip": "Überspringen", + "chooseNewTarget": "Wähle das neue Ziel des Zauberspruchs", + "chooseNewTargetForSpell": "Wähle ein neues Ziel für {{spell}}" }, "log": { "title": "Spielprotokoll", @@ -1247,6 +1253,9 @@ "proliferateSubtitle": "Wähle eine beliebige Anzahl bleibender Karten und Spieler mit Marken. Jedes gewählte Ziel erhält eine weitere Marke jeder Art, die bereits dort vorhanden ist.", "chooseObjectsTitle": "Bleibende Karten wählen", "chooseObjectsSubtitle": "Wähle eine beliebige Anzahl bleibender Karten. Du zahlst für jede gewählte Karte Kosten.", + "selectAll": "Alle", + "selectNone": "Keine", + "selectMine": "Meine Seite", "confirm": "Bestätigen" }, "replacement": { @@ -1260,7 +1269,8 @@ "scopeSingle": "Wähle ein neues Ziel für den Zauberspruch", "scopeMulti": "Wähle neue Ziele für den Zauberspruch", "subtitle": "{{scope}}. Aktuell: {{current}}", - "confirm": "Bestätigen" + "confirm": "Bestätigen", + "badgeNewTarget": "Neues Ziel" }, "separatePiles": { "chooserName": "Spieler {{number}}", @@ -1321,5 +1331,27 @@ "subtitleLabel": "Wähle eine Kennzeichnung für {{name}}", "subtitleVoteRemaining": "Gib eine Stimme ab ({{count}} verbleibend)", "subtitleVote": "Gib deine Stimme ab" + }, + "advance": { + "resolve": "Auflösen", + "passPriority": "Priorität abgeben", + "toPhase": "Zu {{phase}}" + }, + "outsideGame": { + "fromExile": "Aus dem Exil", + "fromSideboard": "Aus dem Sideboard" + }, + "phaseName": { + "Upkeep": "Versorgung", + "Draw": "Ziehen", + "PreCombatMain": "Hauptphase 1", + "BeginCombat": "Kampfbeginn", + "DeclareAttackers": "Angreifer deklarieren", + "DeclareBlockers": "Blocker deklarieren", + "CombatDamage": "Kampfschaden", + "EndCombat": "Kampfende", + "PostCombatMain": "Hauptphase 2", + "End": "Endschritt", + "Cleanup": "Aufräumen" } } diff --git a/client/src/i18n/locales/de/menu.json b/client/src/i18n/locales/de/menu.json index 57fa25a17d..34f7158ddd 100644 --- a/client/src/i18n/locales/de/menu.json +++ b/client/src/i18n/locales/de/menu.json @@ -4,7 +4,14 @@ }, "aiDifficulty": { "label": "KI-Schwierigkeit", - "ariaLabel": "KI-Schwierigkeit: {{difficulty}}" + "ariaLabel": "KI-Schwierigkeit: {{difficulty}}", + "levels": { + "VeryEasy": "Sehr leicht", + "Easy": "Leicht", + "Medium": "Mittel", + "Hard": "Schwer", + "VeryHard": "Sehr schwer" + } }, "bracketFilter": { "label": "Bracket-Filter" @@ -205,7 +212,8 @@ "browseAll": "Alle durchsuchen", "loadMore": "Mehr laden", "selectedDeck": "Ausgewähltes Deck", - "chooseDeckToContinue": "Wähle ein Deck, um fortzufahren" + "chooseDeckToContinue": "Wähle ein Deck, um fortzufahren", + "filterAll": "Alle" }, "subscriptions": { "emptyTitle": "Keine Feed-Abonnements", @@ -213,5 +221,13 @@ "feedMeta_one": "· {{count}} Deck · Aktualisiert {{date}}", "feedMeta_other": "· {{count}} Decks · Aktualisiert {{date}}", "error": "Fehler: {{error}}" + }, + "myDecksPage": { + "eyebrow": "Decks", + "title": "Decks.", + "description": "Öffne eine gespeicherte Liste, importiere eine neue oder mach im Deck-Builder weiter." + }, + "loadGameState": { + "readFailed": "Datei konnte nicht gelesen werden" } } diff --git a/client/src/i18n/locales/de/settings.json b/client/src/i18n/locales/de/settings.json index 0264415295..32f9e228fc 100644 --- a/client/src/i18n/locales/de/settings.json +++ b/client/src/i18n/locales/de/settings.json @@ -32,6 +32,15 @@ "random": "Zufällig", "customUrl": "Eigene URL", "none": "Keiner" + }, + "cardSizeOptions": { + "small": "Klein", + "medium": "Mittel", + "large": "Groß" + }, + "logDefaultOptions": { + "open": "Offen", + "closed": "Geschlossen" } }, "visual": { @@ -45,7 +54,12 @@ "clearArtOverrides_one": "Alle Artwork-Überschreibungen aufheben ({{count}})", "clearArtOverrides_other": "Alle Artwork-Überschreibungen aufheben ({{count}})", "clearArtOverridesConfirm_one": "Alle {{count}} Artwork-Überschreibung aufheben?", - "clearArtOverridesConfirm_other": "Alle {{count}} Artwork-Überschreibungen aufheben?" + "clearArtOverridesConfirm_other": "Alle {{count}} Artwork-Überschreibungen aufheben?", + "vfxQualityOptions": { + "full": "Voll", + "reduced": "Reduziert", + "minimal": "Minimal" + } }, "audio": { "title": "Audio", @@ -106,7 +120,17 @@ "slowest": "Am langsamsten", "resetSliderLabel": "{{label}} auf Standard zurücksetzen", "atDefault": "Auf Standard", - "resetToDefault": "Auf Standard zurücksetzen" + "resetToDefault": "Auf Standard zurücksetzen", + "labels": { + "effects": "Effekt-Tempo", + "combat": "Kampf-Tempo", + "banners": "Banner-Tempo" + }, + "descriptions": { + "effects": "Zaubersprüche, Zonenwechsel, Tode, Lebenspunkteänderungen, Marken, Tappen/Enttappen.", + "combat": "Zeitpunkt des Kampfschadens — wie lange Blocker und Angreifer verweilen, bevor der Schaden abgehandelt wird.", + "banners": "Anzeige des Banners zu Zugbeginn." + } }, "artChain": { "emptyState": "Standard-Scryfall-Artwork wird verwendet. Füge unten Regeln hinzu, um anzupassen.", diff --git a/client/src/i18n/locales/en/common.json b/client/src/i18n/locales/en/common.json index e4409db870..3e513cd883 100644 --- a/client/src/i18n/locales/en/common.json +++ b/client/src/i18n/locales/en/common.json @@ -198,5 +198,11 @@ }, "turnBanner": { "turn": "Turn {{number}}" + }, + "chrome": { + "back": "Back", + "settings": "Settings", + "languageSettings": "Language ({{lang}}) — open settings", + "languageTitle": "Language: {{lang}}" } } diff --git a/client/src/i18n/locales/en/draft.json b/client/src/i18n/locales/en/draft.json index d6f5f45bd9..3dc186512c 100644 --- a/client/src/i18n/locales/en/draft.json +++ b/client/src/i18n/locales/en/draft.json @@ -39,7 +39,9 @@ "overrideResult": "Override Result", "versusPair": "{{a}} v {{b}}", "kickReplace": "Kick + Replace", - "replaceWithBot": "Replace {{name}} with Bot" + "replaceWithBot": "Replace {{name}} with Bot", + "endDraft": "End Draft", + "endDraftConfirm": "End this draft for everyone? This cannot be undone." }, "manaCurve": { "title": "Mana Curve" @@ -60,7 +62,8 @@ "botDifficulty": "Bot Difficulty", "chooseSet": "Choose a Set", "noPools": "No draft pools available. Run the draft data pipeline first.", - "setIconAlt": "{{name}} set icon" + "setIconAlt": "{{name}} set icon", + "loadFailed": "Failed to load sets" }, "pack": { "confirmPick": "Confirm Pick", diff --git a/client/src/i18n/locales/en/game.json b/client/src/i18n/locales/en/game.json index 8608bf6c17..5db3918a59 100644 --- a/client/src/i18n/locales/en/game.json +++ b/client/src/i18n/locales/en/game.json @@ -8,6 +8,8 @@ "preview": { "holdCtrlBack": "Hold Ctrl for back face", "holdCtrlFront": "Hold Ctrl for front face", + "holdCtrlFlip": "Hold Ctrl to flip", + "flip": "Flip", "altParsedAbilities": "Alt: parsed abilities", "debugId": "ID: {{id}}", "engineParse": "Engine Parse", @@ -1251,6 +1253,9 @@ "proliferateSubtitle": "Choose any number of permanents and players with counters. Each chosen target gets one more counter of each kind already there.", "chooseObjectsTitle": "Choose Permanents", "chooseObjectsSubtitle": "Choose any number of permanents. You pay a cost for each one chosen.", + "selectAll": "All", + "selectNone": "None", + "selectMine": "My side", "confirm": "Confirm" }, "replacement": { @@ -1326,5 +1331,27 @@ "subtitleLabel": "Choose a label for {{name}}", "subtitleVoteRemaining": "Cast a vote ({{count}} remaining)", "subtitleVote": "Cast your vote" + }, + "advance": { + "resolve": "Resolve", + "passPriority": "Pass Priority", + "toPhase": "To {{phase}}" + }, + "outsideGame": { + "fromExile": "From exile", + "fromSideboard": "From sideboard" + }, + "phaseName": { + "Upkeep": "Upkeep", + "Draw": "Draw", + "PreCombatMain": "Main Phase 1", + "BeginCombat": "Begin Combat", + "DeclareAttackers": "Declare Attackers", + "DeclareBlockers": "Declare Blockers", + "CombatDamage": "Combat Damage", + "EndCombat": "End Combat", + "PostCombatMain": "Main Phase 2", + "End": "End Step", + "Cleanup": "Cleanup" } } diff --git a/client/src/i18n/locales/en/menu.json b/client/src/i18n/locales/en/menu.json index 4d8f92044d..19efcc2140 100644 --- a/client/src/i18n/locales/en/menu.json +++ b/client/src/i18n/locales/en/menu.json @@ -4,7 +4,14 @@ }, "aiDifficulty": { "label": "AI difficulty", - "ariaLabel": "AI difficulty: {{difficulty}}" + "ariaLabel": "AI difficulty: {{difficulty}}", + "levels": { + "VeryEasy": "Very Easy", + "Easy": "Easy", + "Medium": "Medium", + "Hard": "Hard", + "VeryHard": "Very Hard" + } }, "bracketFilter": { "label": "Bracket filter" @@ -205,7 +212,8 @@ "browseAll": "Browse All", "loadMore": "Load More", "selectedDeck": "Selected deck", - "chooseDeckToContinue": "Choose a deck to continue" + "chooseDeckToContinue": "Choose a deck to continue", + "filterAll": "All" }, "subscriptions": { "emptyTitle": "No feed subscriptions", @@ -213,5 +221,13 @@ "feedMeta_one": "· {{count}} deck · Updated {{date}}", "feedMeta_other": "· {{count}} decks · Updated {{date}}", "error": "Error: {{error}}" + }, + "myDecksPage": { + "eyebrow": "Decks", + "title": "Decks.", + "description": "Open a saved list, import a new one, or continue in deck builder." + }, + "loadGameState": { + "readFailed": "Failed to read file" } } diff --git a/client/src/i18n/locales/en/settings.json b/client/src/i18n/locales/en/settings.json index d6daab5b0d..1be4806a2c 100644 --- a/client/src/i18n/locales/en/settings.json +++ b/client/src/i18n/locales/en/settings.json @@ -32,6 +32,15 @@ "random": "Random", "customUrl": "Custom URL", "none": "None" + }, + "cardSizeOptions": { + "small": "Small", + "medium": "Medium", + "large": "Large" + }, + "logDefaultOptions": { + "open": "Open", + "closed": "Closed" } }, "visual": { @@ -45,7 +54,12 @@ "clearArtOverrides_one": "Clear All Art Overrides ({{count}})", "clearArtOverrides_other": "Clear All Art Overrides ({{count}})", "clearArtOverridesConfirm_one": "Clear all {{count}} art override?", - "clearArtOverridesConfirm_other": "Clear all {{count}} art overrides?" + "clearArtOverridesConfirm_other": "Clear all {{count}} art overrides?", + "vfxQualityOptions": { + "full": "Full", + "reduced": "Reduced", + "minimal": "Minimal" + } }, "audio": { "title": "Audio", @@ -106,7 +120,17 @@ "slowest": "Slowest", "resetSliderLabel": "Reset {{label}} to default", "atDefault": "At default", - "resetToDefault": "Reset to default" + "resetToDefault": "Reset to default", + "labels": { + "effects": "Effect Pacing", + "combat": "Combat Pacing", + "banners": "Banner Pacing" + }, + "descriptions": { + "effects": "Spell casts, zone changes, deaths, life changes, counters, tap/untap.", + "combat": "Combat damage timing — how long blockers and attackers linger before damage resolves.", + "banners": "Turn-start banner display." + } }, "artChain": { "emptyState": "Using default Scryfall art. Add rules below to customize.", diff --git a/client/src/i18n/locales/es/common.json b/client/src/i18n/locales/es/common.json index 7a818962f7..3476212a86 100644 --- a/client/src/i18n/locales/es/common.json +++ b/client/src/i18n/locales/es/common.json @@ -198,5 +198,11 @@ }, "turnBanner": { "turn": "Turno {{number}}" + }, + "chrome": { + "back": "Atrás", + "settings": "Ajustes", + "languageSettings": "Idioma ({{lang}}) — abrir ajustes", + "languageTitle": "Idioma: {{lang}}" } } diff --git a/client/src/i18n/locales/es/draft.json b/client/src/i18n/locales/es/draft.json index 76ff2003ec..4a43163a6c 100644 --- a/client/src/i18n/locales/es/draft.json +++ b/client/src/i18n/locales/es/draft.json @@ -39,7 +39,9 @@ "overrideResult": "Anular resultado", "versusPair": "{{a}} contra {{b}}", "kickReplace": "Expulsar + reemplazar", - "replaceWithBot": "Reemplazar a {{name}} con un bot" + "replaceWithBot": "Reemplazar a {{name}} con un bot", + "endDraft": "Terminar draft", + "endDraftConfirm": "¿Terminar este draft para todos? Esta acción no se puede deshacer." }, "manaCurve": { "title": "Curva de maná" @@ -60,7 +62,8 @@ "botDifficulty": "Dificultad de los bots", "chooseSet": "Elige una colección", "noPools": "No hay grupos de draft disponibles. Ejecuta primero la canalización de datos del draft.", - "setIconAlt": "Icono de la colección {{name}}" + "setIconAlt": "Icono de la colección {{name}}", + "loadFailed": "Error al cargar las colecciones" }, "pack": { "confirmPick": "Confirmar elección", diff --git a/client/src/i18n/locales/es/game.json b/client/src/i18n/locales/es/game.json index f22ebb2b0c..53cdf6298e 100644 --- a/client/src/i18n/locales/es/game.json +++ b/client/src/i18n/locales/es/game.json @@ -8,6 +8,8 @@ "preview": { "holdCtrlBack": "Mantén Ctrl para la cara trasera", "holdCtrlFront": "Mantén Ctrl para la cara frontal", + "holdCtrlFlip": "Mantén Ctrl para girar", + "flip": "Girar", "altParsedAbilities": "Alt: habilidades analizadas", "debugId": "ID: {{id}}", "engineParse": "Análisis del motor", @@ -307,7 +309,9 @@ "statHand": "Mano", "statCreatures": "Criaturas", "statLands": "Tierras", - "statOther": "Otros" + "statOther": "Otros", + "compactHud": "HUD de oponente compacto. Contrae cada oponente a una sola fila fina (nombre + vidas); toca un oponente para enfocar todo su campo de batalla.", + "expandHud": "Expandir HUD de oponente. Muestra la mano y el desglose del campo de batalla de cada oponente." }, "combat": { "attackAll": "Atacar a todos", @@ -380,7 +384,9 @@ "nounTarget": "objetivo", "confirmTap": "Confirmar giro ({{selected}}/{{count}})", "keepCurrentTargets": "Conservar objetivos actuales", - "skip": "Omitir" + "skip": "Omitir", + "chooseNewTarget": "Elige el nuevo objetivo del hechizo", + "chooseNewTargetForSpell": "Elige un nuevo objetivo para {{spell}}" }, "log": { "title": "Registro de la partida", @@ -1247,6 +1253,9 @@ "proliferateSubtitle": "Elige cualquier número de permanentes y jugadores con contadores. Cada objetivo elegido recibe un contador más de cada tipo que ya tenga.", "chooseObjectsTitle": "Elegir permanentes", "chooseObjectsSubtitle": "Elige cualquier número de permanentes. Pagas un coste por cada uno elegido.", + "selectAll": "Todos", + "selectNone": "Ninguno", + "selectMine": "Mi lado", "confirm": "Confirmar" }, "replacement": { @@ -1260,7 +1269,8 @@ "scopeSingle": "Elige un nuevo objetivo para el hechizo", "scopeMulti": "Elige nuevos objetivos para el hechizo", "subtitle": "{{scope}}. Actual: {{current}}", - "confirm": "Confirmar" + "confirm": "Confirmar", + "badgeNewTarget": "Nuevo objetivo" }, "separatePiles": { "chooserName": "Jugador {{number}}", @@ -1321,5 +1331,27 @@ "subtitleLabel": "Elige una etiqueta para {{name}}", "subtitleVoteRemaining": "Emite un voto ({{count}} restantes)", "subtitleVote": "Emite tu voto" + }, + "advance": { + "resolve": "Resolver", + "passPriority": "Pasar prioridad", + "toPhase": "A {{phase}}" + }, + "outsideGame": { + "fromExile": "Desde el exilio", + "fromSideboard": "Desde el banquillo" + }, + "phaseName": { + "Upkeep": "Mantenimiento", + "Draw": "Robar", + "PreCombatMain": "Fase principal 1", + "BeginCombat": "Inicio del combate", + "DeclareAttackers": "Declarar atacantes", + "DeclareBlockers": "Declarar bloqueadores", + "CombatDamage": "Daño de combate", + "EndCombat": "Fin del combate", + "PostCombatMain": "Fase principal 2", + "End": "Paso final", + "Cleanup": "Limpieza" } } diff --git a/client/src/i18n/locales/es/menu.json b/client/src/i18n/locales/es/menu.json index 0829d66de4..dc4f2b027e 100644 --- a/client/src/i18n/locales/es/menu.json +++ b/client/src/i18n/locales/es/menu.json @@ -4,7 +4,14 @@ }, "aiDifficulty": { "label": "Dificultad de la IA", - "ariaLabel": "Dificultad de la IA: {{difficulty}}" + "ariaLabel": "Dificultad de la IA: {{difficulty}}", + "levels": { + "VeryEasy": "Muy fácil", + "Easy": "Fácil", + "Medium": "Media", + "Hard": "Difícil", + "VeryHard": "Muy difícil" + } }, "bracketFilter": { "label": "Filtro de nivel" @@ -205,7 +212,8 @@ "browseAll": "Explorar todos", "loadMore": "Cargar más", "selectedDeck": "Mazo seleccionado", - "chooseDeckToContinue": "Elige un mazo para continuar" + "chooseDeckToContinue": "Elige un mazo para continuar", + "filterAll": "Todos" }, "subscriptions": { "emptyTitle": "Sin suscripciones a feeds", @@ -213,5 +221,13 @@ "feedMeta_one": "· {{count}} mazo · Actualizado el {{date}}", "feedMeta_other": "· {{count}} mazos · Actualizado el {{date}}", "error": "Error: {{error}}" + }, + "myDecksPage": { + "eyebrow": "Mazos", + "title": "Mazos.", + "description": "Abre una lista guardada, importa una nueva o continúa en el editor de mazos." + }, + "loadGameState": { + "readFailed": "Error al leer el archivo" } } diff --git a/client/src/i18n/locales/es/settings.json b/client/src/i18n/locales/es/settings.json index c024373f65..c9851d26de 100644 --- a/client/src/i18n/locales/es/settings.json +++ b/client/src/i18n/locales/es/settings.json @@ -32,6 +32,15 @@ "random": "Aleatorio", "customUrl": "URL personalizada", "none": "Ninguno" + }, + "cardSizeOptions": { + "small": "Pequeño", + "medium": "Mediano", + "large": "Grande" + }, + "logDefaultOptions": { + "open": "Abierto", + "closed": "Cerrado" } }, "visual": { @@ -45,7 +54,12 @@ "clearArtOverrides_one": "Borrar todas las anulaciones de ilustración ({{count}})", "clearArtOverrides_other": "Borrar todas las anulaciones de ilustración ({{count}})", "clearArtOverridesConfirm_one": "¿Borrar la anulación de ilustración? ({{count}})", - "clearArtOverridesConfirm_other": "¿Borrar las {{count}} anulaciones de ilustración?" + "clearArtOverridesConfirm_other": "¿Borrar las {{count}} anulaciones de ilustración?", + "vfxQualityOptions": { + "full": "Completa", + "reduced": "Reducida", + "minimal": "Mínima" + } }, "audio": { "title": "Audio", @@ -106,7 +120,17 @@ "slowest": "Lo más lento", "resetSliderLabel": "Restablecer {{label}} al valor predeterminado", "atDefault": "En el valor predeterminado", - "resetToDefault": "Restablecer al valor predeterminado" + "resetToDefault": "Restablecer al valor predeterminado", + "labels": { + "effects": "Ritmo de efectos", + "combat": "Ritmo de combate", + "banners": "Ritmo de carteles" + }, + "descriptions": { + "effects": "Lanzamientos de hechizos, cambios de zona, muertes, cambios de vida, contadores, girar/enderezar.", + "combat": "Tiempo del daño de combate: cuánto permanecen bloqueadores y atacantes antes de que se resuelva el daño.", + "banners": "Visualización del cartel de inicio de turno." + } }, "artChain": { "emptyState": "Usando la ilustración predeterminada de Scryfall. Añade reglas a continuación para personalizar.", diff --git a/client/src/i18n/locales/fr/common.json b/client/src/i18n/locales/fr/common.json index 22dcbe4d88..76565137e4 100644 --- a/client/src/i18n/locales/fr/common.json +++ b/client/src/i18n/locales/fr/common.json @@ -198,5 +198,11 @@ }, "turnBanner": { "turn": "Tour {{number}}" + }, + "chrome": { + "back": "Retour", + "settings": "Paramètres", + "languageSettings": "Langue ({{lang}}) — ouvrir les paramètres", + "languageTitle": "Langue : {{lang}}" } } diff --git a/client/src/i18n/locales/fr/draft.json b/client/src/i18n/locales/fr/draft.json index 417597fcfd..7662ad16a1 100644 --- a/client/src/i18n/locales/fr/draft.json +++ b/client/src/i18n/locales/fr/draft.json @@ -39,7 +39,9 @@ "overrideResult": "Modifier le résultat", "versusPair": "{{a}} c. {{b}}", "kickReplace": "Exclure + Remplacer", - "replaceWithBot": "Remplacer {{name}} par un bot" + "replaceWithBot": "Remplacer {{name}} par un bot", + "endDraft": "Terminer le draft", + "endDraftConfirm": "Terminer ce draft pour tout le monde ? Cette action est irreversible." }, "manaCurve": { "title": "Courbe de mana" @@ -60,7 +62,8 @@ "botDifficulty": "Difficulté des bots", "chooseSet": "Choisir une extension", "noPools": "Aucun pool de draft disponible. Lancez d'abord le pipeline de données de draft.", - "setIconAlt": "Icône d'extension {{name}}" + "setIconAlt": "Icône d'extension {{name}}", + "loadFailed": "Échec du chargement des éditions" }, "pack": { "confirmPick": "Confirmer le choix", diff --git a/client/src/i18n/locales/fr/game.json b/client/src/i18n/locales/fr/game.json index 1c09fd3e51..3faccc579b 100644 --- a/client/src/i18n/locales/fr/game.json +++ b/client/src/i18n/locales/fr/game.json @@ -8,6 +8,8 @@ "preview": { "holdCtrlBack": "Maintenez Ctrl pour la face arrière", "holdCtrlFront": "Maintenez Ctrl pour la face avant", + "holdCtrlFlip": "Maintenez Ctrl pour pivoter", + "flip": "Pivoter", "altParsedAbilities": "Alt : capacités analysées", "debugId": "ID : {{id}}", "engineParse": "Analyse du moteur", @@ -307,7 +309,9 @@ "statHand": "Main", "statCreatures": "Créatures", "statLands": "Terrains", - "statOther": "Autre" + "statOther": "Autre", + "compactHud": "HUD d'adversaire compact. Réduit chaque adversaire à une seule ligne fine (nom + points de vie) ; touchez un adversaire pour afficher tout son champ de bataille.", + "expandHud": "Développer le HUD d'adversaire. Affiche la main et le détail du champ de bataille de chaque adversaire." }, "combat": { "attackAll": "Attaquer tous", @@ -380,7 +384,9 @@ "nounTarget": "cible", "confirmTap": "Confirmer l'engagement ({{selected}}/{{count}})", "keepCurrentTargets": "Conserver les cibles actuelles", - "skip": "Ignorer" + "skip": "Ignorer", + "chooseNewTarget": "Choisissez la nouvelle cible du sort", + "chooseNewTargetForSpell": "Choisissez une nouvelle cible pour {{spell}}" }, "log": { "title": "Journal de partie", @@ -1247,6 +1253,9 @@ "proliferateSubtitle": "Choisissez n'importe quel nombre de permanents et de joueurs ayant des marqueurs. Chaque cible choisie reçoit un marqueur de plus de chaque sorte déjà présente.", "chooseObjectsTitle": "Choisir des permanents", "chooseObjectsSubtitle": "Choisissez n'importe quel nombre de permanents. Vous payez un coût pour chacun choisi.", + "selectAll": "Tout", + "selectNone": "Aucun", + "selectMine": "Mon côté", "confirm": "Confirmer" }, "replacement": { @@ -1260,7 +1269,8 @@ "scopeSingle": "Choisissez une nouvelle cible pour le sort", "scopeMulti": "Choisissez de nouvelles cibles pour le sort", "subtitle": "{{scope}}. Actuelle : {{current}}", - "confirm": "Confirmer" + "confirm": "Confirmer", + "badgeNewTarget": "Nouvelle cible" }, "separatePiles": { "chooserName": "Joueur {{number}}", @@ -1321,5 +1331,27 @@ "subtitleLabel": "Choisissez une étiquette pour {{name}}", "subtitleVoteRemaining": "Votez ({{count}} restant(s))", "subtitleVote": "Exprimez votre vote" + }, + "advance": { + "resolve": "Résoudre", + "passPriority": "Passer la priorité", + "toPhase": "Vers {{phase}}" + }, + "outsideGame": { + "fromExile": "Depuis l'exil", + "fromSideboard": "Depuis la réserve" + }, + "phaseName": { + "Upkeep": "Entretien", + "Draw": "Pioche", + "PreCombatMain": "Phase principale 1", + "BeginCombat": "Début de combat", + "DeclareAttackers": "Déclaration des attaquants", + "DeclareBlockers": "Déclaration des bloqueurs", + "CombatDamage": "Blessures de combat", + "EndCombat": "Fin de combat", + "PostCombatMain": "Phase principale 2", + "End": "Étape de fin", + "Cleanup": "Nettoyage" } } diff --git a/client/src/i18n/locales/fr/menu.json b/client/src/i18n/locales/fr/menu.json index b4fff56628..9f4dc9224d 100644 --- a/client/src/i18n/locales/fr/menu.json +++ b/client/src/i18n/locales/fr/menu.json @@ -4,7 +4,14 @@ }, "aiDifficulty": { "label": "Difficulté de l'IA", - "ariaLabel": "Difficulté de l'IA : {{difficulty}}" + "ariaLabel": "Difficulté de l'IA : {{difficulty}}", + "levels": { + "VeryEasy": "Très facile", + "Easy": "Facile", + "Medium": "Moyen", + "Hard": "Difficile", + "VeryHard": "Très difficile" + } }, "bracketFilter": { "label": "Filtre par palier" @@ -205,7 +212,8 @@ "browseAll": "Tout parcourir", "loadMore": "Charger plus", "selectedDeck": "Deck sélectionné", - "chooseDeckToContinue": "Choisissez un deck pour continuer" + "chooseDeckToContinue": "Choisissez un deck pour continuer", + "filterAll": "Tous" }, "subscriptions": { "emptyTitle": "Aucun abonnement à un flux", @@ -213,5 +221,13 @@ "feedMeta_one": "· {{count}} deck · Mis à jour le {{date}}", "feedMeta_other": "· {{count}} decks · Mis à jour le {{date}}", "error": "Erreur : {{error}}" + }, + "myDecksPage": { + "eyebrow": "Decks", + "title": "Decks.", + "description": "Ouvrez une liste enregistrée, importez-en une nouvelle ou continuez dans l'éditeur de deck." + }, + "loadGameState": { + "readFailed": "Échec de la lecture du fichier" } } diff --git a/client/src/i18n/locales/fr/settings.json b/client/src/i18n/locales/fr/settings.json index b3e13fc25d..a04854c409 100644 --- a/client/src/i18n/locales/fr/settings.json +++ b/client/src/i18n/locales/fr/settings.json @@ -4,7 +4,7 @@ "subtitle": "Réglez le gameplay, les visuels, l'audio et les valeurs par défaut du multijoueur." }, "tabs": { - "gameplay": "Gameplay", + "gameplay": "Jouabilité", "visual": "Visuel", "combat": "Rythme", "audio": "Audio", @@ -13,7 +13,7 @@ "experimental": "Expérimental" }, "gameplay": { - "title": "Gameplay", + "title": "Jouabilité", "language": "Langue", "cardSize": "Taille des cartes", "logDefault": "Journal par défaut", @@ -32,6 +32,15 @@ "random": "Aléatoire", "customUrl": "URL personnalisée", "none": "Aucun" + }, + "cardSizeOptions": { + "small": "Petit", + "medium": "Moyen", + "large": "Grand" + }, + "logDefaultOptions": { + "open": "Ouvert", + "closed": "Fermé" } }, "visual": { @@ -45,7 +54,12 @@ "clearArtOverrides_one": "Effacer tous les remplacements d'illustration ({{count}})", "clearArtOverrides_other": "Effacer tous les remplacements d'illustration ({{count}})", "clearArtOverridesConfirm_one": "Effacer les {{count}} remplacement d'illustration ?", - "clearArtOverridesConfirm_other": "Effacer les {{count}} remplacements d'illustration ?" + "clearArtOverridesConfirm_other": "Effacer les {{count}} remplacements d'illustration ?", + "vfxQualityOptions": { + "full": "Complète", + "reduced": "Réduite", + "minimal": "Minimale" + } }, "audio": { "title": "Audio", @@ -106,7 +120,17 @@ "slowest": "Le plus lent", "resetSliderLabel": "Réinitialiser {{label}} par défaut", "atDefault": "Valeur par défaut", - "resetToDefault": "Réinitialiser par défaut" + "resetToDefault": "Réinitialiser par défaut", + "labels": { + "effects": "Rythme des effets", + "combat": "Rythme du combat", + "banners": "Rythme des bannières" + }, + "descriptions": { + "effects": "Lancements de sorts, changements de zone, morts, changements de points de vie, marqueurs, engagement/dégagement.", + "combat": "Minutage des blessures de combat : combien de temps les bloqueurs et les attaquants restent avant que les blessures soient infligées.", + "banners": "Affichage de la bannière de début de tour." + } }, "artChain": { "emptyState": "Utilisation de l'illustration Scryfall par défaut. Ajoutez des règles ci-dessous pour personnaliser.", diff --git a/client/src/i18n/locales/it/common.json b/client/src/i18n/locales/it/common.json index 76e7ebcff1..b704f18c03 100644 --- a/client/src/i18n/locales/it/common.json +++ b/client/src/i18n/locales/it/common.json @@ -198,5 +198,11 @@ }, "turnBanner": { "turn": "Turno {{number}}" + }, + "chrome": { + "back": "Indietro", + "settings": "Impostazioni", + "languageSettings": "Lingua ({{lang}}) — apri impostazioni", + "languageTitle": "Lingua: {{lang}}" } } diff --git a/client/src/i18n/locales/it/draft.json b/client/src/i18n/locales/it/draft.json index 7892a88600..4975cdabd0 100644 --- a/client/src/i18n/locales/it/draft.json +++ b/client/src/i18n/locales/it/draft.json @@ -39,7 +39,9 @@ "overrideResult": "Sovrascrivi risultato", "versusPair": "{{a}} v {{b}}", "kickReplace": "Espelli + sostituisci", - "replaceWithBot": "Sostituisci {{name}} con un bot" + "replaceWithBot": "Sostituisci {{name}} con un bot", + "endDraft": "Termina draft", + "endDraftConfirm": "Terminare questo draft per tutti? L'azione non puo essere annullata." }, "manaCurve": { "title": "Curva del mana" @@ -60,7 +62,8 @@ "botDifficulty": "Difficoltà bot", "chooseSet": "Scegli un set", "noPools": "Nessun pool di draft disponibile. Esegui prima la pipeline dei dati di draft.", - "setIconAlt": "Icona set {{name}}" + "setIconAlt": "Icona set {{name}}", + "loadFailed": "Impossibile caricare i set" }, "pack": { "confirmPick": "Conferma scelta", diff --git a/client/src/i18n/locales/it/game.json b/client/src/i18n/locales/it/game.json index aae64166b7..e6d30c1468 100644 --- a/client/src/i18n/locales/it/game.json +++ b/client/src/i18n/locales/it/game.json @@ -8,6 +8,8 @@ "preview": { "holdCtrlBack": "Tieni premuto Ctrl per il retro", "holdCtrlFront": "Tieni premuto Ctrl per il fronte", + "holdCtrlFlip": "Tieni premuto Ctrl per girare", + "flip": "Gira", "altParsedAbilities": "Alt: abilità analizzate", "debugId": "ID: {{id}}", "engineParse": "Analisi del motore", @@ -307,7 +309,9 @@ "statHand": "Mano", "statCreatures": "Creature", "statLands": "Terre", - "statOther": "Altro" + "statOther": "Altro", + "compactHud": "HUD avversario compatto. Riduce ogni avversario a una singola riga sottile (nome + punti vita); tocca un avversario per visualizzare l'intero campo di battaglia.", + "expandHud": "Espandi HUD avversario. Mostra la mano e il dettaglio del campo di battaglia di ogni avversario." }, "combat": { "attackAll": "Attacca tutti", @@ -380,7 +384,9 @@ "nounTarget": "bersaglio", "confirmTap": "Conferma TAP ({{selected}}/{{count}})", "keepCurrentTargets": "Mantieni i bersagli attuali", - "skip": "Salta" + "skip": "Salta", + "chooseNewTarget": "Scegli il nuovo bersaglio della magia", + "chooseNewTargetForSpell": "Scegli un nuovo bersaglio per {{spell}}" }, "log": { "title": "Registro di gioco", @@ -1247,6 +1253,9 @@ "proliferateSubtitle": "Scegli un numero qualsiasi di permanenti e giocatori con segnalini. Ogni bersaglio scelto riceve un altro segnalino di ciascun tipo già presente.", "chooseObjectsTitle": "Scegli permanenti", "chooseObjectsSubtitle": "Scegli un numero qualsiasi di permanenti. Paghi un costo per ciascuno scelto.", + "selectAll": "Tutti", + "selectNone": "Nessuno", + "selectMine": "Il mio lato", "confirm": "Conferma" }, "replacement": { @@ -1260,7 +1269,8 @@ "scopeSingle": "Scegli un nuovo bersaglio per la magia", "scopeMulti": "Scegli nuovi bersagli per la magia", "subtitle": "{{scope}}. Attuale: {{current}}", - "confirm": "Conferma" + "confirm": "Conferma", + "badgeNewTarget": "Nuovo bersaglio" }, "separatePiles": { "chooserName": "Giocatore {{number}}", @@ -1321,5 +1331,27 @@ "subtitleLabel": "Scegli un'etichetta per {{name}}", "subtitleVoteRemaining": "Esprimi un voto ({{count}} rimanenti)", "subtitleVote": "Esprimi il tuo voto" + }, + "advance": { + "resolve": "Risolvi", + "passPriority": "Passa priorità", + "toPhase": "A {{phase}}" + }, + "outsideGame": { + "fromExile": "Dall'esilio", + "fromSideboard": "Dalla sideboard" + }, + "phaseName": { + "Upkeep": "Mantenimento", + "Draw": "Pesca", + "PreCombatMain": "Fase principale 1", + "BeginCombat": "Inizio combattimento", + "DeclareAttackers": "Dichiarazione attaccanti", + "DeclareBlockers": "Dichiarazione bloccanti", + "CombatDamage": "Danno da combattimento", + "EndCombat": "Fine combattimento", + "PostCombatMain": "Fase principale 2", + "End": "Sottofase finale", + "Cleanup": "Ripulitura" } } diff --git a/client/src/i18n/locales/it/menu.json b/client/src/i18n/locales/it/menu.json index 6660e30503..04d376a89e 100644 --- a/client/src/i18n/locales/it/menu.json +++ b/client/src/i18n/locales/it/menu.json @@ -4,7 +4,14 @@ }, "aiDifficulty": { "label": "Difficoltà IA", - "ariaLabel": "Difficoltà IA: {{difficulty}}" + "ariaLabel": "Difficoltà IA: {{difficulty}}", + "levels": { + "VeryEasy": "Molto facile", + "Easy": "Facile", + "Medium": "Media", + "Hard": "Difficile", + "VeryHard": "Molto difficile" + } }, "bracketFilter": { "label": "Filtro bracket" @@ -205,7 +212,8 @@ "browseAll": "Sfoglia tutti", "loadMore": "Carica altri", "selectedDeck": "Mazzo selezionato", - "chooseDeckToContinue": "Scegli un mazzo per continuare" + "chooseDeckToContinue": "Scegli un mazzo per continuare", + "filterAll": "Tutti" }, "subscriptions": { "emptyTitle": "Nessuna iscrizione ai feed", @@ -213,5 +221,13 @@ "feedMeta_one": "· {{count}} mazzo · Aggiornato {{date}}", "feedMeta_other": "· {{count}} mazzi · Aggiornato {{date}}", "error": "Errore: {{error}}" + }, + "myDecksPage": { + "eyebrow": "Mazzi", + "title": "Mazzi.", + "description": "Apri una lista salvata, importane una nuova o continua nell'editor di mazzi." + }, + "loadGameState": { + "readFailed": "Impossibile leggere il file" } } diff --git a/client/src/i18n/locales/it/settings.json b/client/src/i18n/locales/it/settings.json index 4a506e9dc7..c375f911af 100644 --- a/client/src/i18n/locales/it/settings.json +++ b/client/src/i18n/locales/it/settings.json @@ -4,7 +4,7 @@ "subtitle": "Regola gameplay, grafica, audio e impostazioni predefinite del multigiocatore." }, "tabs": { - "gameplay": "Gameplay", + "gameplay": "Giocabilità", "visual": "Grafica", "combat": "Ritmo", "audio": "Audio", @@ -13,7 +13,7 @@ "experimental": "Sperimentale" }, "gameplay": { - "title": "Gameplay", + "title": "Giocabilità", "language": "Lingua", "cardSize": "Dimensione carte", "logDefault": "Registro predefinito", @@ -32,6 +32,15 @@ "random": "Casuale", "customUrl": "URL personalizzato", "none": "Nessuno" + }, + "cardSizeOptions": { + "small": "Piccolo", + "medium": "Medio", + "large": "Grande" + }, + "logDefaultOptions": { + "open": "Aperto", + "closed": "Chiuso" } }, "visual": { @@ -45,7 +54,12 @@ "clearArtOverrides_one": "Cancella tutte le sostituzioni di illustrazione ({{count}})", "clearArtOverrides_other": "Cancella tutte le sostituzioni di illustrazione ({{count}})", "clearArtOverridesConfirm_one": "Cancellare tutte le {{count}} sostituzione di illustrazione?", - "clearArtOverridesConfirm_other": "Cancellare tutte le {{count}} sostituzioni di illustrazione?" + "clearArtOverridesConfirm_other": "Cancellare tutte le {{count}} sostituzioni di illustrazione?", + "vfxQualityOptions": { + "full": "Completa", + "reduced": "Ridotta", + "minimal": "Minima" + } }, "audio": { "title": "Audio", @@ -106,7 +120,17 @@ "slowest": "Più lento", "resetSliderLabel": "Reimposta {{label}} al valore predefinito", "atDefault": "Al valore predefinito", - "resetToDefault": "Reimposta al valore predefinito" + "resetToDefault": "Reimposta al valore predefinito", + "labels": { + "effects": "Ritmo degli effetti", + "combat": "Ritmo del combattimento", + "banners": "Ritmo degli striscioni" + }, + "descriptions": { + "effects": "Lancio di magie, cambi di zona, morti, cambi di punti vita, segnalini, TAP/STAP.", + "combat": "Tempistica del danno da combattimento: per quanto tempo bloccanti e attaccanti rimangono prima che il danno venga risolto.", + "banners": "Visualizzazione dello striscione di inizio turno." + } }, "artChain": { "emptyState": "In uso l'illustrazione predefinita di Scryfall. Aggiungi regole qui sotto per personalizzare.", diff --git a/client/src/i18n/locales/pl/common.json b/client/src/i18n/locales/pl/common.json new file mode 100644 index 0000000000..cf1479ec17 --- /dev/null +++ b/client/src/i18n/locales/pl/common.json @@ -0,0 +1,208 @@ +{ + "actions": { + "cancel": "Anuluj", + "save": "Zapisz", + "close": "Zamknij", + "closeNamed": "Zamknij {{name}}" + }, + "modal": { + "defaultEyebrow": "Narzędzie warsztatu" + }, + "pausedBanner": { + "message": "Gra wstrzymana — {{reason}}" + }, + "fullscreen": { + "enter": "Włącz pełny ekran", + "exit": "Wyłącz pełny ekran" + }, + "buildBadge": { + "downloading": "pobieranie… {{progress}}%", + "checking": "sprawdzanie…", + "updating": "aktualizowanie…", + "updatePending": "aktualizacja oczekuje po zakończeniu gry", + "cards": "karty {{age}} ({{commit}})", + "cardDataTitle": "Dane kart wygenerowane {{date}} z {{commit}}", + "checkForUpdates": "Sprawdź aktualizacje", + "updaterDebugInfo": "Informacje debugowania aktualizatora", + "updaterIssue": "Problem z aktualizatorem: {{error}}", + "updateIssue": "problem z aktualizacją", + "updated": "zaktualizowano" + }, + "gameMenu": { + "menu": "Menu gry", + "sandboxTools": "Narzędzia piaskownicy", + "sandboxToolsTitle": "Narzędzia piaskownicy — skonfiguruj dowolny stan pola bitwy (`)", + "resume": "Wznów", + "settings": "Ustawienia", + "helpShortcuts": "Pomoc i skróty", + "showAiHand": "Pokaż rękę AI", + "hideAiHand": "Ukryj rękę AI", + "concede": "Poddaj się", + "backToDraft": "Wróć do draftu", + "mainMenu": "Menu główne", + "cards": "karty {{commit}}", + "cardDataTitle": "Dane kart: {{date}}" + }, + "volume": { + "mute": "Wycisz", + "unmute": "Wyłącz wyciszenie", + "volume": "Głośność" + }, + "hostControl": { + "seatHost": "Gospodarz", + "seatPlayer": "Gracz", + "seatOpen": "Wolne", + "seatAi": "AI ({{difficulty}})", + "waiting": "Oczekiwanie…", + "seatLabel": "Miejsce {{id}}", + "addAi": "Dodaj AI", + "remove": "Usuń", + "human": "Człowiek", + "kick": "Wyrzuć", + "replaceAi": "Zastąp przez AI", + "random": "Losowo", + "connecting": "Łączenie…", + "cancelHosting": "Anuluj hostowanie", + "kickConfirm": "Wyrzucić gracza {{name}} z pokoju?", + "replaceConfirm": "Zastąpić gracza {{name}} przez AI? Spowoduje to usunięcie go z pokoju.", + "fallbackPlayerName": "Gracz {{number}}", + "seatsOccupied": "{{occupied}}/{{total}} miejsc zajętych", + "startGame": "Rozpocznij grę", + "startNow": "Rozpocznij teraz", + "fillWithAi": "Wypełnij przez AI" + }, + "grantDebug": { + "heading": "Piaskownica: uprawnienia debugowania", + "description": "Przyznaj innym graczom możliwość wykonywania akcji debugowania. Ich akcje są rejestrowane publicznie.", + "playerLabel": "Gracz {{number}}", + "revoke": "ODBIERZ", + "grant": "PRZYZNAJ" + }, + "help": { + "eyebrow": "Pomoc", + "title": "Pomoc i skróty", + "subtitle": "Cyfrowy przebieg Magic dla graczy papierowych: zatrzymania, priorytet, pasowanie i narzędzia naprawcze.", + "closeHelp": "Zamknij pomoc", + "searchPlaceholder": "Szukaj w pomocy lub skrótach", + "whatCanIDo": "Co mogę teraz zrobić?", + "recoveryTitle": "Narzędzia naprawcze", + "recoveryDescription": "Jeśli karta zachowuje się nieprawidłowo, najpierw wyeksportuj stan. Zaawansowany panel debugowania jest dostępny, gdy musisz sprawdzić lub dostosować grę.", + "copyState": "Kopiuj stan", + "exportState": "Eksportuj stan", + "openAdvancedDebug": "Otwórz zaawansowane debugowanie", + "sections": { + "Flow": "Przebieg", + "Shortcuts": "Skróty", + "Recovery": "Naprawa" + }, + "status": { + "copied": "Skopiowano stan gry do schowka.", + "copyFailed": "Nie można skopiować stanu gry.", + "exported": "Wyeksportowano {{filename}}.", + "exportFailed": "Nie można wyeksportować stanu gry." + }, + "prompt": { + "starting": "Gra się rozpoczyna lub przywraca stan.", + "gameOver": "Gra zakończona.", + "mulliganDecide": "Zdecyduj, czy zatrzymać tę rękę początkową, czy wykonać mulligan.", + "mulliganWaitDecide": "Oczekiwanie, aż inny gracz zdecyduje o swojej ręce początkowej.", + "mulliganBottom": "Wybierz karty do umieszczenia na spodzie po zatrzymaniu ręki po mulliganie.", + "mulliganWaitBottom": "Oczekiwanie, aż inny gracz zakończy swój mulligan.", + "openingHandBottom": "Wybierz kartę do umieszczenia na spodzie, zanim rozpoczną się normalne mulligany.", + "openingHandWait": "Oczekiwanie, aż inny gracz rozstrzygnie swoją rękę początkową.", + "waitOther": "Oczekiwanie na ruch innego gracza.", + "priorityStack": "Masz priorytet, gdy coś jest na stosie. Rozstrzygnięcie pasuje priorytet, aby górny element mógł się rozstrzygnąć.", + "priorityAutoPass": "Masz priorytet. Klient może automatycznie pasować spokojne okna, chyba że włączone jest zatrzymanie lub Pełna kontrola.", + "priorityActions": "Masz priorytet. Możesz użyć dostępnych kart lub spasować, aby kontynuować turę.", + "priorityPass": "Masz priorytet. Pasowanie przechodzi do następnego kroku lub gracza.", + "manaPayment": "Zapłać manę za oczekujący czar lub zdolność. Naciśnij T, aby przekręcić dostępne ziemie.", + "targetSelection": "Wybierz podświetlony legalny cel lub anuluj, jeśli komunikat na to pozwala.", + "declareAttackers": "Wybierz atakujących, a następnie potwierdź atakujących. Możesz też zaatakować bez żadnych.", + "declareBlockers": "Wybierz blokujących i przypisz ich do atakujących, a następnie potwierdź blokujących.", + "chooseXValue": "Wybierz wartość X przed kontynuowaniem czaru lub zdolności.", + "payAmountChoice": "Wybierz, ile z żądanego zasobu zapłacić.", + "default": "Gra czeka na twój wybór. Postępuj zgodnie z aktywnym komunikatem, aby kontynuować." + }, + "entries": { + "automaticPhaseSkips": { + "title": "Automatyczne pomijanie faz", + "body": "Pomijanie faz jest automatyczne. Użyj zatrzymań lub Pełnej kontroli, gdy chcesz uzyskać okna priorytetu w stylu papierowym." + }, + "phaseStops": { + "title": "Zatrzymania faz", + "body": "Zatrzymanie wstrzymuje grę przed danym krokiem, podobnie jak deklaracja, że możesz zagrać przed walką, przed dobraniem lub przed krokiem końcowym w papierze." + }, + "fullControl": { + "title": "Pełna kontrola", + "body": "Pełna kontrola zapobiega pomijaniu okien priorytetu, w tym miejsc, w których cyfrowy klient normalnie kontynuowałby grę." + }, + "resolve": { + "title": "Rozstrzygnij", + "body": "Rozstrzygnięcie oznacza, że pasujesz priorytet, aby górny czar lub zdolność na stosie mogły się rozstrzygnąć, jeśli wszyscy inni również spasują." + }, + "passToEnd": { + "title": "Pasuj do końca", + "body": "Pasowanie do końca pasuje priorytet przez całą turę, chyba że przerwie je wybór, zatrzymanie lub Pełna kontrola." + }, + "manaPayment": { + "title": "Płatność many", + "body": "Podczas płatności many możesz ręcznie przekręcać ziemie lub nacisnąć T, aby przekręcić dostępne ziemie." + }, + "combatDeclarations": { + "title": "Deklaracje walki", + "body": "Gra prosi o atakujących i blokujących tylko podczas kroków deklaracji. Wybierz stwory, a następnie potwierdź deklarację." + }, + "openHelp": { + "title": "Otwórz pomoc", + "body": "Otwórz ten arkusz pomocy." + }, + "passPriority": { + "title": "Pasuj priorytet", + "body": "Pasuj priorytet lub przejdź przez bieżący komunikat priorytetu." + }, + "undo": { + "title": "Cofnij", + "body": "Cofnij ostatnią lokalną akcję, która nie ujawniła ukrytych informacji." + }, + "cancel": { + "title": "Anuluj", + "body": "Anuluj bieżący wybór, płatność many, wybór celu lub automatyczne pasowanie, gdy jest dostępne." + }, + "advancedDebugPanel": { + "title": "Zaawansowany panel debugowania", + "body": "Otwórz zaawansowany panel debugowania. Większość graczy powinna najpierw zacząć od Narzędzi naprawczych." + }, + "reportOrExportState": { + "title": "Zgłoś lub wyeksportuj stan", + "body": "Jeśli karta zachowuje się nieprawidłowo, wyeksportuj bieżący stan gry, aby można było odtworzyć dokładną pozycję na polu bitwy." + }, + "boardRightClickMenu": { + "title": "Menu pola bitwy pod prawym przyciskiem myszy", + "body": "Na komputerze kliknij prawym przyciskiem myszy puste miejsce na polu bitwy, aby uzyskać dziennik gry, narzędzia naprawcze/debugowania i ustawienia tła." + } + }, + "flowNudge": { + "message": "Pomijanie faz jest automatyczne. Użyj zatrzymań lub Pełnej kontroli, gdy chcesz uzyskać okna priorytetu w stylu papierowym.", + "dismiss": "Odrzuć", + "learnFlow": "Poznaj przebieg" + }, + "sandboxNudge": { + "message": "Skonfiguruj dowolny stan pola bitwy za pomocą Narzędzi piaskownicy — dodawaj karty i żetony, zmieniaj życie i znaczniki, kopiuj trwałe permanenty lub przeskakuj fazy. Otwórz je w dowolnej chwili klawiszem `.", + "dismiss": "Odrzuć", + "open": "Otwórz Narzędzia piaskownicy" + } + }, + "splash": { + "ready": "Gotowe", + "loading": "Ładowanie..." + }, + "turnBanner": { + "turn": "Tura {{number}}" + }, + "chrome": { + "back": "Wstecz", + "settings": "Ustawienia", + "languageSettings": "Język ({{lang}}) — otwórz ustawienia", + "languageTitle": "Język: {{lang}}" + } +} diff --git a/client/src/i18n/locales/pl/deck-builder.json b/client/src/i18n/locales/pl/deck-builder.json new file mode 100644 index 0000000000..7026fe95ae --- /dev/null +++ b/client/src/i18n/locales/pl/deck-builder.json @@ -0,0 +1,210 @@ +{ + "search": { + "title": "Szukaj", + "subtitle": "Dodaj karty do bieżącej listy.", + "reset": "Resetuj", + "textPlaceholder": "Szukaj kart...", + "allTypes": "Wszystkie typy", + "cmcMax": "Maks. CMC:", + "sets": "Dodatki", + "addSetPlaceholder": "Dodaj kod dodatku...", + "addSet": "Dodaj", + "removeSet": "Usuń {{name}}", + "emptyHint": "Dodaj filtr, aby rozpocząć przeglądanie", + "searching": "Wyszukiwanie...", + "searchFailed": "Wyszukiwanie nie powiodło się", + "results_one": "{{count}} wynik", + "results_other": "{{count}} wyników", + "browseFormat": { + "all": "Wszystkie karty" + } + }, + "filters": { + "title": "Filtry", + "close": "Zamknij filtry", + "done": "Gotowe", + "search": "Szukaj" + }, + "deck": { + "backToDeck": "Powrót do talii", + "listView": "Widok listy", + "stackView": "Widok stosu" + }, + "tabs": { + "ariaLabel": "Powierzchnia kreatora talii", + "deck": "Talia", + "info": "Informacje" + }, + "toolbar": { + "menu": "Menu", + "deckBuilder": "Kreator talii", + "untitledDeck": "Talia bez tytułu", + "deckName": "Nazwa talii", + "format": "Format", + "bracket": "Poziom", + "saved": "Zapisano ✓", + "clone": "Klonuj", + "cloneTitle": "Zapisz kopię pod nową nazwą", + "loadDeck": "Wczytaj talię...", + "nameToSave": "Nazwij talię, aby ją zapisać" + }, + "unsaved": { + "title": "Niezapisane zmiany", + "dismiss": "Zamknij okno dialogowe", + "bodyLeaving": "Masz niezapisane zmiany w tej talii. Zapisać je przed wyjściem?", + "bodyLoading": "Masz niezapisane zmiany w tej talii. Zapisać je przed wczytaniem innej talii?", + "discard": "Odrzuć", + "saveAndContinue": "Zapisz i kontynuuj" + }, + "card": { + "moveToSideboard": "Przenieś jedną kartę {{name}} do zaplecza", + "moveToMain": "Przenieś jedną kartę {{name}} do talii głównej", + "removeOne": "Usuń jedną kartę {{name}}", + "makeCommander": "Ustaw {{name}} jako dowódcę", + "makeCommanderTitle": "Ustaw jako mojego dowódcę", + "chooseArtFor": "Wybierz grafikę dla {{name}}", + "alternateArtTap": "Dostępna alternatywna grafika — dotknij, aby wybrać", + "alternateArtRightClick": "Dostępna alternatywna grafika — kliknij prawym przyciskiem, aby wybrać", + "unsupportedExpand_one": "{{count}} nieobsługiwana mechanika; rozwiń szczegóły", + "unsupportedExpand_other": "{{count}} nieobsługiwanych mechanik; rozwiń szczegóły", + "unsupportedTitle_one": "{{count}} nieobsługiwana mechanika — kliknij, aby rozwinąć", + "unsupportedTitle_other": "{{count}} nieobsługiwanych mechanik — kliknij, aby rozwinąć" + }, + "grid": { + "allFormats": "Wszystkie", + "addCard": "Dodaj {{name}}", + "notLegal": "{{name}} - Niedozwolona w formacie {{format}}", + "notFormat": "Nie {{format}}" + }, + "deckList": { + "currentList": "Bieżąca lista", + "import": "Importuj", + "importTitle": "Importuj talię z tekstu (format MTGA lub .dck)", + "export": "Eksportuj", + "exportTitle": "Eksportuj talię", + "mainTab": "Główna ({{count}})", + "sideboardTab": "Zaplecze ({{count}})", + "mainDeckHeading_one": "Talia główna ({{count}} karta)", + "mainDeckHeading_other": "Talia główna ({{count}} kart)", + "sideboardUnlimited": "Zaplecze ({{count}})", + "sideboardLimited": "Zaplecze ({{count}}/{{max}})", + "sideboardExceeds": "Zaplecze przekracza limit {{max}} kart", + "sideboardEmptyHint": "Najedź na kartę z talii głównej i kliknij →, aby ją tu przenieść.", + "group": { + "Creatures": "Stwory", + "Spells": "Czary", + "Lands": "Ziemie" + }, + "importModalTitle": "Importuj talię", + "pastePlaceholder": "Wklej listę talii (format MTGA lub .dck)...", + "fromFile": "Z pliku", + "parse": "Przetwórz", + "exportModalTitle": "Eksportuj talię", + "saveToFile": "Zapisz do pliku", + "copied": "Skopiowano!", + "copy": "Kopiuj" + }, + "moveList": { + "empty": "Pusto" + }, + "stack": { + "deckView": "Widok talii", + "visualDeckStack": "Wizualny stos talii", + "mainBadge": "Główna {{count}}", + "sideboardBadge": "Zaplecze {{count}}", + "emptyHint": "Dodane karty pojawią się tutaj jako kaskadowy stos.", + "commanderBadge": "Dowódca", + "commanderLane": "Dowódca", + "mainDeckLane": "Talia główna", + "noCommander": "Nie wybrano dowódcy.", + "mainEmpty": "Karty talii głównej pojawią się tutaj.", + "cardCount_one": "{{count}} karta", + "cardCount_other": "{{count}} kart", + "mainSideBadge": "{{main}} główna + {{side}} zaplecze", + "groupCards_one": "{{count}} karta", + "groupCards_other": "{{count}} kart", + "group": { + "Creatures": "Stwory", + "Spells": "Czary", + "Lands": "Ziemie" + }, + "sideboardGroup": "Zaplecze", + "addOne": "Dodaj jedną kartę {{name}}", + "copyLimit": "{{name}} osiągnęła limit kopii", + "removeOne": "Usuń jedną kartę {{name}}", + "removeCommander": "Usuń {{name}} jako dowódcę" + }, + "printingPicker": { + "title": "Wybierz grafikę", + "noPrintings": "Brak alternatywnych wydań dla tej karty.", + "filterPlaceholder": "Filtruj według dodatku lub numeru kolekcjonerskiego…", + "filterAriaLabel": "Filtruj wydania", + "count_one": "{{count}} wydanie", + "count_other": "{{count}} wydań", + "countMatching_one": "{{count}} wydanie pasujące do „{{query}}”", + "countMatching_other": "{{count}} wydań pasujących do „{{query}}”", + "useDefault": "Użyj domyślnej", + "noMatches": "Żadne wydanie nie pasuje do filtra.", + "noImage": "Brak obrazu", + "showMore_one": "Pokaż więcej (pozostało {{count}})", + "showMore_other": "Pokaż więcej (pozostało {{count}})" + }, + "stats": { + "formatLegality": "Dozwolenie w formacie", + "engineCoverage": "Obsługa silnika", + "unsupportedTitle": "Nieobsługiwane:\n{{list}}" + }, + "commanderPanel": { + "heading": "Dowódca", + "noCommander": "Nie wybrano dowódcy", + "remove": "Usuń", + "identity": "Tożsamość:", + "setAsCommander": "Ustaw jako dowódcę:", + "cardCount": "{{count}}/{{expected}} kart", + "singletonViolations": "Naruszenia zasady singletona: {{cards}}", + "colorViolations": "Naruszenia tożsamości kolorów: {{cards}}" + }, + "manaCurve": { + "title": "Krzywa many", + "colors": "Kolory" + }, + "legality": { + "legal": "Dozwolona", + "banned": "Zakazana", + "restricted": "Ograniczona", + "notLegal": "Niedozwolona" + }, + "contextMenu": { + "chooseArt": "Wybierz grafikę…", + "noAlternates": "Brak alternatywnych wydań", + "clearOverride": "Wyczyść nadpisanie grafiki" + }, + "bracket": { + "unavailable": "Szacowanie poziomu nie jest dostępne w tej wersji.", + "addCommander": "Dodaj dowódcę, aby zobaczyć szacowany poziom.", + "estimated": "Szacowany: B{{tier}} {{label}}", + "manual": "Ręczny: B{{tier}} {{label}}", + "mismatch": " ⚠ niezgodność", + "hideBreakdown": "Ukryj szczegóły", + "showBreakdown": "Pokaż szczegóły", + "hideBreakdownButton": "▲ Ukryj szczegóły", + "showBreakdownButton": "▼ Pokaż szczegóły", + "forced": "(wymuszony B{{tier}})", + "dataVersion": "Dane: {{version}}", + "aboutBrackets": "O poziomach ↗", + "estimatedChip": "Szacowany: B{{tier}}", + "estimatedChipFull": "Szacowany poziom: B{{tier}} {{label}}" + }, + "bracketPicker": { + "ariaLabel": "Poziom talii", + "unrated": "Bez oceny" + }, + "warnings": { + "commanderCount": "Talia ma {{count}} kart (potrzeba dokładnie {{expected}})", + "singleton": "{{name}}: wiele kopii (format singleton)", + "colorIdentity": "{{name}}: poza tożsamością kolorów dowódcy", + "minimumCount": "Talia ma {{count}} kart (minimum 60)", + "maxCopies": "{{name}}: {{count}} kopii (maks. 4)", + "companionInMain": "{{name}} jest twoim towarzyszem, ale znajduje się też w talii głównej — to narusza jego warunek budowy talii. Usuń go z talii głównej, aby użyć go jako towarzysza." + } +} diff --git a/client/src/i18n/locales/pl/draft.json b/client/src/i18n/locales/pl/draft.json new file mode 100644 index 0000000000..d3d53e0fa0 --- /dev/null +++ b/client/src/i18n/locales/pl/draft.json @@ -0,0 +1,326 @@ +{ + "intro": { + "quickTitle": "Szybki draft", + "podTitle": "Draft w grupie", + "subtitle": "Oto jak to działa", + "startDrafting": "Rozpocznij draft", + "quick": { + "step1": "Otworzysz 3 boostery po 14 kart każdy", + "step2": "Wybierz jedną kartę z każdego boostera, a resztę przekaż dalej do botów", + "step3": "Boostery zmieniają kierunek w każdej rundzie — w lewo, w prawo, w lewo", + "step4": "Po wszystkich wyborach zbuduj talię z 40 kart i rozegraj mecz" + }, + "pod": { + "step1": "Draftujesz z {{count}} graczami w grupie", + "step2": "Otwórz 3 boostery po 14 kart — wybierz jedną, resztę przekaż dalej", + "step3": "Boostery zmieniają kierunek w każdej rundzie — w lewo, w prawo, w lewo", + "step4": "Po drafcie zbuduj talię z 40 kart i rozegraj mecze turniejowe" + } + }, + "steps": { + "navLabel": "Postęp draftu", + "chooseSet": "Wybierz dodatek", + "draft": "Draft", + "buildDeck": "Zbuduj talię", + "play": "Graj" + }, + "bracket": { + "title": "Drabinka", + "versus": "vs", + "quarterfinals": "Ćwierćfinały", + "semifinals": "Półfinały", + "final": "Finał" + }, + "hostControls": { + "title": "Sterowanie gospodarza", + "resumeDraft": "Wznów draft", + "pauseDraft": "Wstrzymaj draft", + "startNextRound": "Rozpocznij następną rundę", + "overrideResult": "Zmień wynik", + "versusPair": "{{a}} v {{b}}", + "kickReplace": "Wyrzuć i zastąp", + "replaceWithBot": "Zastąp gracza {{name}} botem", + "endDraft": "Zakoncz draft", + "endDraftConfirm": "Zakonczyc ten draft dla wszystkich? Tej akcji nie mozna cofnac." + }, + "manaCurve": { + "title": "Krzywa many" + }, + "pickTimer": { + "label": "Czas na wybór", + "seconds": "{{count}}s" + }, + "scoreBadge": { + "label": "Wynik: {{count}} wygranych" + }, + "seat": { + "label": "Miejsce {{number}}", + "passingLeft": "→ Przekazywanie w lewo →", + "passingRight": "← Przekazywanie w prawo ←" + }, + "setSelector": { + "botDifficulty": "Poziom trudności botów", + "chooseSet": "Wybierz dodatek", + "noPools": "Brak dostępnych pul do draftu. Najpierw uruchom pipeline danych draftu.", + "setIconAlt": "Ikona dodatku {{name}}", + "loadFailed": "Nie udało się wczytać dodatków" + }, + "pack": { + "confirmPick": "Potwierdź wybór", + "waitingNext": "Oczekiwanie na następny booster...", + "cardsInPack": "{{count}} kart w boosterze", + "picking": "Wybieranie…", + "autoPick": "Wybór automatyczny" + }, + "pool": { + "cardsDrafted": "{{count}} wybranych kart", + "empty": "Nie wybrano jeszcze żadnych kart", + "sortColor": "Kolor", + "sortType": "Typ", + "sortCmc": "CMC" + }, + "standings": { + "title": "Klasyfikacja — Runda {{round}}", + "rank": "#", + "player": "Gracz", + "record": "Bilans", + "gwp": "GWP", + "currentPairings": "Aktualne pary", + "versus": "vs" + }, + "limitedDeck": { + "poolHeading": "Pula ({{count}})", + "allAdded": "Wszystkie karty dodane do talii.", + "mainDeck": "Talia główna", + "emptyDeckHint": "Klikaj karty z puli, aby dodać je do talii.", + "addableCards": "Karty do dodania", + "autoLands": "Automatyczne ziemie", + "suggestDeck": "Zaproponuj talię", + "submitDeck": "Zatwierdź talię", + "removeCard": "Usuń {{name}}", + "addCard": "Dodaj {{name}}", + "cardCount": "/ {{min}} kart", + "spellCount_one": "{{count}} czar", + "spellCount_other": "{{count}} czarów", + "landCount_one": "{{count}} ziemia", + "landCount_other": "{{count}} ziem", + "readyToSubmit": "gotowe do zatwierdzenia", + "moreNeeded": "potrzeba jeszcze {{count}}" + }, + "landing": { + "title": "Draft", + "startNew": "Rozpocznij nowy", + "experimental": "Eksperymentalne", + "quickDraft": { + "title": "Szybki draft", + "description": "Zdraftuj 3 boostery z 7 botami, zbuduj talię z 40 kart, a potem rozegraj mecz Bo1 przeciwko jednemu z nich." + }, + "cubeDraft": { + "title": "Draft kostki", + "description": "Wklej lub wczytaj listę kostki, dostosuj zasady boosterów i talii, a potem zdraftuj szybko przeciwko botom." + }, + "podDraft": { + "title": "Draft w grupie", + "description": "Załóż lub dołącz do grupy liczącej do 8 graczy, draftujcie razem na żywo, a potem rozegrajcie turniej szwajcarski lub na wylot." + }, + "podInProgress": "Trwa draft w grupie", + "podLabel": "Grupa {{kind}}", + "seatCount": "{{count}} miejsc", + "roomLabel": "Pokój {{code}}", + "cardsPicked": "{{count}} wybranych kart", + "resume": "Wznów", + "draftInProgress": "Trwa draft", + "draftComplete": "Draft zakończony", + "resumeMatch": "Wznów mecz", + "viewResults": "Zobacz wyniki", + "setIconAlt": "Ikona {{code}}" + }, + "podPhase": { + "lobby": "Poczekalnia", + "drafting": "Draftowanie", + "deckbuilding": "Budowanie talii", + "pairing": "Dobieranie par", + "matchInProgress": "Trwa mecz", + "complete": "Zakończono" + }, + "quickPhase": { + "drafting": "Draftowanie", + "deckbuilding": "Budowanie talii", + "matchRecord": "Mecz {{number}} — {{wins}}W-{{losses}}P", + "record": "{{wins}}W-{{losses}}P", + "runComplete": "Bieg zakończony — {{wins}}W-{{losses}}P" + }, + "relativeTime": { + "justNow": "przed chwilą", + "minutes": "{{count}} min temu", + "hours": "{{count}} godz. temu", + "days": "{{count}} dni temu" + }, + "cubeSetup": { + "defaultCubeName": "Własna kostka", + "fetchError": "Nie udało się pobrać listy kostki", + "startError": "Nie udało się rozpocząć draftu kostki", + "cubeName": "Nazwa kostki", + "seats": "Miejsca", + "packs": "Boostery", + "packSize": "Rozmiar boostera", + "minDeck": "Minimalna talia", + "exportUrl": "URL eksportu CubeCobra", + "exportUrlPlaceholder": "Wklej surowy URL eksportu lub wklej listę poniżej", + "loadUrl": "Wczytaj URL", + "deckAddables": "Karty dodawane do talii", + "addablesStandardBasics": "Standardowe ziemie podstawowe", + "addablesBasicsPlusCustom": "Ziemie podstawowe plus własne", + "addablesCustomOnly": "Tylko własne", + "customAddableCards": "Własne karty do dodania", + "customAddablePlaceholder": "Jedna nazwa karty w wierszu", + "startCubeDraft": "Rozpocznij draft kostki" + }, + "formatPicker": { + "title": "Twoja talia jest gotowa", + "subtitle": "Wybierz, jak chcesz grać.", + "startMatch": "Rozpocznij mecz", + "single": { + "label": "Pojedynczy mecz", + "description": "Rozegraj jeden mecz Bo1 swoją zdraftowaną talią." + }, + "bo3": { + "label": "Do dwóch wygranych", + "description": "Rozegraj mecz Bo3 z wymianą kart bocznych między grami." + }, + "run": { + "label": "Pełny bieg", + "description": "Graj mecze Bo1, aż osiągniesz 7 wygranych lub 3 porażki." + } + }, + "run": { + "draftRun": "Bieg draftu", + "upNext": "Następny — Mecz {{number}}", + "nextMatch": "Następny mecz", + "endRun": "Zakończ bieg", + "done": "Gotowe", + "perfectRun": "Idealny bieg", + "runComplete": "Bieg zakończony", + "runOver": "Koniec biegu", + "finishedFlawless": "Zakończyłeś z bilansem {{wins}}–{{losses}}. Bezbłędnie.", + "finishedCongrats": "Zakończyłeś z bilansem {{wins}}–{{losses}}. Gratulacje!", + "finishedRecord": "Zakończyłeś z bilansem {{wins}}–{{losses}}.", + "wins": "Wygrane", + "losses": "Porażki", + "drawCount_one": "+ {{count}} remis", + "drawCount_other": "+ {{count}} remisów", + "matchLog": "Dziennik meczu", + "matchResultTitle": "Mecz {{number}}: {{result}}", + "result": { + "win": "wygrana", + "loss": "porażka", + "draw": "remis" + }, + "resultShort": { + "win": "W", + "loss": "P", + "draw": "R" + } + }, + "page": { + "cubeDraftTitle": "Draft kostki", + "quickDraftTitle": "Szybki draft", + "setDraftTab": "Draft dodatku", + "cubeTab": "Kostka" + }, + "podSetup": { + "title": "Draft w grupie", + "subtitle": "Draftuj ze znajomymi — otwieraj boostery, wybieraj karty i graj mecze turniejowe.", + "hostCardTitle": "Załóż grupę", + "hostCardDesc": "Utwórz nowy pokój draftu. Wybierasz dodatek, format i rozmiar grupy — a potem udostępnij kod pokoju znajomym. Puste miejsca można wypełnić botami.", + "joinCardTitle": "Dołącz do grupy", + "joinCardDesc": "Wpisz kod pokoju od gospodarza, aby dołączyć do istniejącego draftu. Zostaniesz posadzony na następnym wolnym miejscu i będziesz draftować razem ze wszystkimi w czasie rzeczywistym.", + "howItWorksTitle": "Jak działa draft w grupie", + "howItWorks1": "Każdy gracz otwiera 3 boostery po 14 kart — wybierz jedną, resztę przekaż dalej", + "howItWorks2": "Boostery zmieniają kierunek w każdej rundzie (w lewo → w prawo → w lewo)", + "howItWorks3": "Po drafcie każdy buduje talię z 40 kart ze swojej puli", + "howItWorks4": "Następnie rozgrywacie turniej szwajcarski lub na pojedyncze wyeliminowanie", + "back": "← Wstecz", + "hostTitle": "Załóż grupę", + "joinTitle": "Dołącz do grupy", + "displayName": "Wyświetlana nazwa", + "namePlaceholder": "Wpisz swoją nazwę...", + "draftType": "Typ draftu", + "kindPremier": "Premier", + "kindPremierDesc": "Mecze turniejowe do jednej wygranej po budowaniu talii. Szybsze rundy, bez wymiany kart bocznych między grami.", + "kindTraditional": "Tradycyjny", + "kindTraditionalDesc": "Mecze turniejowe do dwóch wygranych po budowaniu talii, z wymianą kart bocznych między grami.", + "tournamentFormat": "Format turnieju", + "tournamentSwiss": "Szwajcarski", + "tournamentSwissDesc": "Wszyscy grają przez trzy rundy, nawet po porażce.", + "tournamentElimination": "Pojedyncze wyeliminowanie", + "tournamentEliminationDesc": "Gracze odpadają po przegranym meczu, aż pozostanie jeden zwycięzca.", + "podPolicy": "Zasady grupy", + "policyCompetitive": "Wyczynowe", + "policyCompetitiveDesc": "Wybory na czas, automatyczne wybory po upływie czasu i automatyczne przechodzenie do kolejnych rund.", + "policyCasual": "Swobodne", + "policyCasualDesc": "Wybory bez limitu czasu, ręczne przechodzenie do kolejnych rund i narzędzia gospodarza do rozwiązywania problemów.", + "podSize": "Rozmiar grupy", + "podSizeDesc": "Łącznie {{count}} miejsc. Puste miejsca można wypełnić botami z poczekalni przed rozpoczęciem draftu.", + "playerCount": "{{count}} graczy", + "setSelectorHint": "Wybierz dodatek do draftu na końcu. Wybór dodatku wczytuje jego pulę kart i tworzy pokój grupy.", + "loadingPool": "Wczytywanie danych puli dodatku...", + "roomCode": "Kod pokoju", + "roomCodePlaceholder": "Wpisz kod pokoju...", + "joinPod": "Dołącz do grupy" + }, + "podPhaseView": { + "tournamentPairings": "Pary turniejowe", + "matchesInProgress": "Trwające mecze", + "yourMatch": "Twój mecz", + "versusOpponent": "vs {{name}}", + "youAreHosting": "Jesteś gospodarzem", + "connectingOpponent": "Łączenie z przeciwnikiem...", + "waitingResults": "Oczekiwanie na wyniki meczu...", + "hidePool": "Ukryj pulę", + "reviewPool": "Przejrzyj pulę", + "roundComplete": "Runda zakończona", + "waitingNextRound": "Oczekiwanie, aż gospodarz rozpocznie następną rundę...", + "nextRoundShortly": "Następna runda rozpocznie się wkrótce..." + }, + "betweenGames": { + "game": "Gra {{number}}", + "lostPreviousGame": "Przegrałeś poprzednią grę. Wybierz:", + "seconds": "{{count}}s", + "secondsRemaining": "pozostało {{count}}s", + "playFirst": "Zagraj pierwszy", + "drawFirst": "Dobierz pierwszy", + "sideboarding": "Wymiana kart bocznych", + "waitingSideboard": "Oczekiwanie, aż przeciwnik zatwierdzi karty boczne...", + "sideboardGame": "Karty boczne — Gra {{number}}", + "sideboardHint": "Wprowadź zmiany w kartach bocznych, a potem zatwierdź. Twoja pula jest dostępna poniżej.", + "submitSideboard": "Zatwierdź karty boczne", + "preparingNext": "Przygotowywanie następnej gry..." + }, + "podComplete": { + "title": "Draft zakończony", + "returnToMenu": "Powrót do menu" + }, + "podError": { + "kicked": "Zostałeś wyrzucony z grupy", + "hostLeft": "Gospodarz opuścił draft", + "connection": "Błąd połączenia" + }, + "lobby": { + "title": "Poczekalnia grupy draftu", + "draftKind": "Draft {{kind}}", + "botSeat": "Bot", + "waitingSeat": "Oczekiwanie...", + "seatNumber": "Miejsce {{number}}", + "hostBadge": "GOSPODARZ", + "kick": "Wyrzuć", + "copyRoomCodeTitle": "Kliknij, aby skopiować kod pokoju", + "roomCode": "Kod pokoju", + "clickToCopy": "Kliknij, aby skopiować", + "seatsFilled": "{{current}} / {{total}} miejsc zajętych", + "fillWithBots": "Wypełnij puste miejsca botami", + "leave": "Opuść", + "startDraft": "Rozpocznij draft", + "waitingForHost": "Oczekiwanie, aż gospodarz rozpocznie draft..." + } +} diff --git a/client/src/i18n/locales/pl/game.json b/client/src/i18n/locales/pl/game.json new file mode 100644 index 0000000000..88779097e2 --- /dev/null +++ b/client/src/i18n/locales/pl/game.json @@ -0,0 +1,1357 @@ +{ + "card": { + "faceDownName": "Karta zakryta", + "loading": "Wczytywanie {{name}}", + "unimplemented": "Niezaimplementowane: {{mechanics}}", + "dfc": "DFC" + }, + "preview": { + "holdCtrlBack": "Przytrzymaj Ctrl, aby zobaczyć tylną stronę", + "holdCtrlFront": "Przytrzymaj Ctrl, aby zobaczyć przednią stronę", + "holdCtrlFlip": "Przytrzymaj Ctrl, aby odwrócić", + "flip": "Odwróć", + "altParsedAbilities": "Alt: przeanalizowane zdolności", + "debugId": "ID: {{id}}", + "engineParse": "Analiza silnika", + "altKey": "Alt", + "parse": "Analiza", + "rulings": "Interpretacje", + "rulingCount_one": "{{count}} interpretacja", + "rulingCount_other": "{{count}} interpretacji", + "vanilla": "Vanilla — brak przeanalizowanych zdolności", + "unsupported": "nieobsługiwane", + "showMore": "Pokaż {{count}} więcej", + "showLess": "Pokaż mniej", + "fromSource": "(od {{source}})", + "basePT": "(bazowo {{power}}/{{toughness}})", + "damage": "Obrażenia: {{amount}}", + "ptDeltaFrom": "{{delta}} od {{source}}", + "colors": "Kolory: {{colors}}", + "colorless": "Bezbarwny" + }, + "actionButton": { + "clearAttackers": "Wyczyść atakujących", + "attackWithAll": "Atakuj wszystkimi", + "confirmAttackers": "Potwierdź atakujących ({{count}})", + "attackWithNone": "Nie atakuj nikim", + "attackWithNoneConfirm": "Naciśnij ponownie: Nie atakuj nikim", + "confirmBlockers": "Potwierdź blokujących ({{count}})", + "resetBlocks": "Zresetuj bloki", + "blockWithNone": "Nie blokuj nikim", + "blockWithNoneConfirm": "Naciśnij ponownie: Nie blokuj nikim", + "selectAttackerForBlocker": "Wybierz atakującego, przed którym ten blokujący ma bronić", + "companionToHand": "Towarzysz do ręki", + "resolve": "Rozpatrz", + "resolveTooltip": "Przekaż priorytet, aby górny element na stosie mógł zostać rozpatrzony, jeśli każdy gracz również przekaże priorytet. Skrót: Spacja.", + "resolveAll": "Rozpatrz wszystko", + "resolveAllTooltip": "Przekazuj priorytet, dopóki stos się rozpatruje. Wymagany wybór lub zatrzymanie może to przerwać.", + "waiting": "Oczekiwanie", + "priorityTooltip": "Przekaż priorytet. Jeśli stos jest pusty, spowoduje to przejście przez bieżące okno priorytetu. Skrót: Spacja.", + "pass": "Przekaż", + "passToEndTooltip": "Automatycznie przekazuj priorytet aż do kroku końcowego, chyba że przerwie to wybór, zatrzymanie lub Pełna kontrola. Skrót: Enter.", + "autoPassing": "Automatyczne przekazywanie do kroku końcowego..." + }, + "board": { + "regroupCreatures": "Pogrupuj zduplikowane grupy stworów", + "changeBackground": "Zmień tło…", + "gameLog": "Dziennik gry", + "debugLog": "Dziennik debugowania", + "waitingForGame": "Oczekiwanie na grę...", + "clickOpponent": "Kliknij przeciwnika, aby zobaczyć jego pole bitwy", + "undo": "Cofnij" + }, + "permanent": { + "expandGroup": "Rozwiń grupę {{name}}", + "chooseToken": "Wybierz token {{name}}", + "pick": "Wybierz", + "close": "zamknij", + "eligibleCount": "{{count}} dozwolonych", + "attackingCount": "atak {{count}}", + "selectedCount": "wyb {{count}}", + "pickAll": "Wszystko", + "pickNone": "Nic", + "underAttack_one": "Atakowany przez {{count}} stwora", + "underAttack_other": "Atakowany przez {{count}} stworów", + "target": "Cel", + "copy": "Kopia", + "copyTooltip": "Kopia tokenowa prawdziwej karty" + }, + "player": { + "opponent": "Przec {{seat}}", + "cardsInHand": "Karty w ręce", + "hand": "Ręka", + "creaturesAbbr": "Stw", + "landsAbbr": "Ziem", + "otherAbbr": "Inne", + "out": "POZA GRĄ", + "eliminated": "Wyeliminowany", + "phasedOut": "Przefazowany", + "you": "Ty", + "commanderDamageFrom": "Obrażenia od dowódcy od {{source}}: {{damage}}/{{threshold}}" + }, + "attackTargetPicker": { + "heading": "Wybierz cel ataku", + "attackAll": "Atakuj wszystkich", + "splitAttacks": "Rozdziel ataki", + "confirmSplitAttacks": "Potwierdź rozdzielenie ataków", + "attackWith_one": "Atakuj {{label}} {{count}} stworem", + "attackWith_other": "Atakuj {{label}} {{count}} stworami", + "allTo": "Wszyscy → {{label}}", + "creatureFallback": "Stwór #{{id}}", + "planeswalkerFallback": "Wędrowiec #{{id}}", + "you": "Ty", + "ally": "Sojusznik", + "restoreDialog": "Przywróć okno dialogowe", + "restoreDialogTitle": "Przywróć okno dialogowe celu ataku" + }, + "fullControl": { + "on": "Pełna kontrola włączona", + "off": "Pełna kontrola wyłączona", + "tooltip": "Zatrzymuj okna priorytetu zamiast pozwalać na automatyczne przekazywanie w spokojnych momentach. Skrót: F." + }, + "passButton": { + "resolve": "Rozpatrz", + "done": "Gotowe" + }, + "lifeTotal": { + "playerLabel": "G{{seat}}" + }, + "phaseStop": { + "untapLabel": "Krok odwracania", + "untapDescription": "Twoje obrócone trwałe odwracają się tutaj.", + "upkeepLabel": "Krok utrzymania", + "upkeepDescription": "Wyzwalacze utrzymania i powtarzające się koszty następują tutaj.", + "drawLabel": "Krok dobierania", + "drawDescription": "Tutaj dobierasz kartę na turę.", + "preCombatMainLabel": "Pierwsza faza główna", + "preCombatMainDescription": "Zagraj ziemie i rzuć czary przed walką.", + "beginCombatLabel": "Krok rozpoczęcia walki", + "beginCombatDescription": "Ostatnie okno priorytetu przed wybraniem atakujących.", + "declareAttackersLabel": "Krok deklaracji atakujących", + "declareAttackersDescription": "Atakujący gracz wybiera atakujących.", + "declareBlockersLabel": "Krok deklaracji blokujących", + "declareBlockersDescription": "Broniący gracze wybierają blokujących.", + "combatDamageLabel": "Krok obrażeń bojowych", + "combatDamageDescription": "Stwory przydzielają i zadają obrażenia bojowe.", + "endCombatLabel": "Krok zakończenia walki", + "endCombatDescription": "Ostatnie okno priorytetu walki po obrażeniach.", + "postCombatMainLabel": "Druga faza główna", + "postCombatMainDescription": "Zagraj ziemie lub rzuć czary po walce.", + "endLabel": "Krok końcowy", + "endDescription": "Wyzwalacze kroku końcowego i działania końca tury następują tutaj.", + "cleanupLabel": "Krok porządkowy", + "cleanupDescription": "Obrażenia znikają i sprawdzany jest maksymalny rozmiar ręki.", + "tooltipStopSet": "Zatrzymanie ustawione: kliknij, aby usunąć to zatrzymanie automatycznego przekazywania.", + "tooltipNoStop": "Brak zatrzymania: kliknij, aby wstrzymać tutaj automatyczne przekazywanie.", + "tooltipCurrentPhase": "Bieżąca faza.", + "tooltip": "Zatrzymanie fazy: {{label}}. {{description}} {{stopText}}{{activeText}}" + }, + "phaseTracker": { + "turn": "Tura {{number}}" + }, + "attachments": { + "enchantmentsOnPlayer": "Zaklęcia na graczu", + "attachedTo": "Dołączony do", + "attachedCount_one": "{{count}} dołączony trwały", + "attachedCount_other": "{{count}} dołączonych trwałych", + "unknownHost": "Nieznany", + "target": "Cel" + }, + "battlefieldPeek": { + "boardOf": "Pole bitwy {{name}}", + "noNonlandPermanents": "Brak trwałych niebędących ziemiami", + "morePermanents_one": "+{{count}} trwały więcej", + "morePermanents_other": "+{{count}} trwałych więcej", + "notTargetable": " (nie można obrać za cel)" + }, + "disconnectDialog": { + "title": "{{name}} rozłączył się", + "reconnecting": "Ponowne łączenie… pozostało {{seconds}}s", + "pauseAndWait": "Wstrzymaj i czekaj", + "continueWithout": "Kontynuuj bez nich" + }, + "kickDialog": { + "title": "Wyrzucić {{name}}?", + "body": "Przegrają grę i nie będą mogli dołączyć ponownie.", + "kick": "Wyrzuć" + }, + "enchantmentsBadge": { + "ariaLabel_one": "1 zaklęcie na tym graczu", + "ariaLabel_other": "{{count}} zaklęć na tym graczu", + "tooltip_one": "Najedź, aby podejrzeć, kliknij, aby zobaczyć", + "tooltip_other": "{{count}} zaklęć — najedź, aby podejrzeć, kliknij, aby zobaczyć" + }, + "badges": { + "monarch": "Monarcha", + "monarchTooltip": "Monarcha — dobiera dodatkową kartę na końcu tury", + "initiative": "Inicjatywa", + "initiativeTooltip": "Ma Inicjatywę — wyprawia się do Podmiasta na początku utrzymania", + "cityBlessing": "Błogosławieństwo miasta", + "cityBlessingTooltip": "Błogosławieństwo miasta — kontroluje dziesięć lub więcej trwałych (Ascend)", + "dungeonAriaLabel": "Wyprawa w {{name}}, pokój {{room}}", + "dungeonTooltip": "Wyprawa w {{name}} — pokój {{room}}", + "poisonAriaLabel_one": "{{count}} znacznik trucizny", + "poisonAriaLabel_other": "{{count}} znaczników trucizny", + "poisonTooltip": "Znaczniki trucizny: {{count}}", + "energyAriaLabel_one": "{{count}} znacznik energii", + "energyAriaLabel_other": "{{count}} znaczników energii", + "energyTooltip": "Energia: {{count}}", + "ringAriaLabel": "Pierścień cię kusi (poziom {{level}})", + "ringTooltip": "Pierścień cię kusi — poziom {{level}}", + "radAriaLabel_one": "{{count}} znacznik promieniowania", + "radAriaLabel_other": "{{count}} znaczników promieniowania", + "radTooltip": "Znaczniki promieniowania: {{count}}", + "speedAriaLabel": "Prędkość {{value}}", + "speedTooltip": "Prędkość: {{value}}", + "companion": "Towarzysz" + }, + "avatar": { + "underAttack": "{{name}} jest atakowany" + }, + "incomingAttackers": { + "summary": "⚔×{{count}} nadchodzi od {{name}}", + "clickToFocus": "kliknij, aby skupić" + }, + "manaPool": { + "onlyForSpellType": "Wydaj tylko na rzucanie czarów określonego typu", + "onlyForCreatureType": "Wydaj tylko na rzucenie czaru stwora wybranego typu", + "onlyForTypeSpellsOrAbilities": "Wydaj tylko na czary lub zdolności określonego typu", + "onlyForSpellWithKeywordKind": "Wydaj tylko na rzucanie czarów z określonym słowem kluczowym", + "onlyForSpellWithKeywordKindFromZone": "Wydaj tylko na rzucanie czarów ze słowem kluczowym z cmentarza", + "onlyForActivation": "Wydaj tylko na aktywowanie zdolności", + "onlyForXCosts": "Wydaj tylko na koszty zawierające {X}", + "convokePayment": "Płatność Convoke", + "grantsProperty": "Nadaje czarowi właściwość" + }, + "zone": { + "emblem": "Emblemat", + "emblemFallback": "Emblemat", + "commander": "Dowódca", + "commanderTitle": "Dowódca: {{name}}", + "commanderTitleTax": "Dowódca: {{name}} (Podatek: +{{tax}})", + "castCommander": "Rzuć {{name}} — kliknij dwukrotnie lub przeciągnij, aby zagrać", + "castCommanderTax": "Rzuć {{name}} (Podatek: +{{tax}}) — kliknij dwukrotnie lub przeciągnij, aby zagrać", + "tax": "Podatek: +{{tax}}", + "companion": "Towarzysz", + "companionTitle": "Towarzysz: {{name}}", + "companionTitleUsed": "Towarzysz: {{name}} (Wykorzystany)", + "companionActivate": "Zapłać {3}: Weź {{name}} do ręki", + "used": "Wykorzystany", + "exile": "Wygnanie", + "exileTitle": "Wygnanie ({{count}})", + "graveyard": "Cmentarz", + "graveyardShort": "CM", + "graveyardTitle": "Cmentarz ({{count}})", + "libraryAlt": "Biblioteka", + "libraryCount_one": "Biblioteka ({{count}} karta)", + "libraryCount_other": "Biblioteka ({{count}} kart)", + "playFromTop": "Zagraj {{name}} z wierzchu biblioteki", + "topOfLibrary": "wierzch biblioteki", + "indicatorOpponentPrefix": "Przec ", + "graveyardLower": "cmentarz", + "exileLower": "wygnanie", + "zoneTitle": "{{zone}} ({{count}})", + "noCardsIn": "Brak kart w strefie {{zone}}", + "castFromZone": "Rzuć z {{zone}}: {{name}}", + "viewCount": "Zobacz {{count}}", + "castableCount": "{{zone}} — {{count}} możliwych do rzucenia" + }, + "mana": { + "free": "Za darmo", + "chooseXTitle": "Wybierz wartość dla X", + "chooseXAria": "Wybierz wartość X", + "xEquals": "X = {{value}}", + "minMax": "min {{min}} / maks {{max}}", + "maxOnly": "maks {{max}}", + "confirmX": "Potwierdź X = {{value}}", + "payMana": "Zapłać koszt many", + "convokeHint": "Obróć stwory, aby pomóc zapłacić.", + "improviseHint": "Obróć artefakty, aby pomóc zapłacić.", + "convokeOrImproviseHint": "Obróć stwory lub artefakty, aby pomóc zapłacić.", + "lifeAmount": "2 życia", + "lifeCostSummary": "({{count}} życia)", + "paymentPending": "Płatność wciąż oczekuje. Obróć trwałe lub anuluj tę akcję.", + "poolLabel": "Pula:", + "poolEmpty": "Pusta", + "pay": "Zapłać", + "chooseAmountTitle": "Wybierz kwotę do zapłaty", + "chooseAmountAria": "Wybierz kwotę do zapłaty", + "payAmount": "Zapłać {{value}} {{resource}}", + "resourceEnergy": "energii", + "resourceMana": "many" + }, + "hand": { + "viewFullHand_one": "Zobacz całą rękę ({{count}} karta)", + "viewFullHand_other": "Zobacz całą rękę ({{count}} kart)", + "handLabel": "Ręka {{count}}", + "handTitle": "Ręka ({{count}})", + "cardBack": "Rewers karty" + }, + "opponentHud": { + "ally": "Sojusznik", + "out": "Poza grą", + "phased": "Przefazowany", + "followingActiveOpponent": "Śledzenie aktywnego przeciwnika. Skupienie przełącza się na przeciwnika, którego jest tura.", + "followActiveOpponent": "Śledź aktywnego przeciwnika. Skupienie przełączy się na przeciwnika, którego jest tura.", + "compactHud": "Kompaktowy HUD przeciwnika. Zwija każdego przeciwnika do jednego cienkiego wiersza (imię + życie); dotknij przeciwnika, aby skupić jego całe pole bitwy.", + "expandHud": "Rozwiń HUD przeciwnika. Pokazuje rękę każdego przeciwnika i zestawienie pola bitwy.", + "targetPlayer": "Obierz za cel {{name}}", + "viewBoardThenTarget": "Zobacz pole bitwy {{name}} (kliknij ponownie, aby obrać za cel {{name}})", + "viewBoard": "Zobacz pole bitwy {{name}}", + "clickToTarget": "Kliknij, aby obrać za cel {{name}}", + "clickToViewThenTarget": "Kliknij, aby zobaczyć pole bitwy {{name}}, a następnie kliknij ponownie, aby obrać za cel {{name}}", + "clickToViewBoard": "Kliknij, aby zobaczyć pole bitwy {{name}}", + "underAttack": "{{name}} jest atakowany", + "kickPlayer": "Wyrzuć gracza {{seat}}", + "kickPlayerTooltip": "Wyrzuć gracza (poddanie)", + "incomingAttackers_one": "{{count}} stwór cię atakuje", + "incomingAttackers_other": "{{count}} stworów cię atakuje", + "disconnected": "Rozłączony", + "connected": "Połączony", + "statHand": "Ręka", + "statCreatures": "Stwory", + "statLands": "Ziemie", + "statOther": "Inne" + }, + "combat": { + "attackAll": "Atakuj wszystkich", + "skip": "Pomiń", + "confirmAttackers": "Potwierdź atakujących ({{count}})", + "confirmBlockers": "Potwierdź blokujących ({{count}})", + "clickAttackerToAssignBlocker": "Kliknij atakującego, aby przydzielić blokującego", + "menaceRequirement": "Musi zostać zablokowany przez {{count}} lub wcale", + "blockNeedsBadge": "Potrzeba {{required}}", + "blockProgressBadge": "{{assigned}} / {{required}}", + "blockSatisfiedBadge": "✓ {{required}}", + "blockIncompleteAttacker": "{{assigned}} z {{required}} blokujących przydzielonych", + "blockIncomplete_one": "{{count}} atakujący potrzebuje kolejnego blokującego", + "blockIncomplete_other": "{{count}} atakujących potrzebuje kolejnego blokującego", + "assignDamageTitle": "Przydziel {{amount}} obrażeń bojowych", + "assignDamageSubtitle": "{{name}} — Pozostało: {{remaining}}", + "assignDamageButton": "Przydziel obrażenia", + "lethalLabel": "śmiertelne: {{amount}}", + "lethalBadge": "Śmiertelne", + "planeswalkerLoyalty": "Wędrowiec (lojalność: {{loyalty}})", + "defendingPlayerTrample": "Broniący gracz (Tratowanie)", + "pwControllerTrample": "Kontroler wędrowca (Tratowanie przez wędrowca)" + }, + "stack": { + "title": "Stos", + "collapsePanel": "Zwiń panel stosu", + "expandPanel": "Rozwiń panel stosu", + "dockLeft": "Zadokuj stos po lewej", + "dockRight": "Zadokuj stos po prawej", + "casting": "Rzucanie…", + "next": "Dalej", + "activated": "Aktywowana", + "triggered": "Wyzwolona", + "triggeredFrom": "Wyzwolona — Od {{source}}", + "controllerYou": "Ty", + "controllerOpp": "Przec", + "controllerInitialYou": "T", + "controllerInitialOpp": "G{{seat}}", + "targetingLabel": "Obiera za cel {{label}}", + "titleTargets": "Cele: {{targets}}", + "titlePaid": "Zapłacono: {{paid}}", + "titleContext": "Kontekst: {{context}}", + "paidXValue": "X={{value}}", + "paidManaSpent": "{{amount}} many", + "paidColorsSpent": "{{count}} kolorów", + "paidKicked": "kicker", + "paidKickedTimes": "kicker ×{{count}}", + "paidAdditionalCost": "dodatkowy koszt", + "paidConvoked": "convoke ×{{count}}" + }, + "targeting": { + "choosePermanentToCopy": "Wybierz trwałego do skopiowania", + "retargetCopySlot": "Zmień cel kopii: slot {{current}} z {{total}}", + "chooseTargetForCopySlot": "Wybierz cel dla kopii: slot {{current}} z {{total}}", + "chooseNewTargetForCopy": "Wybierz nowy cel dla kopii", + "chooseTargetForCopy": "Wybierz cel dla kopii", + "chooseNewTarget": "Wybierz nowy cel czaru", + "chooseNewTargetForSpell": "Wybierz nowy cel dla {{spell}}", + "chooseCreatureToExplore": "Wybierz, który stwór eksploruje jako następny", + "tapUntappedCreatures_one": "Obróć {{count}} nieobróconego stwora", + "tapUntappedCreatures_other": "Obróć {{count}} nieobróconych stworów", + "chooseTargetOf": "Wybierz cel {{current}} z {{total}}", + "chooseTarget": "Wybierz cel", + "noLegalTargets": "Brak dostępnych dozwolonych celów", + "upToOne": "do jednego {{target}}", + "one": "{{target}}", + "nounPlayer": "gracz", + "nounNonlandPermanent": "trwały niebędący ziemią", + "nounCreature": "stwór", + "nounPlaneswalker": "wędrowiec", + "nounTargetPermanent": "docelowy trwały", + "nounTarget": "cel", + "confirmTap": "Potwierdź obrócenie ({{selected}}/{{count}})", + "keepCurrentTargets": "Zachowaj bieżące cele", + "skip": "Pomiń" + }, + "log": { + "title": "Dziennik gry", + "noEvents": "Brak zdarzeń", + "closeLog": "Zamknij dziennik gry", + "verbosityFull": "pełny", + "verbosityCompact": "kompaktowy", + "verbosityMinimal": "minimalny", + "gameStarted": "Gra rozpoczęta", + "turnStarted": "Tura {{turn}} -- {{player}}", + "phaseChanged": "Faza: {{phase}}", + "priorityPassed": "{{player}} przekazał priorytet", + "spellCast": "Czar rzucony przez {{player}}", + "abilityActivated": "Zdolność aktywowana (źródło {{sourceId}})", + "zoneChangedMoved": "Obiekt {{objectId}} przeniesiony {{from}} -> {{to}}", + "zoneChangedEnters": "Obiekt {{objectId}} wchodzi do {{to}}", + "lifeChanged": "Życie {{player}}: {{sign}}{{amount}}", + "manaAdded": "{{player}} dodał manę {{manaType}}", + "permanentTapped": "Trwały {{objectId}} obrócony", + "permanentUntapped": "Trwały {{objectId}} odwrócony", + "playerLost": "{{player}} przegrał grę", + "mulliganStarted": "Faza mulligana", + "cardsDrawn_one": "{{player}} dobrał {{count}} kartę", + "cardsDrawn_other": "{{player}} dobrał {{count}} kart", + "cardDrawn": "{{player}} dobrał kartę", + "landPlayed": "{{player}} zagrał ziemię", + "stackPushed": "Obiekt {{objectId}} dodany na stos", + "stackResolved": "Wpis stosu {{objectId}} rozpatrzony", + "discarded": "{{player}} odrzucił", + "damageCleared": "Obrażenia usunięte z {{objectId}}", + "gameOverWinner": "Koniec gry -- {{player}} wygrywa!", + "gameOverDraw": "Koniec gry -- Remis", + "damageDealtPlayer": "Źródło {{sourceId}} zadaje {{amount}} obrażeń graczowi {{player}}", + "damageDealtObject": "Źródło {{sourceId}} zadaje {{amount}} obrażeń obiektowi {{objectId}}", + "spellCountered": "Obiekt {{objectId}} skontrowany przez {{counteredBy}}", + "counterAdded": "{{counterType}} x{{count}} dodano do {{objectId}}", + "counterRemoved": "{{counterType}} x{{count}} usunięto z {{objectId}}", + "tokenCreated": "Token \"{{name}}\" utworzony", + "creatureDestroyed": "Stwór {{objectId}} zniszczony", + "permanentSacrificed": "{{player}} poświęcił {{objectId}}", + "effectResolved": "Efekt {{kind}} rozpatrzony", + "attackersDeclared_one": "{{count}} atakujący zadeklarowany", + "attackersDeclared_other": "{{count}} atakujących zadeklarowanych", + "blockersDeclared_one": "{{count}} blokujący przydzielony", + "blockersDeclared_other": "{{count}} blokujących przydzielonych", + "becomesTarget": "Obiekt {{objectId}} obrany za cel przez {{sourceId}}", + "replacementApplied": "Zastosowano efekt zastępujący: {{eventType}}", + "companionRevealed": "{{player}} ujawnił towarzysza: {{cardName}}", + "companionMovedToHand": "{{player}} wziął towarzysza {{cardName}} do ręki", + "powerToughnessChanged": "Obiekt {{objectId}} ma teraz {{power}}/{{toughness}} ({{powerDelta}}/{{toughnessDelta}})", + "genericEvent": "Zdarzenie: {{type}}" + }, + "coverage": { + "engineTools": "Narzędzia silnika", + "title": "Pokrycie kart", + "subtitle": "Sprawdź pokrycie implementacji i obsługiwane handlery silnika.", + "tabCardCoverage": "Pokrycie kart", + "tabBySet": "Wg dodatku", + "tabGapAnalysis": "Analiza braków", + "tabSupportedHandlers": "Obsługiwane handlery", + "noDataAvailable": "Brak dostępnych danych o pokryciu.", + "generateHint": "Wygeneruj je za pomocą: cargo run --bin coverage-report -- /path/to/cards --all > client/public/coverage-data.json", + "emptyData": "Dane o pokryciu są puste (przeanalizowano 0 kart).", + "runHint": "Uruchom: cargo run --bin coverage-report -- /path/to/cards --all > client/public/coverage-data.json", + "searchCards": "Szukaj kart...", + "filterAll": "Wszystkie", + "filterSupported": "Obsługiwane", + "filterUnsupported": "Nieobsługiwane", + "sortAz": "Sortuj: A-Z", + "sortMostGaps": "Sortuj: Najwięcej braków", + "sortFewestGaps": "Sortuj: Najmniej braków", + "shownOfTotal": "{{shown}} z {{total}} wyświetlonych", + "noMatches": "Brak dopasowań", + "matchesCount": "{{shown}} z {{total}} dopasowań", + "cardsCount": "{{count}} kart", + "selectCardPrompt": "Wybierz kartę, aby sprawdzić jej rozbicie analizy", + "navHint": "Użyj strzałek do nawigacji, Escape do odznaczenia", + "coverageByFormat": "Pokrycie wg formatu", + "allFormats": "Wszystkie formaty", + "setThresholdHint": "Dodatki z ≥{{minCards}} kartami i ≥{{minCoverage}}% w pełni obsługiwanymi. Rozwiń dodatek, aby sprawdzić pozostałe braki.", + "setsCount": "({{count}} dodatków)", + "noSetsMeetThreshold": "Żaden dodatek nie spełnia jeszcze progu.", + "setMembershipMissing": "Dane o przynależności do dodatku nie znajdują się w tym eksporcie pokrycia.", + "regenerateHint": "Wygeneruj ponownie za pomocą: ./scripts/gen-card-data.sh", + "gapCount_one": "{{count}} brak", + "gapCount_other": "{{count}} braków", + "noUnsupportedInSet": "Brak nieobsługiwanych kart w tym dodatku.", + "unsupportedCardsHint": "Nieobsługiwane karty ({{count}}) — kliknij jedną, aby sprawdzić drzewo analizy", + "noGapDetails": "brak szczegółów braku", + "noGapAnalysis": "Brak dostępnych danych analizy braków.", + "filterByFormat": "Filtruj wg formatu:", + "topGaps": "Najważniejsze braki wg wpływu (Top 50)", + "copyAsTsv": "Kopiuj jako TSV", + "totalLabel": "{{count}} łącznie", + "unlockLabel": "{{count}} odblokuje", + "indLabel": "{{ratio}} ind", + "exampleCard": "np. {{name}}", + "oraclePatterns": "Wzorce tekstu Oracle", + "coOccurringGaps": "Współwystępujące braki", + "singleGapUnlockByFormat": "Odblokowanie jednym brakiem wg formatu", + "twoGapBundles": "Pakiety 2 braków (zaimplementuj oba, aby odblokować karty)", + "threeGapBundles": "Pakiety 3 braków (zaimplementuj wszystkie trzy, aby odblokować karty)", + "bundleCards": "{{count}} kart", + "backToList": "Powrót do listy", + "supported": "Obsługiwane", + "unsupported": "Nieobsługiwane", + "copyOracle": "Kopiuj tekst Oracle", + "unresolvedGaps": "Nierozwiązane braki ({{count}})", + "vanillaCard": "Brak przeanalizowanych elementów (karta vanilla).", + "additionalParseItems": "Dodatkowe elementy analizy", + "itemUnsupported": "nieobsługiwane", + "categoryKeyword": "Słowo kluczowe", + "categoryAbility": "Zdolność", + "categoryTrigger": "Wyzwalacz", + "categoryStatic": "Statyczna", + "categoryReplacement": "Zastępująca", + "categoryCost": "Koszt", + "copied": "Skopiowano!", + "handlersObserved": "{{count}} handlerów zaobserwowanych na obsługiwanych kartach", + "derivedFrom": "wyprowadzone z wyniku analizy · zaślepki i nieużywane warianty wykluczone", + "tabEffects": "Efekty", + "tabTriggers": "Wyzwalacze", + "tabKeywords": "Słowa kluczowe", + "tabStatics": "Statyczne", + "tabReplacements": "Zastępujące", + "search": "Szukaj...", + "cardCount_one": "{{formattedCount}} karta", + "cardCount_other": "{{formattedCount}} kart", + "noMatchesFound": "Nie znaleziono dopasowań dla „{{query}}”", + "noHandlersInCategory": "Żadne handlery z tej kategorii nie są tworzone przez parser na w pełni obsługiwanych kartach.", + "footerSummary": "{{handlers}} handlerów w {{categories}} kategoriach · wyprowadzone z silnika z {{cards}} obsługiwanych kart" + }, + "gamePage": { + "actions": { + "returnToMenu": "Powrót do menu", + "returnToMenuLower": "Powrót do menu", + "concedeGame": "Poddaj grę", + "ok": "OK" + }, + "toasts": { + "playerDisconnected": "{{name}} rozłączył się", + "gamePausedPlayerDisconnected": "Gra wstrzymana — {{name}} rozłączył się", + "roomFull": "Pokój pełny — gotowe do rozpoczęcia!", + "hostReconnecting": "Gospodarz rozłączył się — ponowne łączenie (próba {{attempt}})…", + "roomStillClaimed": "Twój poprzedni pokój wciąż jest wygaszany na serwerze sygnalizacyjnym. Odczekaj około minuty, a potem spróbuj wznowić ponownie." + }, + "sandbox": { + "banner": "Tryb piaskownicy — akcje debugowania włączone", + "bannerAria": "Baner trybu piaskownicy" + }, + "reconnect": { + "banner": "Ponowne łączenie… (próba {{attempt}})", + "bannerWithMax": "Ponowne łączenie… (próba {{attempt}}/{{maxAttempts}})", + "connectionLost": "Utracono połączenie" + }, + "opponentDisconnected": { + "title": "Przeciwnik rozłączony", + "body": "Oczekiwanie na ponowne połączenie przeciwnika..." + }, + "resumeReset": { + "message": "{{reason}} Rozpoczęto nową grę." + }, + "playDraw": { + "title": "Gra {{gameNumber}}: Wybierz zagrywanie lub dobieranie", + "matchScore": "Wynik meczu {{p0Wins}}-{{p1Wins}}", + "playFirst": "Zagrywaj pierwszy", + "playFirstDescription": "Wykonaj pierwszą turę", + "drawFirst": "Dobieraj pierwszy", + "drawFirstDescription": "Wykonaj dodatkowe dobranie w swojej pierwszej turze" + }, + "mulligan": { + "opponentDeciding": "Przeciwnik wybiera swoją rękę startową…", + "londonTitle": "Mulligan londyński ({{count}} wziętych)", + "keepHand": "Zatrzymaj rękę", + "putOnBottom": "Połóż {{count}} na spód", + "noCardsToBottom": "Brak kart na spód", + "mulligan": "Mulligan", + "freeMulligan": "Darmowy mulligan", + "mulliganTo": "Mulligan do {{count}}", + "shuffleDrawSevenFree": "Przetasuj i dobierz 7 — brak kart na spód", + "shuffleDrawSevenAgain": "Przetasuj i dobierz 7 ponownie", + "usePowder": "Użyj {{name}}", + "powderDescription": "Wygnaj każdą kartę z ręki, dobierz tyle samo — to nie mulligan", + "powderTooltip": "Wygnaj każdą kartę z ręki i dobierz tyle samo. To nie mulligan — liczba i karty na spodzie pozostają bez zmian.", + "eyebrowMulligan": "Mulligan {{count}} · Londyński", + "eyebrowOpening": "Ręka startowa · Mulligan londyński", + "reviewTitle": "Przejrzyj swoją rękę startową", + "subtitleKeepWithBottom": "Zatrzymaj tę rękę (położysz {{count}} na spód) lub zrób mulligan ponownie po świeże 7.", + "subtitleKeepFree": "Zatrzymaj tę rękę (darmowy mulligan — brak kart na spód) lub zrób mulligan ponownie po świeże 7.", + "subtitleOpeningFree": "Zatrzymaj tę rękę lub zrób darmowy mulligan (brak kart na spód).", + "subtitleOpeningOne": "Zatrzymaj tę rękę lub zrób mulligan po świeże 7 (gdy zatrzymasz, położysz 1 na spód)." + }, + "companion": { + "eyebrow": "Przed grą", + "title": "Ujawnić towarzysza?", + "subtitle": "Możesz ujawnić towarzysza ze swojego sideboardu. Zostanie umieszczony w strefie towarzysza i można go wziąć do ręki raz w trakcie gry, płacąc {3}.", + "decline": "Odmów", + "reveal": "Ujawnij {{name}}" + }, + "bottomCards": { + "eyebrowTinyLeaders": "Tiny Leaders", + "eyebrowLondon": "Mulligan londyński", + "title_one": "Połóż {{count}} kartę na spód", + "title_other": "Połóż {{count}} kart na spód", + "subtitleOpening_one": "Wybierz {{count}} kartę z ręki startowej przed rozpoczęciem mulliganów.", + "subtitleOpening_other": "Wybierz {{count}} kart z ręki startowej przed rozpoczęciem mulliganów.", + "subtitleMulligan_one": "Wybierz {{count}} kartę z ręki. Zostanie zwrócona na spód biblioteki w wybranej tutaj kolejności.", + "subtitleMulligan_other": "Wybierz {{count}} kart z ręki. Zostaną zwrócone na spód biblioteki w wybranej tutaj kolejności.", + "selectedOf": "Wybrano {{selected}} z {{count}}", + "confirmSelection": "Potwierdź wybór" + }, + "gameOver": { + "draw": "REMIS", + "victory": "ZWYCIĘSTWO", + "defeat": "PORAŻKA", + "lifeSummary": "Ty: {{playerLife}} / Przeciwnik: {{opponentLife}}", + "turns": "Tury: {{count}}", + "duration": "Czas trwania: {{time}}", + "backToDraft": "Powrót do draftu", + "continueRun": "Kontynuuj rozgrywkę", + "backToLobby": "Powrót do lobby", + "rematch": "Rewanż" + }, + "abilityChoice": { + "subtitleSneak": "Wybierz, którego atakującego zwrócić (koszt Sneak)", + "subtitlePreparedCopy": "Rzucić przygotowany czar?", + "subtitleActivate": "Aktywować tę zdolność?", + "subtitlePlay": "Wybierz, jak zagrać tę kartę", + "subtitleChoose": "Wybierz zdolność do aktywowania" + }, + "defiler": { + "title": "Redukcja kosztu Defiler", + "subtitle": "Zapłacić {{lifeCost}} życia, aby zmniejszyć koszt many?", + "payLife": "Zapłać {{lifeCost}} życia", + "decline": "Odmów" + }, + "untap": { + "permanentFallback": "Trwały", + "title": "Odwrócić {{name}}?", + "subtitle": "Wybierz, czy ten trwały odwraca się w trakcie twojego kroku odwracania.", + "untap": "Odwróć", + "untapDescription": "{{name}} odwraca się teraz.", + "keepTapped": "Zostaw obrócony", + "keepTappedDescription": "{{name}} pozostaje obrócony w tym kroku odwracania." + }, + "cost": { + "discardCard": "odrzuć kartę", + "life": "{{amount}} życia", + "sacrifice_one": "poświęć trwałego", + "sacrifice_other": "poświęć {{count}} trwałych", + "returnToHand_one": "zwróć trwałego do ręki", + "returnToHand_other": "zwróć {{count}} trwałych do ręki", + "energy": "{{amount}} energii", + "generic": "koszt", + "pay": "Zapłać {{cost}}" + }, + "unlessPayment": { + "defaultEffect": "Skontruj, chyba że zapłacisz", + "defaultChooseCost": "Wybierz koszt", + "title": "{{effect}}, chyba że zapłacisz", + "titleChooseOne": "{{effect}}, chyba że zapłacisz jeden", + "dontPay": "Nie płać", + "takeEffect": "Przyjmij efekt" + }, + "activationCost": { + "title": "Wybierz koszt aktywacji" + }, + "debug": { + "modeBanner": "TRYB DEBUGOWANIA - Kliknij dowolną kartę" + } + }, + "gameProvider": { + "passwordPrompt": "Ten pokój wymaga hasła:", + "toasts": { + "connectionFailed": "Połączenie nie powiodło się. Spróbuj ponownie lub zmień serwer w Ustawieniach.", + "eliminatedSpectating": "Zostałeś wyeliminowany. Teraz obserwujesz." + }, + "resumeReset": { + "appUpdated": "Aplikacja została zaktualizowana, a zapisana gra jest niezgodna z nową wersją.", + "restoreFailed": "Nie udało się przywrócić zapisanej gry: {{error}}" + }, + "notification": { + "title": "Przeciwnik dołączył", + "opponentJoined": "Twój przeciwnik dołączył do gry. Wróć i graj!", + "opponentJoinedNamed": "{{name}} dołączył do twojej gry. Wróć i graj!" + }, + "noLegalAiDecks": { + "withFormat": "Brak dozwolonych talii AI dla formatu {{format}}.", + "generic": "Brak dozwolonych talii AI dla wybranego formatu." + } + }, + "gameSetup": { + "eyebrow": "Konfiguracja meczu", + "title": "Rozpocznij mecz.", + "startMatch": "Rozpocznij mecz", + "startMatchWithOpponents": "Rozpocznij mecz ({{count}} przec.)", + "deckNotLegal": "Talia nie jest dozwolona w formacie {{format}}.", + "selectDeckPrompt": "Wybierz talię", + "commanderNote": "Commander: 100 kart singleton, obrażenia od dowódcy przy {{threshold}}", + "formatChip": { + "kicker": "Format", + "choosePlaceholder": "Wybierz format", + "ariaLabel": "Format: {{label}} ({{group}}). Dotknij, aby zmienić.", + "ariaLabelEmpty": "Wybierz format meczu" + }, + "formatPicker": { + "title": "Wybierz format", + "subtitle": "Wybierz zasady, według których będą grać wszyscy przy stole." + }, + "deckPreview": { + "editDeck": "Edytuj {{name}}", + "cardCount_one": "{{count}} karta", + "cardCount_other": "{{count}} kart", + "unknownBadge": "Nieznane {{count}}", + "unknownCardsTitle": "Nieznane karty:\n{{cards}}" + }, + "config": { + "startingLife": "Życie początkowe", + "players": "Gracze", + "whoGoesFirst": "Kto zaczyna", + "firstPlayer": { + "random": "Losowo", + "play": "Zagrywanie", + "draw": "Dobieranie" + } + }, + "noLegalAiDecks": { + "withFormat": "Dodaj lub zaimportuj talię dozwoloną w formacie {{format}}, aby AI mogło jej użyć.", + "generic": "Dodaj lub zaimportuj talię dozwoloną w tym formacie, aby AI mogło jej użyć." + } + }, + "cardChoice": { + "badges": { + "choose": "Wybierz", + "selected": "Wybrano", + "keep": "Zatrzymaj", + "top": "Wierzch", + "bottom": "Spód", + "graveyard": "Cmentarz", + "put": "Połóż", + "sacrifice": "Poświęć", + "exile": "Wygnaj", + "battlefield": "Pole bitwy", + "target": "Cel", + "return": "Zwróć", + "discard": "Odrzuć", + "evidence": "Dowody", + "manifest": "Manifest", + "crew": "Crew ({{power}})", + "saddle": "Saddle ({{power}})", + "station": "Station (+{{power}})" + }, + "buttons": { + "confirm": "Potwierdź", + "decline": "Odmów", + "pass": "Przekaż", + "skip": "Pomiń", + "repeat": "Powtórz", + "stop": "Zatrzymaj", + "confirmCount": "Potwierdź ({{selected}}/{{count}})", + "confirmOrder": "Potwierdź kolejność ({{selected}}/{{count}})", + "sacrificeCount": "Poświęć ({{selected}}/{{count}})", + "exileCount": "Wygnaj ({{selected}}/{{count}})", + "discardCount": "Odrzuć ({{selected}}/{{count}})", + "collectCount": "Zbierz ({{total}}/{{minimum}})", + "labelCount": "{{label}} ({{selected}}/{{count}})" + }, + "ringBearer": { + "title": "Wybierz Nosiciela Pierścienia", + "subtitle": "Wybierz stwora, którego kontrolujesz" + }, + "reorderHint": "Przeciągnij, aby zmienić kolejność · karta na wierzchu jest dobierana pierwsza", + "scry": { + "title": "Scry", + "subtitle_one": "Spójrz na {{count}} kartę z wierzchu swojej biblioteki", + "subtitle_other": "Spójrz na {{count}} kart z wierzchu swojej biblioteki" + }, + "dig": { + "titleReorder": "Zmień kolejność kart", + "title": "Wybierz karty", + "subtitleReorder": "Wybierz wszystkie {{count}} kart w kolejności od wierzchu do spodu", + "subtitleExact_one": "Wybierz {{count}} kartę, aby położyć {{destination}}", + "subtitleExact_other": "Wybierz {{count}} kart, aby położyć {{destination}}", + "subtitleUpTo_one": "Wybierz do {{count}} karty, aby położyć {{destination}}", + "subtitleUpTo_other": "Wybierz do {{count}} kart, aby położyć {{destination}}", + "destinationTop": "na wierzchu twojej biblioteki", + "destinationBattlefield": "na pole bitwy", + "destinationHand": "do twojej ręki" + }, + "surveil": { + "title": "Surveil", + "subtitle_one": "Spójrz na {{count}} kartę z wierzchu swojej biblioteki", + "subtitle_other": "Spójrz na {{count}} kart z wierzchu swojej biblioteki" + }, + "reveal": { + "titleReveal": "Ujawnij z ręki", + "titleOpponentHand": "Ręka przeciwnika", + "subtitleReveal": "Wybierz kartę do ujawnienia lub odmów", + "subtitleChoose": "Wybierz kartę" + }, + "search": { + "title": "Przeszukaj bibliotekę", + "subtitleExact_one": "Wybierz {{count}} kartę", + "subtitleExact_other": "Wybierz {{count}} kart", + "subtitleUpTo_one": "Wybierz do {{count}} karty", + "subtitleUpTo_other": "Wybierz do {{count}} kart", + "subtitleMatchExact_one": "Wybierz {{count}} kartę spełniającą wymienione kryteria wyszukiwania", + "subtitleMatchExact_other": "Wybierz {{count}} kart spełniających wymienione kryteria wyszukiwania", + "subtitleMatchUpTo_one": "Wybierz do {{count}} karty spełniającej wymienione kryteria wyszukiwania", + "subtitleMatchUpTo_other": "Wybierz do {{count}} kart spełniających wymienione kryteria wyszukiwania", + "subtitleDistinctExact_one": "Wybierz {{count}} kartę o odrębnych cechach", + "subtitleDistinctExact_other": "Wybierz {{count}} kart o odrębnych cechach", + "subtitleDistinctUpTo_one": "Wybierz do {{count}} karty o odrębnych cechach", + "subtitleDistinctUpTo_other": "Wybierz do {{count}} kart o odrębnych cechach", + "subtitleManaValueExact_one": "Wybierz {{count}} kartę w granicach limitu wartości many", + "subtitleManaValueExact_other": "Wybierz {{count}} kart w granicach limitu wartości many", + "subtitleManaValueUpTo_one": "Wybierz do {{count}} karty w granicach limitu wartości many", + "subtitleManaValueUpTo_other": "Wybierz do {{count}} kart w granicach limitu wartości many" + }, + "searchPartition": { + "title": "Wybierz karty na pole bitwy", + "subtitle_one": "Wybierz {{count}} kartę, aby położyć na pole bitwy{{tapped}}; reszta trafia do twojej ręki", + "subtitle_other": "Wybierz {{count}} kart, aby położyć na pole bitwy{{tapped}}; reszta trafia do twojej ręki", + "tapped": " obrócone" + }, + "outsideGame": { + "title": "Wybierz z sideboardu", + "subtitleExact": "Wybierz {{count}}", + "subtitleUpTo": "Wybierz do {{count}}" + }, + "chooseFromZone": { + "title": "Wybierz karty", + "subtitleExact_one": "Wybierz {{count}} kartę", + "subtitleExact_other": "Wybierz {{count}} kart", + "subtitleUpTo_one": "Wybierz do {{count}} karty", + "subtitleUpTo_other": "Wybierz do {{count}} kart", + "subtitleDistinctCardTypes": "Wybierz do {{count}} kart o odrębnych typach kart" + }, + "pair": { + "title": "Wybierz partnera Soulbond", + "subtitle": "Połącz z niepołączonym stworem, którego kontrolujesz" + }, + "effectZone": { + "titleSacrifice": "Poświęć", + "titleTopdeck": "Połóż na bibliotekę", + "titleBattlefield": "Połóż na pole bitwy", + "subtitleSacrificeExact_one": "Wybierz {{count}} trwałego do poświęcenia", + "subtitleSacrificeExact_other": "Wybierz {{count}} trwałych do poświęcenia", + "subtitleSacrificeUpTo_one": "Wybierz do {{count}} trwałego do poświęcenia", + "subtitleSacrificeUpTo_other": "Wybierz do {{count}} trwałych do poświęcenia", + "subtitleSacrificeRange_one": "Wybierz {{min}}-{{count}} trwałego do poświęcenia", + "subtitleSacrificeRange_other": "Wybierz {{min}}-{{count}} trwałych do poświęcenia", + "subtitleTopdeckExact_one": "Wybierz {{count}} kartę, aby położyć na wierzchu swojej biblioteki", + "subtitleTopdeckExact_other": "Wybierz {{count}} kart, aby położyć na wierzchu swojej biblioteki", + "subtitleTopdeckUpTo_one": "Wybierz do {{count}} karty, aby położyć na wierzchu swojej biblioteki", + "subtitleTopdeckUpTo_other": "Wybierz do {{count}} kart, aby położyć na wierzchu swojej biblioteki", + "subtitleTopdeckRange_one": "Wybierz {{min}}-{{count}} kartę, aby położyć na wierzchu swojej biblioteki", + "subtitleTopdeckRange_other": "Wybierz {{min}}-{{count}} kart, aby położyć na wierzchu swojej biblioteki", + "subtitleBattlefieldExact_one": "Wybierz {{count}} kartę, aby położyć na pole bitwy", + "subtitleBattlefieldExact_other": "Wybierz {{count}} kart, aby położyć na pole bitwy", + "subtitleBattlefieldUpTo_one": "Wybierz do {{count}} karty, aby położyć na pole bitwy", + "subtitleBattlefieldUpTo_other": "Wybierz do {{count}} kart, aby położyć na pole bitwy", + "subtitleBattlefieldRange_one": "Wybierz {{min}}-{{count}} kartę, aby położyć na pole bitwy", + "subtitleBattlefieldRange_other": "Wybierz {{min}}-{{count}} kart, aby położyć na pole bitwy", + "labelSkip": "Pomiń", + "labelDecline": "Odmów", + "labelPutOnTop": "Połóż na wierzch ({{order}})", + "labelConfirm": "Potwierdź ({{selected}}/{{count}})", + "labelTop": "Wierzch ({{selected}}/{{count}})", + "labelPut": "Połóż ({{selected}}/{{count}})", + "orderTop": "Wierzch" + }, + "drawnThisTurn": { + "title": "Dobrane w tej turze", + "subtitle": "Połóż do {{count}} na wierzch; zapłać {{life}} życia za każdą zatrzymaną", + "labelPayLife": "Zapłać {{life}} życia", + "labelConfirm": "Potwierdź ({{selected}}/{{count}})" + }, + "sacrifice": { + "title": "Poświęć", + "subtitle_one": "Wybierz {{count}} trwałego do poświęcenia", + "subtitle_other": "Wybierz {{count}} trwałych do poświęcenia" + }, + "exileBattlefield": { + "title": "Wygnaj", + "subtitle_one": "Wybierz {{count}} trwałego do wygnania", + "subtitle_other": "Wybierz {{count}} trwałych do wygnania" + }, + "multiTarget": { + "title": "Wybierz cele", + "subtitleExact_one": "Wybierz {{count}} cel", + "subtitleExact_other": "Wybierz {{count}} celów", + "subtitleRange": "Wybierz {{min}}–{{max}} celów" + }, + "paradigm": { + "title": "Paradigm", + "subtitle": "Rzuć kopię jednego z tych czarów bez płacenia jego kosztu many lub przekaż." + }, + "payManaAbility": { + "title": "Zapłać koszt zdolności manowej", + "subtitle": "Wybierz, którą manę wydać" + }, + "returnToHand": { + "title": "Zwróć", + "subtitle_one": "Wybierz {{count}} trwałego do zwrócenia", + "subtitle_other": "Wybierz {{count}} trwałych do zwrócenia" + }, + "removeCounter": { + "title": "Usuń znacznik", + "subtitle": "Wybierz trwałego, z którego usunąć znacznik", + "label": "Usuń" + }, + "blight": { + "title": "Blight", + "subtitle_one": "Połóż znacznik -1/-1 na {{count}} stwora, którego kontrolujesz", + "subtitle_other": "Połóż znacznik -1/-1 na {{count}} stworów, których kontrolujesz" + }, + "crew": { + "title": "Załoguj pojazd", + "subtitle": "Obróć stwory o łącznej sile {{power}} lub większej", + "label": "Crew ({{total}}/{{power}})" + }, + "station": { + "title": "Station", + "subtitle": "Obróć innego nieobróconego stwora, którego kontrolujesz. Dodana liczba znaczników ładunku równa jest jego sile.", + "labelWithCharge": "Station (+{{charge}} ładunku)", + "label": "Station" + }, + "saddle": { + "title": "Osiodłaj wierzchowca", + "subtitle": "Obróć stwory o łącznej sile {{power}} lub większej", + "label": "Saddle ({{total}}/{{power}})" + }, + "wardSacrifice": { + "title_one": "Ward — Poświęć trwałego", + "title_other": "Ward — Poświęć {{count}} trwałych", + "subtitle": "Wybierz trwałego do poświęcenia" + }, + "unlessBounce": { + "title_one": "Zwróć trwałego do ręki", + "title_other": "Zwróć {{count}} trwałych do ręki", + "subtitle": "Wybierz trwałego do zwrócenia do ręki jego właściciela" + }, + "exileForCost": { + "titleAlternative": "Koszt alternatywny", + "titleEscape": "Escape", + "sourceHand": "twojej ręki", + "sourceGraveyard": "twojego cmentarza", + "subtitle_one": "Wygnaj {{count}} kartę z {{source}}", + "subtitle_other": "Wygnaj {{count}} kart z {{source}}" + }, + "behold": { + "title": "Behold", + "subtitleExile": "Wygnaj pasującego trwałego lub kartę", + "subtitleChoose": "Wybierz pasującego trwałego lub ujawnij pasującą kartę", + "labelExile": "Wygnaj", + "labelBehold": "Behold" + }, + "collectEvidence": { + "title": "Zbierz dowody", + "subtitle": "Wygnaj karty ze swojego cmentarza o łącznej wartości many {{minimum}} lub większej" + }, + "discard": { + "title": "Odrzuć", + "titleAdditionalCost": "Odrzuć jako koszt dodatkowy", + "titleManaAbility": "Odrzuć dla zdolności manowej", + "titleConnive_one": "Connive — Odrzuć kartę", + "titleConnive_other": "Connive — Odrzuć {{count}} kart", + "titleUpTo": "Odrzuć do {{count}} kart", + "titleExact_one": "Odrzuć kartę", + "titleExact_other": "Odrzuć {{count}} kart", + "titleWard": "Ward — Odrzuć kartę", + "subtitleUpTo_one": "Wybierz do {{count}} karty do odrzucenia", + "subtitleUpTo_other": "Wybierz do {{count}} kart do odrzucenia", + "subtitleUnless": "Wybierz {{count}} kart lub 1 pasującą kartę do odrzucenia", + "subtitleExact_one": "Wybierz {{count}} kartę do odrzucenia", + "subtitleExact_other": "Wybierz {{count}} kart do odrzucenia" + }, + "harmonize": { + "title": "Harmonize", + "subtitle": "Obróć stwora, aby zmniejszyć koszt rzucania o jego siłę, lub pomiń", + "labelSkip": "Pomiń (zapłać pełny koszt)" + }, + "legend": { + "title": "Zasada legend", + "subtitle": "Wybierz, którego „{{name}}” zatrzymać", + "keepAria": "Zatrzymaj {{name}} ({{status}})", + "statusJustEntered": "Właśnie wszedł", + "statusAlready": "Już na polu bitwy" + }, + "commanderZone": { + "title": "Strefa dowodzenia", + "subtitle": "{{name}} został umieszczony w strefie {{zone}}. Zwrócić do strefy dowodzenia?", + "commanderFallback": "Dowódca", + "labelCommandZone": "Strefa dowodzenia", + "labelLeave": "Zostaw w {{zone}}" + }, + "revealUntil": { + "title": "Ujawniaj, aż", + "subtitle": "Położyć {{name}} na pole bitwy?", + "cardFallback": "ujawnioną kartę", + "labelBattlefield": "Na pole bitwy", + "labelInto": "Do {{zone}}" + }, + "repeatProcess": { + "title": "Powtórz ten proces", + "subtitle": "Powtórzyć ten proces ponownie?" + }, + "damageSource": { + "title": "Źródło obrażeń", + "subtitle": "Wybierz źródło" + }, + "manifestDread": { + "title": "Manifest Dread", + "subtitle": "Wybierz kartę do zmanifestowania zakrytej. Druga trafi na twój cmentarz.", + "label": "Potwierdź Manifest" + }, + "manaColor": { + "title": "Wybierz kolor many", + "subtitleBatch": "Wybierz kolor, a następnie ile źródeł obrócić", + "subtitle": "Wybierz, jaki kolor many wytworzyć", + "howMany": "Ile?", + "tapFewer": "Obróć mniej", + "tapMore": "Obróć więcej", + "labelAdd": "Dodaj {{count}}", + "labelConfirm": "Potwierdź" + }, + "manaCombination": { + "title": "Wybierz kombinację many", + "subtitleAny": "Wybierz każdy kolor many do wytworzenia", + "subtitle": "Wybierz, jaką kombinację many wytworzyć" + } + }, + "dialogShell": { + "eyebrow": "Wybór w grze", + "restoreDialog": "Przywróć okno dialogowe", + "peekAria": "Odsuń okno dialogowe na bok", + "peekTitle": "Podejrzyj pole bitwy", + "close": "Zamknij", + "closeTitle": "Zamknij (Esc)" + }, + "choiceOverlay": { + "eyebrow": "Wybór w grze", + "scrollLeft": "Przewiń w lewo", + "scrollRight": "Przewiń w prawo", + "confirm": "Potwierdź", + "cancel": "Anuluj" + }, + "adventureCast": { + "eyebrow": "Adventure", + "title": "Wybierz stronę", + "subtitle": "Rzuć jako stwór lub jako czar Adventure.", + "adventureFallback": "Adventure", + "castNamed": "Rzuć {{name}}", + "creatureTag": "(Stwór)", + "adventureTag": "(Adventure)" + }, + "alternativeCost": { + "title": "Wybierz koszt rzucania", + "warpEyebrow": "Warp", + "warpNormalLabel": "Rzuć normalnie", + "warpAltLabel": "Rzuć z Warp", + "warpAltSuffix": "(wygnanie w kroku końcowym)", + "warpSubtitle": "Rzuć {{name}} normalnie lub użyj jego kosztu Warp.", + "evokeEyebrow": "Evoke", + "evokeNormalLabel": "Rzuć normalnie", + "evokeAltLabel": "Rzuć z Evoke", + "evokeSubtitle": "Rzuć {{name}} normalnie lub rzuć go za jego koszt Evoke.", + "overloadEyebrow": "Overload", + "overloadNormalLabel": "Rzuć normalnie", + "overloadAltLabel": "Rzuć z Overload", + "overloadSubtitle": "Rzuć {{name}} normalnie, obierając za cel jednego trwałego, lub zapłać jego koszt Overload, aby wpłynąć na wszystkie poprawne cele.", + "bestowEyebrow": "Bestow", + "bestowNormalLabel": "Rzuć jako stwora", + "bestowAltLabel": "Rzuć z Bestow", + "bestowSubtitle": "Rzuć {{name}} normalnie jako stwora lub zapłać jego koszt Bestow, aby rzucić go jako Aurę.", + "additionalExile": "+ Wygnaj kartę", + "additionalSacrifice": "+ Poświęć trwałego", + "additionalPayLife": "+ Zapłać życie", + "additionalDiscard": "+ Odrzuć kartę", + "additionalTapCreatures": "+ Obróć stwory", + "additionalGeneric": "+ {{type}}" + }, + "battleProtector": { + "title": "Wybierz obrońcę", + "battleFallback": "Bitwa", + "subtitle": "{{name}} potrzebuje nowego obrońcy. Wybierz, który przeciwnik będzie jej bronił." + }, + "cardDataMissing": { + "title": "Brak danych kart", + "body": "Nie znaleziono card-data.json. Gra potrzebuje definicji kart, aby grać prawdziwymi kartami.", + "generatePrompt": "Wygeneruj je, uruchamiając:", + "placeFile": "Następnie umieść plik wynikowy w client/public/card-data.json i odśwież.", + "continueAnyway": "Kontynuuj mimo to" + }, + "cascadeChoice": { + "cascadeEyebrow": "Cascade", + "discoverEyebrow": "Discover", + "title": "Rzucić {{name}}?", + "subtitleCascade": "Cascade wygnało {{name}} (wartość many poniżej {{sourceMv}}). Rzuć go bez płacenia jego kosztu many lub odmów i przetasuj wszystkie {{total}} wygnanych kart na spód swojej biblioteki.", + "subtitleDiscover": "Discover wygnało {{name}}. Rzuć go bez płacenia jego kosztu many lub weź go do ręki i przetasuj pozostałe {{missCount}} wygnanych kart na spód swojej biblioteki.", + "castNamed": "Rzuć {{name}}", + "castSuffix": "(bez płacenia jego kosztu many)", + "putIntoHand": "Weź do ręki", + "decline": "Odmów", + "discoverDeclineSuffix": "(połóż resztę na spód)", + "cascadeDeclineSuffix": "(przetasuj wszystkie wygnane karty na spód)" + }, + "castingVariant": { + "eyebrow": "Rzuć", + "title": "Wybierz rzucanie", + "variantNormal": "Rzuć normalnie", + "variantAdventure": "Rzuć jako Adventure", + "variantOmen": "Rzuć jako Omen", + "variantWarp": "Rzuć z Warp", + "variantEscape": "Rzuć z Escape", + "variantRetrace": "Rzuć z Retrace", + "variantHarmonize": "Rzuć z Harmonize", + "variantFlashback": "Rzuć z Flashback", + "variantAftermath": "Rzuć z Aftermath", + "variantGraveyardPermission": "Rzuć z cmentarza", + "variantHandPermission": "Rzuć z ręki", + "variantMiracle": "Rzuć z Miracle", + "variantMadness": "Rzuć z Madness", + "variantEvoke": "Rzuć z Evoke", + "variantSuspend": "Rzuć z Suspend", + "variantPlot": "Rzuć z Plot", + "variantForetell": "Rzuć z Foretell", + "variantOverload": "Rzuć z Overload", + "variantBestow": "Rzuć z Bestow", + "variantFallback": "Rzuć z {{type}}" + }, + "categoryChoice": { + "title": "Wybierz trwałe do zatrzymania", + "subtitleOpponent": "Wybierz po jednym trwałym każdego typu spośród trwałych niebędących ziemiami, które kontroluje {{name}}; reszta zostaje poświęcona.", + "subtitleSelf": "Wybierz po jednym trwałym każdego typu spośród trwałych niebędących ziemiami, które kontrolujesz; reszta zostaje poświęcona.", + "noneToKeep": "Brak {{category}} — nic do zatrzymania", + "objectFallback": "Obiekt {{id}}" + }, + "chooseOneOfBranch": { + "eyebrow": "Wybór", + "title": "Wybierz jedno", + "subtitle": "Wybierz opcję do rozpatrzenia.", + "optionFallback": "Opcja {{number}}" + }, + "combatTax": { + "eyebrow": "Podatek bojowy", + "titleAttack": "Zapłać, aby atakować", + "titleBlock": "Zapłać, aby blokować", + "subtitleAttack": "Jeden lub więcej atakujących jest opodatkowanych. Zapłać sumę lub usuń ich z ataku.", + "subtitleBlock": "Jeden lub więcej blokujących jest opodatkowanych. Zapłać sumę lub usuń ich z bloku.", + "declineAttack": "Odmów (usuń opodatkowanych atakujących)", + "declineBlock": "Odmów (usuń opodatkowanych blokujących)", + "perCreatureBreakdown": "Zestawienie na stwora", + "total": "Suma", + "pay": "Zapłać", + "creatureFallback": "Stwór {{id}}" + }, + "distributeAmong": { + "unitCounter": "znacznik {{counter}}", + "unitDamage": "obrażeń", + "unitLife": "życia", + "title": "Rozdziel {{total}} {{unit}}", + "subtitle": "Przydziel co najmniej 1 {{unit}} każdemu celowi. Pozostało: {{remaining}}" + }, + "dungeonChoice": { + "title": "Wybierz loch", + "subtitle": "Wybierz loch, do którego się wyprawić", + "roomTitle": "Wybierz pokój", + "roomSubtitle": "Awansuj w {{name}}" + }, + "engineLost": { + "crashTitle": "Silnik uległ awarii", + "crashBody": "phase.rs napotkał wewnętrzny błąd i nie może bezpiecznie kontynuować tej akcji. Twoja ostatnia zapisana tura jest zachowana — odśwież, aby przywrócić grę. Zgłoś to, abyśmy mogli to naprawić.", + "connectionTitle": "Utracono połączenie z silnikiem", + "connectionBody": "phase.rs utracił połączenie z silnikiem gry — najczęściej spowodowane aktualizacją w tle aktywowaną w trakcie gry. Twoja ostatnia zapisana tura jest zachowana; odśwież, aby przywrócić grę.", + "diagnostic": "diagnostyka: {{reason}}", + "showDetails": "Pokaż szczegóły", + "copyDiagnostic": "Kopiuj diagnostykę", + "copied": "Skopiowano!", + "reportOnGithub": "Zgłoś na GitHub", + "reload": "Odśwież", + "reportTitle": "Awaria silnika: {{summary}}", + "reportConnectionSummary": "Utracono połączenie z silnikiem", + "reportWhatHappened": "**Co się stało**\n_opisz krótko, co robiłeś_\n\n**Diagnostyka**\n```\n{{diagnostic}}\n```", + "copyPrompt": "Skopiuj tę diagnostykę:" + }, + "miracleReveal": { + "eyebrowMiracle": "Miracle", + "eyebrowMadness": "Madness", + "titleReveal": "Ujawnić {{name}}?", + "titleCast": "Rzucić {{name}}?", + "subtitleReveal": "Możesz ujawnić tę kartę, aby rzucić ją za jej koszt Miracle.", + "subtitleCastMiracle": "Możesz rzucić tę kartę za jej koszt Miracle.", + "subtitleCastMadness": "Możesz rzucić tę kartę za jej koszt Madness.", + "reveal": "Ujawnij", + "cast": "Rzuć", + "decline": "Odmów" + }, + "modalFace": { + "eyebrow": "Modalny DFC", + "title": "Wybierz stronę", + "subtitle": "Wybierz, którą stronę zagrać lub rzucić.", + "backFaceFallback": "Tylna strona", + "labelFront": "Przód", + "labelBack": "Tył", + "play": "Zagraj {{name}}", + "cast": "Rzuć {{name}}" + }, + "modeChoice": { + "eyebrowAbility": "Tryby zdolności", + "eyebrowSpell": "Tryby czaru", + "subtitle": "Wybierz tryb lub tryby do zastosowania.", + "chooseExact": "Wybierz {{count}}", + "chooseRange": "Wybierz od {{min}} do {{max}}", + "confirm": "Potwierdź ({{selected}}/{{count}})", + "clear": "Wyczyść", + "alreadyChosen": "(już wybrane)" + }, + "namedChoice": { + "title": { + "creatureType": "Wybierz typ stwora", + "color": "Wybierz kolor", + "oddOrEven": "Wybierz nieparzyste lub parzyste", + "basicLandType": "Wybierz typ ziemi podstawowej", + "cardType": "Wybierz typ karty", + "cardName": "Nazwij kartę", + "landType": "Wybierz typ ziemi", + "opponent": "Wybierz przeciwnika", + "player": "Wybierz gracza", + "twoColors": "Wybierz dwa kolory", + "numberRange": "Wybierz liczbę", + "labeled": "Dokonaj wyboru", + "keyword": "Wybierz zdolność", + "fallback": "Dokonaj wyboru" + }, + "searchSubtitle": "Pisz, aby przeszukać wszystkie karty", + "searchPlaceholder": "Szukaj po nazwie...", + "noCardsFound": "Nie znaleziono kart", + "buttonSubtitle": "Wybierz jedną opcję" + }, + "nonFatalPanic": { + "heading": "Ostrzeżenie silnika (niekrytyczne)", + "dismiss": "Odrzuć", + "body": "phase.rs napotkał wewnętrzne ostrzeżenie, ale odzyskał sprawność. Można bezpiecznie kontynuować grę. Prosimy zgłosić to, abyśmy mogli zbadać sprawę.", + "dontShowAgain": "Nie pokazuj ponownie w tej sesji", + "reportTitle": "Niekrytyczna awaria silnika: {{summary}}", + "reportWhatHappened": "**Co się stało**\n_opisz krótko, co robiłeś_\n\n**Diagnostyka**\n```\n{{diagnostic}}\n```" + }, + "optionalEffect": { + "sourceFallback": "Efekt", + "title": "{{name}} - Efekt opcjonalny", + "yes": "Tak", + "no": "Nie", + "dontAskAgain": "Nie pytaj ponownie w tej grze" + }, + "permanentTypeSlot": { + "eyebrow": "Typ trwałego", + "title": "Wybierz slot typu", + "subtitle": "{{name}} ma wiele typów trwałego. Wybierz, którego slotu typu użyć." + }, + "proliferate": { + "proliferateTitle": "Proliferate", + "proliferateSubtitle": "Wybierz dowolną liczbę trwałych i graczy ze znacznikami. Każdy wybrany cel otrzymuje po jednym dodatkowym znaczniku każdego rodzaju, który już tam jest.", + "chooseObjectsTitle": "Wybierz trwałe", + "chooseObjectsSubtitle": "Wybierz dowolną liczbę trwałych. Płacisz koszt za każdego wybranego.", + "selectAll": "Wszystko", + "selectNone": "Żaden", + "selectMine": "Moja strona", + "confirm": "Potwierdź" + }, + "replacement": { + "eyebrow": "Kolejność rozpatrywania", + "title": "Efekty zastępujące", + "subtitle": "Wybierz, który efekt zastępujący zastosować jako pierwszy.", + "candidateFallback": "Efekt zastępujący {{number}}" + }, + "retargetChoice": { + "title": "Zmień cel", + "scopeSingle": "Wybierz nowy cel dla czaru", + "scopeMulti": "Wybierz nowe cele dla czaru", + "subtitle": "{{scope}}. Obecnie: {{current}}", + "confirm": "Potwierdź", + "badgeNewTarget": "Nowy cel" + }, + "separatePiles": { + "chooserName": "Gracz {{number}}", + "partitionTitle": "Rozdziel na dwa stosy", + "partitionSubtitle": "Rozdziel swoje stwory na dwa stosy. {{chooser}} wybierze jeden.", + "partitionSubtitleRemaining_one": "Rozdziel swoje stwory na dwa stosy. {{chooser}} wybierze jeden. ({{count}} gracz więcej po tobie)", + "partitionSubtitleRemaining_other": "Rozdziel swoje stwory na dwa stosy. {{chooser}} wybierze jeden. ({{count}} graczy więcej po tobie)", + "pileALabel": "Stos A:", + "pileBLabel": "Stos B:", + "pileAria": "{{name}} — stos {{pile}}", + "pileBadge": "Stos {{pile}}", + "chooseTitle": "Wybierz stos", + "chooseSubtitle": "Wybierz jeden ze stosów gracza {{subject}}.", + "chooseSubtitleRemaining_one": "Wybierz jeden ze stosów gracza {{subject}}. ({{count}} podział więcej po tym)", + "chooseSubtitleRemaining_other": "Wybierz jeden ze stosów gracza {{subject}}. ({{count}} podziałów więcej po tym)", + "pileHeader": "Stos {{pile}} ({{count}})", + "empty": "(pusty)", + "choosePileA": "Wybierz stos A", + "choosePileB": "Wybierz stos B" + }, + "tribute": { + "sourceFallback": "Stwór z Tribute", + "title": "Tribute — {{name}}", + "subtitle_one": "Położyć {{count}} znacznik +1/+1 na {{name}}?", + "subtitle_other": "Położyć {{count}} znaczników +1/+1 na {{name}}?", + "payLabel": "Zapłać Tribute", + "payDescription_one": "Połóż {{count}} znacznik +1/+1 na {{name}}.", + "payDescription_other": "Połóż {{count}} znaczników +1/+1 na {{name}}.", + "declineLabel": "Odmów", + "declineDescription": "Odmów Tribute. Wyzwala „jeśli Tribute nie został zapłacony”." + }, + "triggerOrder": { + "eyebrow": "Kolejność rozpatrywania", + "title": "Ustal kolejność swoich zdolności wyzwalanych", + "subtitle": "Wybierz kolejność, w jakiej te wyzwalacze trafią na stos. Wierzch tej listy rozpatrywany jest jako ostatni (CR 405.3).", + "confirmOrder": "Potwierdź kolejność", + "resolvesLast": "Rozpatrywany jako ostatni (spód stosu)", + "resolvesFirst": "Rozpatrywany jako pierwszy (wierzch stosu)", + "moveUp": "Przesuń w górę", + "moveDown": "Przesuń w dół", + "triggerFallback": "Wyzwalacz {{number}}" + }, + "unhandledWaitingFor": { + "title": "Wymagana akcja, ale brakuje interfejsu", + "body": "Gra oczekuje na twoją akcję, ale ta wersja phase.rs nie ma jeszcze do tego interfejsu. To błąd — prosimy go zgłosić, abyśmy mogli go naprawić. Użyj przycisku poniżej, aby opuścić grę.", + "missingState": "Brakujący stan", + "copyDiagnostic": "Kopiuj diagnostykę", + "copied": "Skopiowano!", + "reportOnGithub": "Zgłoś na GitHub", + "copyPrompt": "Skopiuj tę diagnostykę:", + "reportTitle": "Nieobsłużony WaitingFor: {{type}}", + "reportWhatHappened": "**Co się stało**\n_opisz krótko, co robiłeś_\n\n**Diagnostyka**\n```\n{{diagnostic}}\n```" + }, + "voteChoice": { + "subjectName": "Gracz {{number}}", + "titleLabel": "Oznacz gracza", + "titleVote": "Głosuj", + "subtitleLabel": "Wybierz oznaczenie dla {{name}}", + "subtitleVoteRemaining": "Oddaj głos (pozostało {{count}})", + "subtitleVote": "Oddaj swój głos" + }, + "advance": { + "resolve": "Rozpatrz", + "passPriority": "Przekaż priorytet", + "toPhase": "Do {{phase}}" + }, + "outsideGame": { + "fromExile": "Z wygnania", + "fromSideboard": "Z zaplecza" + }, + "phaseName": { + "Upkeep": "Utrzymanie", + "Draw": "Dobieranie", + "PreCombatMain": "Główna faza 1", + "BeginCombat": "Początek walki", + "DeclareAttackers": "Deklaracja atakujących", + "DeclareBlockers": "Deklaracja blokujących", + "CombatDamage": "Obrażenia bojowe", + "EndCombat": "Koniec walki", + "PostCombatMain": "Główna faza 2", + "End": "Krok końcowy", + "Cleanup": "Sprzątanie" + } +} diff --git a/client/src/i18n/locales/pl/menu.json b/client/src/i18n/locales/pl/menu.json new file mode 100644 index 0000000000..7837ac0929 --- /dev/null +++ b/client/src/i18n/locales/pl/menu.json @@ -0,0 +1,233 @@ +{ + "backButton": { + "label": "Wstecz" + }, + "aiDifficulty": { + "label": "Poziom trudności AI", + "ariaLabel": "Poziom trudności AI: {{difficulty}}", + "levels": { + "VeryEasy": "Bardzo łatwy", + "Easy": "Łatwy", + "Medium": "Średni", + "Hard": "Trudny", + "VeryHard": "Bardzo trudny" + } + }, + "bracketFilter": { + "label": "Filtr poziomu" + }, + "gamePresets": { + "heading": "Szybki start" + }, + "shell": { + "disclaimer": "phase.rs to niekomercyjny projekt fanowski, niepowiązany z Wizards of the Coast ani przez nich nieautoryzowany. Magic: The Gathering jest © Wizards of the Coast LLC. Obrazy kart są pobierane ze Scryfall w czasie działania; dane kart pochodzą z MTGJSON. Żadne objęte prawem autorskim zasoby WotC nie są dołączone do tego projektu." + }, + "home": { + "resume": { + "title": "Wznów grę", + "description": "Kontynuuj ostatnio zapisany pojedynek od bieżącej tury i stanu pola gry." + }, + "setup": { + "title": "Graj z AI", + "titleSaved": "Nowy pojedynek z AI", + "description": "Rozegraj samotny pojedynek przeciwko przeciwnikowi AI — wybierz format, talię, archetyp i poziom trudności." + }, + "online": { + "title": "Graj online", + "description": "Załóż pokój, dołącz przez kod lub połącz się ponownie z trybem wieloosobowym." + }, + "draft": { + "title": "Draft", + "description": "Szybki draft przeciwko AI oraz eksperymentalne opcje draftu kostkowego i podu." + }, + "decks": { + "title": "Talie", + "description": "Otwórz zapisane talie, zmień aktywną listę i edytuj konstrukcje." + }, + "social": { + "sponsor": "Wesprzyj" + }, + "preview": { + "cta": "Wypróbuj wersję zapowiadającą", + "tooltip": "Zagraj w najnowszą wersję zapowiadającą — nowe karty i poprawki trafiają tu przed każdym wydaniem." + }, + "coverage": { + "title": "Otwórz panel obsługi kart", + "ariaLabel": "Otwórz panel obsługi kart", + "heading": "Panel obsługi kart", + "viewDetails": "Zobacz szczegóły" + }, + "preparingCards": "Przygotowywanie kart…", + "alpha": { + "label": "Wczesna alfa", + "message": " — spodziewaj się niedziałających kart i brakujących funkcji." + }, + "savedMatchAvailable": "Dostępny zapisany pojedynek", + "load": { + "title": "Wczytaj stan gry" + }, + "exit": "Wyjdź" + }, + "loadState": { + "title": "Wczytaj stan gry", + "subtitle": "Przywróć wyeksportowany lub wklejony stan i kontynuuj grę przeciwko AI.", + "tabPaste": "Wklej JSON", + "tabFile": "Z pliku", + "pastePlaceholder": "Wklej JSON stanu gry…", + "fileSupports": "Obsługuje pliki .json, .txt i .zip wyeksportowane z panelu debugowania", + "chooseFile": "Wybierz plik", + "load": "Wczytaj" + }, + "aiOpponent": { + "heading": "Przeciwnik AI", + "headingMulti": "Przeciwnicy AI ({{count}})", + "analyzingDecks": "Analizowanie talii…", + "noLegalDecks": "Brak dozwolonych talii AI dla tego formatu.", + "catalogUnavailable": "Katalog talii AI niedostępny: {{error}}", + "randomPoolFilters": "Filtry losowej puli", + "archetype": "Archetyp", + "cardCoverage": "Obsługa kart", + "coverageThresholdHint": "Wyklucz talie poniżej tego progu obsługi przez silnik", + "bracket": "Poziom", + "bracketHint": "Losowe AI wybiera z tych poziomów. Talie bez tagów są wykluczane podczas filtrowania.", + "opponentLabel": "Przeciwnik {{number}}", + "deck": "Talia", + "difficulty": "Poziom trudności", + "deckRandom": "Losowa", + "deckRandomCount": "Losowa ({{count}})", + "source": { + "feed": "Kanał", + "user": "Użytkownik", + "precon": "Precon" + } + }, + "feedManager": { + "title": "Zarządzaj kanałami", + "refreshing": "Odświeżanie…", + "refreshAll": "Odśwież wszystkie", + "refresh": "Odśwież", + "subscribe": "Subskrybuj", + "unsubscribe": "Anuluj subskrypcję", + "lastRefreshed": "Ostatnio odświeżono: {{date}}", + "addCustomFeed": "Dodaj własny kanał", + "add": "Dodaj", + "done": "Gotowe" + }, + "importDeck": { + "title": "Importuj talię", + "tabPaste": "Wklej tekst", + "tabFile": "Z pliku", + "deckNamePlaceholder": "Nazwa talii", + "import": "Importuj", + "fileSupports": "Obsługuje formaty .dck, .dec, .txt oraz MTGA", + "chooseFile": "Wybierz plik" + }, + "precon": { + "savePrompt": "Zapisz gotową talię jako:", + "overwriteConfirm": "„{{name}}” już istnieje. Nadpisać?", + "overwriteAllConfirm": "Wszystkie {{count}} wybrane talie już istnieją. Nadpisać?", + "overwriteSomeConfirm": "{{conflicts}} z {{total}} wybranych talii już istnieje. Nadpisać duplikaty? (Anuluj zachowuje istniejące kopie i importuje resztę.)", + "importedSkipped_one": "Zaimportowano {{count}} talię; pominięto {{skipped}} istniejących.", + "importedSkipped_other": "Zaimportowano {{count}} talii; pominięto {{skipped}} istniejących.", + "title": "Gotowe talie", + "subtitle": "Pochodzą z MTGJSON AllDeckFiles — każdy precon wydany przez WotC.", + "totalCount": "· {{count}} łącznie", + "searchPlaceholder": "Szukaj według nazwy, kodu dodatku lub typu…", + "allTypes": "Wszystkie ({{count}})", + "loadingCatalog": "Wczytywanie katalogu talii…", + "noMatch": "Brak pasujących talii.", + "selectDeck": "Wybierz {{name}}", + "cardCount_one": "{{count}} karta", + "cardCount_other": "{{count}} kart", + "commanderSuffix": " · dow.", + "showingFirst": "Wyświetlono pierwsze {{count}} — zawęź wyszukiwanie.", + "selectAllVisible": "Zaznacz wszystkie widoczne", + "clearSelection": "Wyczyść ({{count}})", + "importSelected": "Importuj {{count}} wybranych" + }, + "deckTile": { + "feedBadge": "Kanał", + "edit": "Edytuj {{name}}", + "delete": "Usuń", + "deleteTitle": "Usuń talię", + "copy": "Kopiuj do Moich talii", + "copyTitle": "Kopiuj do Moich talii (usuwa śledzenie kanału)", + "cardCount_one": "{{count}} karta", + "cardCount_other": "{{count}} kart", + "preconBadge": "precon", + "unknown": "Nieznane {{count}}", + "unknownCardsTitle": "Nieznane karty:\n{{cards}}", + "unsupportedTitle": "Nieobsługiwane ({{unique}} unikalnych, {{copies}} kopii):\n{{cards}}", + "setIconAlt": "Ikona dodatku {{code}}" + }, + "myDecks": { + "headingManage": "Moje talie", + "headingSelect": "Wybierz talię", + "tabDecks": "Moje talie", + "tabSubscriptions": "Subskrypcje", + "createNew": "Utwórz nową", + "refreshing": "Odświeżanie…", + "refreshAll": "Odśwież wszystkie", + "manageFeeds": "Zarządzaj kanałami", + "searchPlaceholder": "Szukaj talii…", + "formatLabel": "Format", + "browseAetherhub": "Przeglądaj talie {{format}} na Aetherhub", + "showAllDecks": "Pokaż wszystkie talie", + "showLegalOnly": "Pokaż tylko dozwolone", + "showAllDecksButton": "Pokaż wszystkie talie", + "sortName": "Nazwa", + "sortDateAdded": "Data dodania", + "sortFormat": "Format", + "ascending": "Rosnąco", + "descending": "Malejąco", + "saveAsPrompt": "Zapisz jako:", + "banner": { + "showingAllFor": "Wyświetlanie wszystkich zapisanych talii dla", + "showingLegalIn": "Wyświetlanie talii dozwolonych w", + "deckCount": "· {{visible}} z {{total}}" + }, + "status": { + "fallbackUserDeck": "talia użytkownika", + "fallbackVisibleDeck": "widoczna talia", + "startingWorker": "Uruchamianie procesu zgodności…", + "loadingDatabase": "Wczytywanie bazy danych zgodności…", + "checkingDeck": "Sprawdzanie {{deck}}…", + "checkedDeck": "Sprawdzono {{deck}}", + "loadingCoverage": "Wczytywanie obsługi dla {{deck}}…", + "checkingSelected": "Sprawdzanie zgodności wybranej talii…", + "evaluatingVisible": "Ocenianie widocznych talii…", + "remaining": "Pozostało {{count}}" + }, + "compatibilityError": "Sprawdzanie zgodności niedostępne: {{error}}", + "empty": { + "title": "Brak talii pasujących do tego filtra.", + "selectHint": "Zaimportuj zgodną talię lub zmień format, aby zobaczyć dostępne talie.", + "manageHint": "Wybierz inny filtr lub pokaż wszystkie talie, aby wybrać z całej kolekcji." + }, + "sectionMyDecks": "Moje talie", + "sectionStarterDecks": "Talie startowe", + "sectionLegalPrecons": "Dozwolone precony", + "importDeckTile": "Importuj talię", + "preconstructedTile": "Gotowa talia", + "browseAll": "Przeglądaj wszystkie", + "loadMore": "Wczytaj więcej", + "selectedDeck": "Wybrana talia", + "chooseDeckToContinue": "Wybierz talię, aby kontynuować", + "filterAll": "Wszystkie" + }, + "subscriptions": { + "emptyTitle": "Brak subskrypcji kanałów", + "emptyDescription": "Subskrybuj kanały talii, aby otrzymywać wyselekcjonowane kolekcje talii, które aktualizują się automatycznie.", + "feedMeta_one": "· {{count}} talia · Zaktualizowano {{date}}", + "feedMeta_other": "· {{count}} talii · Zaktualizowano {{date}}", + "error": "Błąd: {{error}}" + }, + "myDecksPage": { + "eyebrow": "Talie", + "title": "Talie.", + "description": "Otwórz zapisaną listę, zaimportuj nową lub kontynuuj w kreatorze talii." + }, + "loadGameState": { + "readFailed": "Nie udało się odczytać pliku" + } +} diff --git a/client/src/i18n/locales/pl/multiplayer.json b/client/src/i18n/locales/pl/multiplayer.json new file mode 100644 index 0000000000..dd1e4551ba --- /dev/null +++ b/client/src/i18n/locales/pl/multiplayer.json @@ -0,0 +1,236 @@ +{ + "concedeDialog": { + "title": "Poddać grę?", + "message": "Twój przeciwnik zostanie ogłoszony zwycięzcą.", + "concede": "Poddaj się" + }, + "connectionDot": { + "connected": "Połączono", + "connecting": "Łączenie...", + "disconnected": "Rozłączono" + }, + "connectionToast": { + "opponentDisconnected": "Przeciwnik się rozłączył", + "forfeitCountdown": "— {{seconds}}s do walkowera", + "retry": "Ponów", + "settings": "Ustawienia" + }, + "emoteOverlay": { + "ariaLabel": "Emotki", + "options": { + "goodGame": "Dobra gra", + "nicePlay": "Niezłe zagranie", + "thinking": "Myślę...", + "hello": "Cześć!", + "oops": "Ups" + } + }, + "lobbyProgress": { + "waiting": "Oczekiwanie na graczy…", + "playersReady": "{{joined}} / {{total}} graczy gotowych" + }, + "serverOfflineDialog": { + "title": "Serwer offline", + "couldNotConnect": "Nie udało się połączyć z dedykowanym serwerem rozgrywki sieciowej.", + "switchedToP2P": "Przełączono na tryb tylko P2P na tę sesję.", + "serverAddress": "Adres serwera:", + "dismiss": "Zamknij", + "openSettings": "Otwórz ustawienia" + }, + "sideboardModal": { + "game": "Gra {{number}}", + "title": "Wymiana kart bocznych", + "matchScore": "Wynik meczu {{p0}}-{{p1}}{{draws}}. Przenoś karty między talią główną a kartami bocznymi, a potem zatwierdź.", + "draws_one": " ({{count}} remis)", + "draws_other": " ({{count}} remisów)", + "main": "Główna ({{count}}/{{total}})", + "sideboard": "Karty boczne ({{count}})", + "moveFromSideboard": "Przenieś karty z kart bocznych.", + "moveFromMain": "Przenieś karty z talii głównej.", + "matchesRegistered": "Talia główna odpowiada zarejestrowanemu rozmiarowi ({{total}}).", + "submitDisabled": "Talia główna ma {{count}} / {{total}} — zatwierdzanie wyłączone.", + "resetAria": "Przywróć zarejestrowaną talię", + "reset": "Resetuj", + "submitAria": "Zatwierdź talię na następną grę", + "submitDeck": "Zatwierdź talię" + }, + "brokerOfflinePrompt": { + "title": "Serwer poczekalni nieosiągalny", + "message": "Twoja gra nie będzie publicznie wyświetlana. Nadal możesz być gospodarzem przez P2P — po prostu udostępnij kod pokoju bezpośrednio przeciwnikowi.", + "continueWithoutLobby": "Kontynuuj bez poczekalni" + }, + "joinErrorDialog": { + "dismiss": "Zamknij" + }, + "serverOfflinePrompt": { + "title": "Dobieranie graczy nieosiągalne", + "message": "Nie udało się połączyć z serwerem gry, więc publiczna poczekalnia jest teraz niedostępna. Nadal możesz grać, udostępniając bezpośredni kod znajomemu.", + "keepTrying": "Próbuj dalej", + "useDirectCode": "Użyj bezpośredniego kodu" + }, + "playerIdentityBanner": { + "playerName": "Nazwa gracza", + "namePlaceholder": "Wpisz swoją nazwę", + "editTitle": "Kliknij, aby edytować swoją nazwę", + "setName": "Ustaw nazwę…", + "change": "Zmień" + }, + "gameListItem": { + "waitTimeJustNow": "przed chwilą", + "waitTimeMinutes": "{{count}} min temu", + "waitTimeHours": "{{count}} godz. temu", + "buildMismatchTitle": "Gospodarz ma wersję {{version}} ({{commit}}) — twoja kompilacja jest inna. Odśwież, aby zaktualizować.", + "gameFull": "Ta gra jest pełna.", + "sandboxConfirm": "To jest gra w trybie piaskownicy. Gracze z uprawnieniami debugowania mogą bezpośrednio manipulować stanem gry. Kontynuować?", + "draftBadgeTitle": "Draft {{kind}} — {{setCode}}", + "draftBadge": "Draft {{setCode}}", + "p2pBadgeTitle": "Gra peer-to-peer (gospodarz uruchamia silnik)", + "sandboxBadgeTitle": "Ta gra pozwala na akcje debugowania. Używaj do testów — to nie jest mecz wyczynowy.", + "anonymous": "Anonimowy", + "by": "autor: {{name}}", + "passwordProtected": "Chronione hasłem" + }, + "serverPicker": { + "title": "Serwer", + "subtitle": "Wybierz region lub połącz się z własną instancją.", + "noneLabel": "Brak (tylko P2P)", + "directCodes": "bezpośrednie kody", + "selfHosted": "Własny hosting", + "customUrlPlaceholder": "wss://twoj-serwer.example/ws", + "test": "Testuj", + "use": "Użyj", + "urlError": "URL musi zaczynać się od ws:// lub wss://", + "connected": "Połączono", + "connectionFailed": "Połączenie nie powiodło się", + "testing": "Testowanie…" + }, + "lobbyDraftRooms": { + "noRooms": "Brak dostępnych pokoi draftu", + "playerHosted": "Hostowane przez gracza", + "server": "Serwer", + "passwordProtected": "Chronione hasłem", + "join": "Dołącz", + "createTitle": "Utwórz draft serwerowy", + "set": "Dodatek", + "kind": "Rodzaj", + "premier": "Premier", + "traditional": "Tradycyjny", + "podSize": "Rozmiar grupy", + "timerSeconds": "Czas (s)", + "passwordOptional": "Hasło (opcjonalnie)", + "passwordPlaceholder": "Pozostaw puste dla gry publicznej", + "creating": "Tworzenie...", + "createDraft": "Utwórz draft" + }, + "lobbyView": { + "directConnection": "Połączenie bezpośrednie", + "onlineLobby": "Poczekalnia online", + "pickServerTitle": "Wybierz serwer, aby korzystać z trybu poczekalni online", + "pickServer": "Wybierz serwer", + "online": "{{count}} online", + "format": "Format", + "allFormats": "Wszystkie formaty", + "roomTypeAll": "Wszystkie", + "roomTypeDraft": "Draft", + "roomTypeP2P": "P2P", + "roomTypeServer": "Serwer", + "openTables": "Otwarte stoły", + "noFormatGames": "Brak gier {{format}} w tej chwili.", + "noOpenGames": "Brak otwartych gier w tej chwili.", + "showAllFormats": "Pokaż wszystkie formaty", + "p2pNotice": "Dedykowany serwer niedostępny. Nadal możesz być gospodarzem lub dołączyć bezpośrednio za pomocą 5-znakowego kodu pokoju.", + "joinByCode": "Dołącz za pomocą kodu", + "joinATable": "Dołącz do stołu", + "p2pCodePlaceholder": "Wpisz 5-znakowy kod P2P", + "serverCodePlaceholder": "Wpisz kod lub KOD@IP:PORT", + "join": "Dołącz", + "host": "Hostuj", + "hostP2PDescription": "Utwórz bezpośredni pokój dla jednego przeciwnika.", + "hostServerDescription": "Otwórz pokój i czekaj na graczy.", + "hostDraft": "Hostuj draft", + "hostGame": "Hostuj grę", + "hostP2PGame": "Hostuj grę P2P", + "passwordRequired": "Wymagane hasło", + "passwordPlaceholder": "Wpisz hasło" + }, + "hostSetup": { + "hostDirectMatch": "Hostuj mecz bezpośredni", + "hostMatch": "Hostuj mecz", + "p2pNotice": "Dedykowany serwer jest niedostępny, więc ten pokój użyje połączenia bezpośredniego.", + "roomName": "Nazwa pokoju", + "optional": "(opcjonalnie)", + "roomNamePlaceholder": "np. Piątkowy wieczór Commander", + "roomNameDefaultPlaceholder": "Stół gracza {{name}}", + "roomNameHelp": "Wyświetlana graczom przeglądającym poczekalnię.", + "roomNameHelpDefault": " Domyślnie \"Stół gracza {{name}}\".", + "format": "Format", + "startingLife": "Początkowe życie", + "deckSize": "Rozmiar talii", + "players": "Gracze", + "matchType": "Typ meczu", + "bo1": "BO1", + "bo3": "BO3", + "bo3Note": "BO3 jest dostępne tylko dla meczów 2-osobowych.", + "commanderDamage": "Obrażenia od dowódcy", + "playerSeats": "Miejsca graczy", + "seat": "Miejsce {{number}}", + "youHost": "Ty (gospodarz)", + "ai": "SI", + "human": "Człowiek", + "waitingForPlayer": "Oczekiwanie na gracza", + "startWhenFull": "Rozpocznij po zapełnieniu", + "listInLobby": "Wyświetl w poczekalni", + "sandboxMode": "Tryb piaskownicy — zezwól na akcje debugowania", + "sandboxModeHelp": "Gospodarz może bezpośrednio manipulować stanem gry (przenosić karty, zmieniać życie, modyfikować znaczniki) i przyznawać uprawnienia debugowania innym graczom. Używaj do testów lub gry w piaskownicy — nie do meczów wyczynowych. Tego ustawienia nie można zmienić po rozpoczęciu gry.", + "setPassword": "Ustaw hasło", + "passwordPlaceholder": "Hasło do gry", + "back": "Wstecz", + "hostP2PGame": "Hostuj grę P2P", + "hostGame": "Hostuj grę" + }, + "page": { + "deckRejected": "Talia została odrzucona przez gospodarza.", + "selectDeckFirst": "Wybierz talię przed kontynuowaniem.", + "couldNotLoadDeck": "Nie udało się wczytać talii. Spróbuj zaimportować ją ponownie.", + "deckNotLegal": "Talia nie jest dozwolona w formacie {{format}}.", + "deckCheckFailed": "Sprawdzanie talii nie powiodło się: {{error}}", + "deckCheckFailedGeneric": "Sprawdzanie talii nie powiodło się.", + "failedToJoinDraft": "Nie udało się dołączyć do grupy draftu.", + "passwordPrompt": "Ten pokój wymaga hasła:", + "joinErrorOutOfDateTitle": "Klient nieaktualny", + "joinErrorRefresh": "Odśwież", + "joinErrorCantJoinTitle": "Nie można dołączyć do tego pokoju", + "titleLobby": "Dołącz do stołu lub załóż własny.", + "titleHostSetup": "Skonfiguruj swój stół.", + "titleDraftLobby": "Grupa draftu", + "titleDeckSelect": "Wybierz talię.", + "descriptionLobby": "Przeglądaj dostępne stoły, dołączaj za pomocą kodu lub załóż nowy mecz.", + "descriptionHostSetup": "Dostosuj format, prywatność i czas przed otwarciem pokoju.", + "descriptionDraftLobby": "Oczekiwanie na graczy dołączających do grupy draftu.", + "descriptionDeckSelectFormat": "Wybierz talię dla formatu {{format}}.", + "descriptionDeckSelect": "Wybierz talię, którą chcesz wystawić online.", + "eyebrow": "Rozgrywka sieciowa", + "activeDeck": "Aktywna talia", + "edit": "Edytuj", + "change": "Zmień", + "noDeckWarning": "Nie wybrano talii — musisz wybrać jedną przed hostowaniem.", + "pickDeck": "Wybierz talię", + "anonymous": "Anonimowy", + "joining": "Dołączanie" + }, + "draftLobbyPanel": { + "draftPod": "Grupa draftu", + "connecting": "Łączenie z grupą draftu...", + "connectionFailed": "Połączenie nie powiodło się.", + "playersJoined": "Dołączyło {{joined}}/{{total}} graczy", + "seat": "Miejsce {{number}}", + "draftInProgress": "Trwa draft. Widok draftu otworzy się automatycznie.", + "leaveDraft": "Opuść draft" + }, + "deckLegalityChip": { + "checking": "Sprawdzanie talii pod kątem formatu {{format}}…", + "checkingLegality": "Sprawdzanie zgodności talii z zasadami…", + "legal": "✓ Dozwolona w formacie {{format}}", + "notLegal": "Niedozwolona w formacie {{format}}" + } +} diff --git a/client/src/i18n/locales/pl/settings.json b/client/src/i18n/locales/pl/settings.json new file mode 100644 index 0000000000..f7d10727ee --- /dev/null +++ b/client/src/i18n/locales/pl/settings.json @@ -0,0 +1,154 @@ +{ + "modal": { + "title": "Ustawienia", + "subtitle": "Dostosuj rozgrywkę, grafikę, dźwięk i domyślne ustawienia trybu wieloosobowego." + }, + "tabs": { + "gameplay": "Rozgrywka", + "visual": "Grafika", + "combat": "Tempo", + "audio": "Dźwięk", + "multiplayer": "Tryb wieloosobowy", + "data": "Dane", + "experimental": "Eksperymentalne" + }, + "gameplay": { + "title": "Rozgrywka", + "language": "Język", + "cardSize": "Rozmiar kart", + "logDefault": "Domyślny dziennik", + "spellPayment": "Płatność za czary", + "manualManaPayment": "Ręczna płatność many za czary", + "boardBackground": "Tło pola bitwy", + "boardBackgroundGroups": { + "automatic": "Automatyczne", + "battlefields": "Pola bitwy", + "plain": "Jednolite", + "custom": "Niestandardowe", + "off": "Wyłączone" + }, + "boardBackgroundOptions": { + "autoMatchDeck": "Auto (dopasuj do talii)", + "random": "Losowe", + "customUrl": "Niestandardowy URL", + "none": "Brak" + }, + "cardSizeOptions": { + "small": "Małe", + "medium": "Średnie", + "large": "Duże" + }, + "logDefaultOptions": { + "open": "Otwarty", + "closed": "Zamknięty" + } + }, + "visual": { + "title": "Grafika", + "vfxQuality": "Jakość efektów wizualnych", + "keywordStrip": "Pasek słów kluczowych", + "showKeywords": "Pokaż słowa kluczowe na kartach na polu bitwy", + "opponentHoverPreview": "Podgląd po najechaniu na przeciwnika", + "showOpponentBoard": "Pokaż pole bitwy przeciwnika po najechaniu na HUD", + "cardArtPreferences": "Preferencje grafiki kart", + "clearArtOverrides_one": "Wyczyść wszystkie nadpisania grafiki ({{count}})", + "clearArtOverrides_other": "Wyczyść wszystkie nadpisania grafiki ({{count}})", + "clearArtOverridesConfirm_one": "Wyczyścić {{count}} nadpisanie grafiki?", + "clearArtOverridesConfirm_other": "Wyczyścić wszystkie nadpisania grafiki ({{count}})?", + "vfxQualityOptions": { + "full": "Pełna", + "reduced": "Zredukowana", + "minimal": "Minimalna" + } + }, + "audio": { + "title": "Dźwięk", + "muteAll": "Wycisz wszystko", + "muteAllAudio": "Wycisz cały dźwięk", + "globalVolume": "Głośność ogólna", + "sfxVolume": "Głośność efektów", + "musicVolume": "Głośność muzyki" + }, + "audioTheme": { + "title": "Motyw dźwiękowy", + "theme": "Motyw", + "importTheme": "Importuj motyw", + "loading": "Ładowanie...", + "import": "Importuj", + "importFailed": "Nie udało się zaimportować motywu", + "customThemes": "Niestandardowe motywy", + "remove": "Usuń" + }, + "multiplayer": { + "title": "Tryb wieloosobowy", + "displayName": "Nazwa wyświetlana", + "displayNamePlaceholder": "Wprowadź swoją nazwę", + "serverSelectionNote": "Wybór serwera został przeniesiony do poczekalni — otwórz Tryb wieloosobowy i użyj plakietki serwera (lub „Wybierz serwer” w trybie P2P), aby zmieniać regiony, skonfigurować własną instancję lub przetestować połączenie." + }, + "experimental": { + "title": "Eksperymentalne", + "description": "Te funkcje są wciąż w fazie rozwoju. Mogą być niekompletne, zawierać błędy lub zmieniać się bez ostrzeżenia. Włącz je, aby wypróbować nowości wcześniej.", + "draftExperiments": "Eksperymenty draftowe", + "enableDraftFeatures": "Włącz eksperymentalne funkcje draftu", + "draftFeaturesDescription": "Odblokowuje Cube Draft i Pod Draft. Szybki draft przeciwko AI jest zawsze dostępny." + }, + "data": { + "title": "Kopia zapasowa i przywracanie", + "description": "Eksport pakuje twoje preferencje, zaimportowane talie i subskrypcje kanałów do jednego pliku JSON. Import przywraca je na innym komputerze. Pamięci podręczne IndexedDB (pamięć kanałów, pamięć dźwięku, zapisane gry) nie są dołączane — odbudowują się automatycznie.", + "exportBackup": "Eksportuj kopię zapasową…", + "importBackup": "Importuj kopię zapasową…", + "importConfirm": "Nadpisać istniejące preferencje i talie?\n\nOK: zastąp wszystko kopią zapasową (destrukcyjne).\nAnuluj: scal — zachowaj istniejące talie, dodaj nowe z kopii zapasowej.", + "backupDownloaded": "Kopia zapasowa pobrana.", + "imported_one": "Zaimportowano {{count}} talię.", + "imported_other": "Zaimportowano {{count}} talii.", + "importedWithPreferences_one": "Zaimportowano {{count}} talię i preferencje.", + "importedWithPreferences_other": "Zaimportowano {{count}} talii i preferencje.", + "skippedMalformed_one": "Pominięto {{count}} nieprawidłowy wpis.", + "skippedMalformed_other": "Pominięto {{count}} nieprawidłowych wpisów." + }, + "resetAll": { + "confirm": "Zresetować wszystkie preferencje do wartości domyślnych? Spowoduje to wyczyszczenie każdego ustawienia w tym oknie.", + "button": "Zresetuj wszystkie preferencje" + }, + "pacing": { + "title": "Tempo", + "resetSection": "Zresetuj sekcję", + "animationSpeed": "Szybkość animacji", + "animationSpeedDescription": "Główna szybkość — wyższa jest szybsza. Skrajnie w prawo całkowicie pomija animacje.", + "hint": "Suwaki poszczególnych kategorii mnożą się na podstawie Szybkości animacji. Kliknij dwukrotnie dowolny suwak — lub dotknij obok niego — aby zresetować.", + "instant": "Natychmiast", + "slowest": "Najwolniej", + "resetSliderLabel": "Zresetuj {{label}} do wartości domyślnej", + "atDefault": "Wartość domyślna", + "resetToDefault": "Zresetuj do wartości domyślnej", + "labels": { + "effects": "Tempo efektów", + "combat": "Tempo walki", + "banners": "Tempo banerów" + }, + "descriptions": { + "effects": "Rzucanie czarów, zmiany stref, śmierci, zmiany życia, znaczniki, obracanie/odwracanie.", + "combat": "Czas obrażeń bojowych — jak długo blokujący i atakujący pozostają, zanim obrażenia zostaną rozpatrzone.", + "banners": "Wyświetlanie banera na początku tury." + } + }, + "artChain": { + "emptyState": "Używanie domyślnej grafiki Scryfall. Dodaj reguły poniżej, aby dostosować.", + "moveUp": "Przesuń w górę", + "moveDown": "Przesuń w dół", + "remove": "Usuń", + "unreachable": "Reguły poniżej „{{rule}}” są nieosiągalne — zawsze pasuje.", + "addRule": "Dodaj regułę", + "setInputPlaceholder": "Kod lub nazwa zestawu…", + "addSet": "Dodaj zestaw", + "rulesPriorityNote": "Reguły są sprawdzane od góry do dołu. Wygrywa pierwsze dopasowanie. „Wydanie źródłowe” używa zestawu z paczki draftowej lub importu talii, gdy jest dostępny. Nadpisania dla poszczególnych kart (prawy przycisk myszy w kreatorze talii) zawsze mają priorytet.", + "setEntry": "Zestaw: {{name}} ({{code}})", + "rules": { + "sourcePrinting": "Wydanie źródłowe", + "newest": "Najnowsze wydanie", + "oldest": "Najstarsze wydanie", + "preferBorderless": "Preferuj bez ramki", + "preferExtended": "Preferuj grafikę rozszerzoną" + } + } +} diff --git a/client/src/i18n/locales/pt/common.json b/client/src/i18n/locales/pt/common.json index d53de92bd6..da6511555a 100644 --- a/client/src/i18n/locales/pt/common.json +++ b/client/src/i18n/locales/pt/common.json @@ -198,5 +198,11 @@ }, "turnBanner": { "turn": "Turno {{number}}" + }, + "chrome": { + "back": "Voltar", + "settings": "Configurações", + "languageSettings": "Idioma ({{lang}}) — abrir configurações", + "languageTitle": "Idioma: {{lang}}" } } diff --git a/client/src/i18n/locales/pt/draft.json b/client/src/i18n/locales/pt/draft.json index cbb1dcf879..99038d5460 100644 --- a/client/src/i18n/locales/pt/draft.json +++ b/client/src/i18n/locales/pt/draft.json @@ -39,7 +39,9 @@ "overrideResult": "Substituir Resultado", "versusPair": "{{a}} v {{b}}", "kickReplace": "Expulsar + Substituir", - "replaceWithBot": "Substituir {{name}} por Bot" + "replaceWithBot": "Substituir {{name}} por Bot", + "endDraft": "Encerrar Draft", + "endDraftConfirm": "Encerrar este draft para todos? Isso nao pode ser desfeito." }, "manaCurve": { "title": "Curva de Mana" @@ -60,7 +62,8 @@ "botDifficulty": "Dificuldade dos Bots", "chooseSet": "Escolha uma Coleção", "noPools": "Nenhum pool de draft disponível. Execute primeiro o pipeline de dados do draft.", - "setIconAlt": "Ícone da coleção {{name}}" + "setIconAlt": "Ícone da coleção {{name}}", + "loadFailed": "Falha ao carregar as coleções" }, "pack": { "confirmPick": "Confirmar Escolha", diff --git a/client/src/i18n/locales/pt/game.json b/client/src/i18n/locales/pt/game.json index 47e026ba0d..9f8daa9d59 100644 --- a/client/src/i18n/locales/pt/game.json +++ b/client/src/i18n/locales/pt/game.json @@ -8,6 +8,8 @@ "preview": { "holdCtrlBack": "Segure Ctrl para a face traseira", "holdCtrlFront": "Segure Ctrl para a face dianteira", + "holdCtrlFlip": "Segure Ctrl para virar", + "flip": "Virar", "altParsedAbilities": "Alt: habilidades processadas", "debugId": "ID: {{id}}", "engineParse": "Análise do Motor", @@ -307,7 +309,9 @@ "statHand": "Mão", "statCreatures": "Criaturas", "statLands": "Terrenos", - "statOther": "Outros" + "statOther": "Outros", + "compactHud": "HUD de oponente compacto. Reduz cada oponente a uma única linha fina (nome + vida); toque em um oponente para focar todo o campo de batalha dele.", + "expandHud": "Expandir HUD de oponente. Mostra a mão e o detalhamento do campo de batalha de cada oponente." }, "combat": { "attackAll": "Atacar Todos", @@ -380,7 +384,9 @@ "nounTarget": "alvo", "confirmTap": "Confirmar Virar ({{selected}}/{{count}})", "keepCurrentTargets": "Manter Alvos Atuais", - "skip": "Pular" + "skip": "Pular", + "chooseNewTarget": "Escolha o novo alvo da mágica", + "chooseNewTargetForSpell": "Escolha um novo alvo para {{spell}}" }, "log": { "title": "Registro do Jogo", @@ -1247,6 +1253,9 @@ "proliferateSubtitle": "Escolha qualquer número de permanentes e jogadores com marcadores. Cada alvo escolhido recebe mais um marcador de cada tipo já presente.", "chooseObjectsTitle": "Escolher Permanentes", "chooseObjectsSubtitle": "Escolha qualquer número de permanentes. Você paga um custo por cada um escolhido.", + "selectAll": "Todos", + "selectNone": "Nenhum", + "selectMine": "Meu lado", "confirm": "Confirmar" }, "replacement": { @@ -1260,7 +1269,8 @@ "scopeSingle": "Escolha um novo alvo para a mágica", "scopeMulti": "Escolha novos alvos para a mágica", "subtitle": "{{scope}}. Atual: {{current}}", - "confirm": "Confirmar" + "confirm": "Confirmar", + "badgeNewTarget": "Novo alvo" }, "separatePiles": { "chooserName": "Jogador {{number}}", @@ -1321,5 +1331,27 @@ "subtitleLabel": "Escolha um rótulo para {{name}}", "subtitleVoteRemaining": "Dê um voto ({{count}} restantes)", "subtitleVote": "Dê seu voto" + }, + "advance": { + "resolve": "Resolver", + "passPriority": "Passar prioridade", + "toPhase": "Para {{phase}}" + }, + "outsideGame": { + "fromExile": "Do exílio", + "fromSideboard": "Do sideboard" + }, + "phaseName": { + "Upkeep": "Manutenção", + "Draw": "Compra", + "PreCombatMain": "Fase principal 1", + "BeginCombat": "Início do combate", + "DeclareAttackers": "Declarar atacantes", + "DeclareBlockers": "Declarar bloqueadores", + "CombatDamage": "Dano de combate", + "EndCombat": "Fim do combate", + "PostCombatMain": "Fase principal 2", + "End": "Etapa final", + "Cleanup": "Limpeza" } } diff --git a/client/src/i18n/locales/pt/menu.json b/client/src/i18n/locales/pt/menu.json index f4f5fd4391..59c29c3219 100644 --- a/client/src/i18n/locales/pt/menu.json +++ b/client/src/i18n/locales/pt/menu.json @@ -4,7 +4,14 @@ }, "aiDifficulty": { "label": "Dificuldade da IA", - "ariaLabel": "Dificuldade da IA: {{difficulty}}" + "ariaLabel": "Dificuldade da IA: {{difficulty}}", + "levels": { + "VeryEasy": "Muito fácil", + "Easy": "Fácil", + "Medium": "Médio", + "Hard": "Difícil", + "VeryHard": "Muito difícil" + } }, "bracketFilter": { "label": "Filtro de bracket" @@ -205,7 +212,8 @@ "browseAll": "Explorar Todos", "loadMore": "Carregar Mais", "selectedDeck": "Deck selecionado", - "chooseDeckToContinue": "Escolha um deck para continuar" + "chooseDeckToContinue": "Escolha um deck para continuar", + "filterAll": "Todos" }, "subscriptions": { "emptyTitle": "Nenhuma inscrição em feed", @@ -213,5 +221,13 @@ "feedMeta_one": "· {{count}} deck · Atualizado em {{date}}", "feedMeta_other": "· {{count}} decks · Atualizado em {{date}}", "error": "Erro: {{error}}" + }, + "myDecksPage": { + "eyebrow": "Decks", + "title": "Decks.", + "description": "Abra uma lista salva, importe uma nova ou continue no construtor de decks." + }, + "loadGameState": { + "readFailed": "Falha ao ler o arquivo" } } diff --git a/client/src/i18n/locales/pt/settings.json b/client/src/i18n/locales/pt/settings.json index d7e2e9bd00..422392eb62 100644 --- a/client/src/i18n/locales/pt/settings.json +++ b/client/src/i18n/locales/pt/settings.json @@ -32,6 +32,15 @@ "random": "Aleatório", "customUrl": "URL personalizada", "none": "Nenhum" + }, + "cardSizeOptions": { + "small": "Pequeno", + "medium": "Médio", + "large": "Grande" + }, + "logDefaultOptions": { + "open": "Aberto", + "closed": "Fechado" } }, "visual": { @@ -45,7 +54,12 @@ "clearArtOverrides_one": "Limpar Todas as Substituições de Arte ({{count}})", "clearArtOverrides_other": "Limpar Todas as Substituições de Arte ({{count}})", "clearArtOverridesConfirm_one": "Limpar todas as {{count}} substituição de arte?", - "clearArtOverridesConfirm_other": "Limpar todas as {{count}} substituições de arte?" + "clearArtOverridesConfirm_other": "Limpar todas as {{count}} substituições de arte?", + "vfxQualityOptions": { + "full": "Completa", + "reduced": "Reduzida", + "minimal": "Mínima" + } }, "audio": { "title": "Áudio", @@ -106,7 +120,17 @@ "slowest": "Mais lento", "resetSliderLabel": "Redefinir {{label}} para o padrão", "atDefault": "No padrão", - "resetToDefault": "Redefinir para o padrão" + "resetToDefault": "Redefinir para o padrão", + "labels": { + "effects": "Ritmo dos efeitos", + "combat": "Ritmo de combate", + "banners": "Ritmo dos banners" + }, + "descriptions": { + "effects": "Conjuração de mágicas, mudanças de zona, mortes, mudanças de vida, marcadores, virar/desvirar.", + "combat": "Tempo do dano de combate — quanto tempo bloqueadores e atacantes permanecem antes de o dano ser resolvido.", + "banners": "Exibição do banner de início de turno." + } }, "artChain": { "emptyState": "Usando a arte padrão do Scryfall. Adicione regras abaixo para personalizar.", diff --git a/client/src/i18n/resources.test.ts b/client/src/i18n/resources.test.ts index 12873b9ad6..6bc84a208c 100644 --- a/client/src/i18n/resources.test.ts +++ b/client/src/i18n/resources.test.ts @@ -1,7 +1,53 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + import { describe, expect, it } from "vitest"; import { detectInitialLanguage, resources, SUPPORTED_LNGS } from "./resources"; +const LOCALES_DIR = join(dirname(fileURLToPath(import.meta.url)), "locales"); + +/** Every `/.json` catalog on disk, as absolute paths. Reads the dir + * tree directly (not the Vite glob) so encoding checks see raw bytes. */ +function localeCatalogFiles(): string[] { + return readdirSync(LOCALES_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .flatMap((dir) => + readdirSync(join(LOCALES_DIR, dir.name)) + .filter((file) => file.endsWith(".json")) + .map((file) => join(LOCALES_DIR, dir.name, file)), + ); +} + +/** Collect every leaf key path in a namespace tree, prefixed with the namespace + * (`game.modeChoice.confirm`). Recurses into nested objects; treats strings (and + * any non-object value) as leaves. */ +function flattenLeafKeys( + tree: Record, + prefix: string, + out: Set, +): void { + for (const [key, value] of Object.entries(tree)) { + const path = prefix ? `${prefix}.${key}` : key; + if (value !== null && typeof value === "object" && !Array.isArray(value)) { + flattenLeafKeys(value as Record, path, out); + } else { + out.add(path); + } + } +} + +/** The full namespace-prefixed leaf-key set for one locale, across every catalog + * the glob discovered for it. */ +function localeKeySet(lng: string): Set { + const keys = new Set(); + for (const [ns, tree] of Object.entries(resources[lng] ?? {})) { + flattenLeafKeys(tree as Record, ns, keys); + } + return keys; +} + // Gate test (plan §9 Phase 0 step 1): proves Vite's import.meta.glob runs under // vitest's transform pipeline AND that the reshape yields { lng: { ns: {...} } }. // Both the runtime catalogs and the "don't mock t, keep getByText" test strategy @@ -23,3 +69,58 @@ describe("i18n resources", () => { expect(SUPPORTED_LNGS as readonly string[]).toContain(detectInitialLanguage()); }); }); + +// Key-parity gate: `en` is the typing oracle, so every other shipped locale must +// carry the exact same namespace-prefixed leaf keys — no missing translations and +// no orphaned keys. A namespace-prefixed set comparison catches both a single +// dropped leaf and a wholesale missing/extra catalog file in one diff. Strict +// equality includes plural suffixes (`_one`/`_other`); the catalogs mirror en's +// structure, so a new CLDR plural category surfacing here is a deliberate review +// signal, not a false failure. +describe("i18n locale key parity", () => { + const enKeys = localeKeySet("en"); + + it("en (the oracle) has a non-empty key set", () => { + expect(enKeys.size).toBeGreaterThan(0); + }); + + for (const lng of SUPPORTED_LNGS) { + if (lng === "en") continue; + it(`${lng} has exactly the same keys as en`, () => { + const localeKeys = localeKeySet(lng); + const missing = [...enKeys].filter((k) => !localeKeys.has(k)).sort(); + const extra = [...localeKeys].filter((k) => !enKeys.has(k)).sort(); + // toEqual surfaces the offending keys directly in the failure diff. + expect({ missing, extra }).toEqual({ missing: [], extra: [] }); + }); + } +}); + +// Encoding gate: catalogs use literal UTF-8 characters (not `\uXXXX` escapes) so +// translations stay human-readable and reviewable. The cost of literals is +// encoding drift — a file saved as Latin-1, or mojibake pasted in — so enforce +// that every catalog is valid, BOM-free UTF-8. This reads raw bytes; the parsed +// `resources` glob cannot see encoding because Vite already decoded it. +describe("i18n locale file encoding", () => { + const files = localeCatalogFiles(); + + it("discovers catalog files to validate", () => { + expect(files.length).toBeGreaterThan(0); + }); + + for (const file of files) { + const rel = file.slice(LOCALES_DIR.length + 1); + it(`${rel} is valid, BOM-free UTF-8`, () => { + const bytes = readFileSync(file); + // A UTF-8 BOM (EF BB BF) is valid UTF-8 but trips some JSON tooling. + expect([...bytes.subarray(0, 3)]).not.toEqual([0xef, 0xbb, 0xbf]); + // fatal:true throws on any malformed UTF-8 byte sequence. + expect(() => + new TextDecoder("utf-8", { fatal: true }).decode(bytes), + ).not.toThrow(); + // A baked-in replacement char (U+FFFD) signals earlier corruption. + const replacementChar = String.fromCharCode(0xfffd); + expect(new TextDecoder("utf-8").decode(bytes)).not.toContain(replacementChar); + }); + } +}); diff --git a/client/src/i18n/resources.ts b/client/src/i18n/resources.ts index d66a4e2dcd..5c46479ed1 100644 --- a/client/src/i18n/resources.ts +++ b/client/src/i18n/resources.ts @@ -9,7 +9,7 @@ const modules = import.meta.glob("./locales/*/*.json", { /** Languages the app ships chrome catalogs for. English is the typing oracle and * the `fallbackLng`; the others may lag without breaking the build. */ -export const SUPPORTED_LNGS = ["en", "es", "fr", "de", "it", "pt"] as const; +export const SUPPORTED_LNGS = ["en", "es", "fr", "de", "it", "pt", "pl"] as const; export type SupportedLng = (typeof SUPPORTED_LNGS)[number]; /** `{ en: { common: {...}, ... }, es: {...}, ... }` reshaped from the flat glob diff --git a/client/src/pages/DraftPodPage.tsx b/client/src/pages/DraftPodPage.tsx index c70e73eb5a..c8cdd17add 100644 --- a/client/src/pages/DraftPodPage.tsx +++ b/client/src/pages/DraftPodPage.tsx @@ -374,6 +374,7 @@ function MatchInProgressView() { ? matchPairing.botName : matchPairing.opponentName : null; + const isBotMatch = matchPairing?.type === "Bot"; const isHost = matchPairing?.type === "HumanHost"; return ( @@ -387,18 +388,24 @@ function MatchInProgressView() {
{t("podPhaseView.versusOpponent", { name: opponentName })}
-
- {isHost - ? t("podPhaseView.youAreHosting") - : t("podPhaseView.connectingOpponent")} -
+ {!isBotMatch && ( +
+ {isHost + ? t("podPhaseView.youAreHosting") + : t("podPhaseView.connectingOpponent")} +
+ )} @@ -692,14 +699,6 @@ export function DraftPodPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); - // Cleanup on unmount - useEffect(() => { - return () => { - void leave(true); - resetPod(); - }; - }, [leave, resetPod]); - useEffect(() => { if (searchParams.get("resume") !== "1") return; void resumeHostedPod(); @@ -715,7 +714,7 @@ export function DraftPodPage() { return (
- navigate("/") : undefined} /> +
{phaseContent(phase, handleLeave)} diff --git a/client/src/pages/GamePage.tsx b/client/src/pages/GamePage.tsx index 5251b13048..9cd9d5e831 100644 --- a/client/src/pages/GamePage.tsx +++ b/client/src/pages/GamePage.tsx @@ -109,6 +109,7 @@ import { useMultiplayerStore, type PlayerSlot, } from "../stores/multiplayerStore.ts"; +import { useMultiplayerDraftStore } from "../stores/multiplayerDraftStore.ts"; import { GameProvider } from "../providers/GameProvider.tsx"; import { useCanActForWaitingState, usePerspectivePlayerId, usePlayerId } from "../hooks/usePlayerId.ts"; import { abilityChoiceLabel, formatAbilityCost } from "../viewmodel/costLabel.ts"; @@ -2057,6 +2058,7 @@ function GameOverScreen({ const source = searchParams.get("source"); const draftId = searchParams.get("draftId"); const isDraft = source === "draft" && !!draftId; + const isDraftPodMatch = mode === "draft-match"; const gameId = useGameStore((s) => s.gameId); const [resultRecorded, setResultRecorded] = useState(false); @@ -2072,6 +2074,17 @@ function GameOverScreen({ }); }, [isDraft, gameId, isDraw, isVictory, resultRecorded]); + useEffect(() => { + if (!isDraftPodMatch || resultRecorded) return; + void useMultiplayerDraftStore + .getState() + .reportActiveMatchGameResult(winner) + .then(() => setResultRecorded(true)) + .catch((err) => { + console.error("[GameOverScreen] failed to report draft pod match result:", err); + }); + }, [isDraftPodMatch, resultRecorded, winner]); + const handleRematch = () => { const newId = crypto.randomUUID(); // Preserve the original launch configuration (format, players, match, @@ -2175,6 +2188,19 @@ function GameOverScreen({ ? t("gamePage.gameOver.backToDraft") : t("gamePage.gameOver.continueRun")} + ) : isDraftPodMatch ? ( + ) : isOnlineMode ? (