diff --git a/client/src/viewmodel/__tests__/dominantColor.test.ts b/client/src/viewmodel/__tests__/dominantColor.test.ts index 7d26c5397d..a9112c3a3c 100644 --- a/client/src/viewmodel/__tests__/dominantColor.test.ts +++ b/client/src/viewmodel/__tests__/dominantColor.test.ts @@ -98,18 +98,20 @@ describe("getDominantManaColor", () => { }); it("counts mana cost shards of non-land permanents", () => { + // The engine serializes shards as Rust variant names (e.g. "White", "Red"), + // not MTG symbols ("W", "R") — mirror the real payload here. const objects: Record = { "1": makeGameObject({ id: 1, name: "Serra Angel", card_types: { supertypes: [], core_types: ["Creature"], subtypes: ["Angel"] }, - mana_cost: { type: "Cost", shards: ["W", "W"], generic: 3 }, + mana_cost: { type: "Cost", shards: ["White", "White"], generic: 3 }, }), "2": makeGameObject({ id: 2, name: "Lightning Bolt", card_types: { supertypes: [], core_types: ["Creature"], subtypes: [] }, - mana_cost: { type: "Cost", shards: ["R"], generic: 0 }, + mana_cost: { type: "Cost", shards: ["Red"], generic: 0 }, }), }; @@ -118,21 +120,46 @@ describe("getDominantManaColor", () => { expect(result).toBe("White"); }); + it("counts both halves of a hybrid shard variant", () => { + // "WhiteBlue" (the {W/U} hybrid variant name) must contribute to both + // White and Blue — SHARD_ABBREVIATION maps it to "W/U", split on "/". + const objects: Record = { + "1": makeGameObject({ + id: 1, + name: "Hybrid Creature", + card_types: { supertypes: [], core_types: ["Creature"], subtypes: [] }, + mana_cost: { type: "Cost", shards: ["WhiteBlue"], generic: 0 }, + }), + "2": makeGameObject({ + id: 2, + name: "Blue Creature", + card_types: { supertypes: [], core_types: ["Creature"], subtypes: [] }, + mana_cost: { type: "Cost", shards: ["Blue"], generic: 0 }, + }), + }; + + // White=1 (from the hybrid), Blue=2 (hybrid + mono-blue) → Blue wins. + const result = getDominantManaColor([1, 2], objects, 0); + + expect(result).toBe("Blue"); + }); + it("combines land subtypes and spell mana costs", () => { const objects: Record = { "1": makeGameObject({ id: 1, card_types: { supertypes: ["Basic"], core_types: ["Land"], subtypes: ["Island"] } }), - "2": makeGameObject({ id: 2, card_types: { supertypes: ["Basic"], core_types: ["Land"], subtypes: ["Island"] } }), - "3": makeGameObject({ - id: 3, + "2": makeGameObject({ + id: 2, name: "Red Creature", card_types: { supertypes: [], core_types: ["Creature"], subtypes: [] }, - mana_cost: { type: "Cost", shards: ["R"], generic: 1 }, + mana_cost: { type: "Cost", shards: ["Red", "Red"], generic: 1 }, }), }; - const result = getDominantManaColor([1, 2, 3], objects, 0); + // Island=Blue 1, creature=Red 2 → Red wins (would be Blue if spell shards + // were silently dropped, as with the pre-fix symbol-keyed lookup). + const result = getDominantManaColor([1, 2], objects, 0); - expect(result).toBe("Blue"); + expect(result).toBe("Red"); }); }); diff --git a/client/src/viewmodel/dominantColor.ts b/client/src/viewmodel/dominantColor.ts index ae3688657e..98875bb350 100644 --- a/client/src/viewmodel/dominantColor.ts +++ b/client/src/viewmodel/dominantColor.ts @@ -1,4 +1,5 @@ import type { GameObject, ManaColor, PlayerId } from "../adapter/types"; +import { SHARD_ABBREVIATION } from "./costLabel"; const LAND_SUBTYPE_TO_COLOR: Record = { Plains: "White", @@ -8,7 +9,10 @@ const LAND_SUBTYPE_TO_COLOR: Record = { Forest: "Green", }; -const SHARD_TO_COLOR: Record = { +// Keyed by MTG symbol halves ("W", "U", …) — the components produced by +// splitting a shard's `SHARD_ABBREVIATION` on "/". Colorless/Snow/X/Phyrexian +// halves have no entry and are ignored, so only true color pips are counted. +const SYMBOL_TO_COLOR: Record = { W: "White", U: "Blue", B: "Black", @@ -35,11 +39,15 @@ function countColors( if (color) colorCounts.set(color, (colorCounts.get(color) ?? 0) + 1); } } else if (obj.mana_cost.type === "Cost") { - // Non-land permanents: count colored mana shards + // Non-land permanents: count colored mana shards. The engine serializes + // shards as Rust variant names ("White", "WhiteBlue", "PhyrexianRed"), + // not MTG symbols — SHARD_ABBREVIATION is the canonical bridge to symbols + // ("W", "W/U", "R/P"). Split hybrid/Phyrexian shards on "/" and count each + // colored half. for (const shard of obj.mana_cost.shards) { - // Handle hybrid shards like "W/U" — count both halves - for (const part of shard.split("/")) { - const color = SHARD_TO_COLOR[part]; + const abbreviation = SHARD_ABBREVIATION[shard] ?? shard; + for (const part of abbreviation.split("/")) { + const color = SYMBOL_TO_COLOR[part]; if (color) colorCounts.set(color, (colorCounts.get(color) ?? 0) + 1); } }