Skip to content

Flexible mana lands render as unlabeled "Tap for mana" — label projection uses the wrong selection authority #6944

Description

@matthewevans

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 GarenbrigManaProduction::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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions