Summary
Every flexible mana source on a land renders as an unlabeled generic "Tap for mana" button.
With Resonating Lute granting the ability to seven lands, the player sees seven identical buttons
and cannot tell which produces which color.
Reporter: "Resonating Lute is allowing me to add mana, but I am just kind of guessing which color
is which..."
The buttons do work — each taps for a different, specific color. The player simply cannot see
which. This is a presentation defect; no wrong game state results.
The parse is correct — rule out the parser
jq '.["resonating lute"]' client/public/card-data.json shows the grant, AnyOneColor
production, count: Fixed(2), all five color_options, and the instant/sorcery spend
restriction all present and correct. Oracle text verified against Scryfall.
Root cause — split authority between action minting and label projection
Two functions build a ManaSourceSelection from the same ManaSourceOption, and they disagree.
Actions are minted with the raw per-color selection.
crates/engine/src/game/mana_sources.rs:410-423 pushes one GameAction::TapLandForMana per
option row via semantic_selection (:347-369), which stamps the concrete color. AnyOneColor
emits one row per color (the catch-all at :2824-2831), so five distinct actions.
Label projection requires the collapsed selection.
crates/engine/src/game/interaction.rs:4223-4227 — the sole engine site emitting
InteractionRoleCode::ProducedMana — resolves through
live_mana_source_option_for_selection (mana_sources.rs:606-617), which gates on
activatable_mana_source_selections(...).contains(selection). That list is built with
manual_selection_for_option (:563-600), which collapses flexible producers:
if flexible_output {
selection.mana_type = ManaType::Colorless;
selection.output = ManaSourceOutput::DeferredColorChoice;
}
ManaSourceSelection derives PartialEq over all fields (types/mana.rs:701-711):
|
mana_type |
output |
Action carries (semantic_selection) |
White |
Concrete(White) |
Gate list holds (manual_selection_for_option) |
Colorless |
DeferredColorChoice |
Never equal → .contains() false → Err → the silent else { return } at interaction.rs:4226
→ zero ProducedMana surfaces → the client falls back.
For a non-flexible land (basic Forest, Castle Garenbrig) flexible_output is false,
manual_selection_for_option returns semantic_selection unchanged, the gate matches, and the
label renders correctly. That is exactly why only flexible sources are affected.
The client is behaving correctly
"Tap for mana" has exactly one definition — client/src/i18n/locales/en/game.json:1059 — and one
call site, client/src/pages/GamePage.tsx:3020:
if (units.length === 0) return { label: t("gamePage.manaRestrictions.tapForMana") };
units are engine-supplied producedMana surfaces. When the engine supplies any, the client
renders the real label at GamePage.tsx:3037 (e.g. "Tap for {G}"). The fallback is purely for
"the engine sent zero surfaces". Per CLAUDE.md's display-layer rule the fix must not go here.
Fix — one call site, not the representations
Do not "make the two selections agree." The divergence is deliberate and documented at
mana_sources.rs:591-596: "The planner emits one concrete row per color, but a manual activation
must retain the source capability and let the normal mana-choice resolver ask for its color."
The defect is that project_action_payload resolves a planner-minted TapLandForMana through
the manual authority. Execution already picks correctly — game/engine.rs:10875 uses
live_land_mana_option_for_selection, which matches semantic_selection. The label path calls the
wrong one of two resolvers.
The silent else { return } should also be reconsidered: a projection that cannot resolve its own
action is a defect, and swallowing it is what let this ship.
Scope — class-level, not card-specific
flexible_output (mana_sources.rs:569-589) enumerates 8 ManaProduction variants; any land
using one is affected. Granting is irrelevant — layer-6 grants are materialized into
object.abilities at game/layers.rs:6812-6813, so both paths are identical.
Verified affected in card-data.json: City of Brass, Mana Confluence, Gemstone Mine
(plain lands, no grant involved — strongest evidence this is not about granting),
Chromatic Lantern (grants to lands), Resonating Lute.
Non-lands are unaffected: they mint as ActivateAbility (mana_sources.rs:426-458) and the client
only overrides the label for TapLandForMana (GamePage.tsx:3133), falling back to the ability's
own description.
Why tests missed it
crates/engine/tests/integration/interaction_contract.rs:1200-1250 asserts ProducedMana
surfaces, but only for Castle Garenbrig — ManaProduction::Fixed, which is not in the
flexible_output list, so the gate matches. No test covers a flexible (AnyOneColor) land.
Not a duplicate of #689
#689 (closed) was Resonating Lute's other ability — {T}: Draw a card activating without the
seven-card requirement. Different ability, different subsystem.
Summary
Every flexible mana source on a land renders as an unlabeled generic "Tap for mana" button.
With Resonating Lute granting the ability to seven lands, the player sees seven identical buttons
and cannot tell which produces which color.
Reporter: "Resonating Lute is allowing me to add mana, but I am just kind of guessing which color
is which..."
The buttons do work — each taps for a different, specific color. The player simply cannot see
which. This is a presentation defect; no wrong game state results.
The parse is correct — rule out the parser
jq '.["resonating lute"]' client/public/card-data.jsonshows the grant,AnyOneColorproduction,
count: Fixed(2), all fivecolor_options, and the instant/sorcery spendrestriction all present and correct. Oracle text verified against Scryfall.
Root cause — split authority between action minting and label projection
Two functions build a
ManaSourceSelectionfrom the sameManaSourceOption, and they disagree.Actions are minted with the raw per-color selection.
crates/engine/src/game/mana_sources.rs:410-423pushes oneGameAction::TapLandForManaperoption row via
semantic_selection(:347-369), which stamps the concrete color.AnyOneColoremits one row per color (the catch-all at
:2824-2831), so five distinct actions.Label projection requires the collapsed selection.
crates/engine/src/game/interaction.rs:4223-4227— the sole engine site emittingInteractionRoleCode::ProducedMana— resolves throughlive_mana_source_option_for_selection(mana_sources.rs:606-617), which gates onactivatable_mana_source_selections(...).contains(selection). That list is built withmanual_selection_for_option(:563-600), which collapses flexible producers:ManaSourceSelectionderivesPartialEqover all fields (types/mana.rs:701-711):mana_typeoutputsemantic_selection)WhiteConcrete(White)manual_selection_for_option)ColorlessDeferredColorChoiceNever equal →
.contains()false →Err→ the silentelse { return }atinteraction.rs:4226→ zero
ProducedManasurfaces → the client falls back.For a non-flexible land (basic Forest, Castle Garenbrig)
flexible_outputis false,manual_selection_for_optionreturnssemantic_selectionunchanged, the gate matches, and thelabel renders correctly. That is exactly why only flexible sources are affected.
The client is behaving correctly
"Tap for mana"has exactly one definition —client/src/i18n/locales/en/game.json:1059— and onecall site,
client/src/pages/GamePage.tsx:3020:unitsare engine-suppliedproducedManasurfaces. When the engine supplies any, the clientrenders the real label at
GamePage.tsx:3037(e.g. "Tap for {G}"). The fallback is purely for"the engine sent zero surfaces". Per CLAUDE.md's display-layer rule the fix must not go here.
Fix — one call site, not the representations
Do not "make the two selections agree." The divergence is deliberate and documented at
mana_sources.rs:591-596: "The planner emits one concrete row per color, but a manual activationmust retain the source capability and let the normal mana-choice resolver ask for its color."
The defect is that
project_action_payloadresolves a planner-mintedTapLandForManathroughthe manual authority. Execution already picks correctly —
game/engine.rs:10875useslive_land_mana_option_for_selection, which matchessemantic_selection. The label path calls thewrong one of two resolvers.
The silent
else { return }should also be reconsidered: a projection that cannot resolve its ownaction is a defect, and swallowing it is what let this ship.
Scope — class-level, not card-specific
flexible_output(mana_sources.rs:569-589) enumerates 8ManaProductionvariants; any landusing one is affected. Granting is irrelevant — layer-6 grants are materialized into
object.abilitiesatgame/layers.rs:6812-6813, so both paths are identical.Verified affected in
card-data.json: City of Brass, Mana Confluence, Gemstone Mine(plain lands, no grant involved — strongest evidence this is not about granting),
Chromatic Lantern (grants to lands), Resonating Lute.
Non-lands are unaffected: they mint as
ActivateAbility(mana_sources.rs:426-458) and the clientonly overrides the label for
TapLandForMana(GamePage.tsx:3133), falling back to the ability'sown description.
Why tests missed it
crates/engine/tests/integration/interaction_contract.rs:1200-1250assertsProducedManasurfaces, but only for Castle Garenbrig —
ManaProduction::Fixed, which is not in theflexible_outputlist, so the gate matches. No test covers a flexible (AnyOneColor) land.Not a duplicate of #689
#689 (closed) was Resonating Lute's other ability —
{T}: Draw a cardactivating without theseven-card requirement. Different ability, different subsystem.