Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions crates/engine/src/game/casting_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30955,6 +30955,178 @@ fn cant_be_activated_aura_blocks_enchanted_creature_not_others() {
);
}

#[test]
fn karn_blocks_liquimetal_coated_opponent_land() {
// Issue #6469: Karn's `TargetFilter::Typed(Artifact)` filter must apply to
// a permanent that becomes an artifact via a continuous type-changing
// effect (Liquimetal Coating's "becomes an artifact in addition to its
// other types until end of turn"), not just to permanents that are
// printed artifacts. Exercises the real GenericEffect -> transient
// continuous effect -> layer-4 AddType pipeline rather than hand-setting
// `card_types`, so a regression in that pipeline would be caught here too.
let mut state = setup_game_at_main_phase();

add_cant_be_activated_source(
&mut state,
PlayerId(0),
ProhibitionScope::AllPlayers,
TargetFilter::Typed(
TypedFilter::new(TypeFilter::Artifact).controller(ControllerRef::Opponent),
),
);

let coating = create_object(
&mut state,
CardId(0x1157),
PlayerId(0),
"Liquimetal Coating".to_string(),
Zone::Battlefield,
);
state
.objects
.get_mut(&coating)
.unwrap()
.card_types
.core_types
.push(CoreType::Artifact);

let land = create_object(
&mut state,
CardId(0x1a2d),
PlayerId(1),
"Utility Land".to_string(),
Zone::Battlefield,
);
{
let obj = state.objects.get_mut(&land).unwrap();
obj.card_types.core_types.push(CoreType::Land);
obj.base_card_types = obj.card_types.clone();
obj.entered_battlefield_turn = Some(0);
Arc::make_mut(&mut obj.abilities).push(
crate::types::ability::AbilityDefinition::new(
crate::types::ability::AbilityKind::Activated,
crate::types::ability::Effect::Draw {
count: QuantityExpr::Fixed { value: 1 },
target: TargetFilter::Controller,
},
)
.cost(AbilityCost::Tap),
);
}
let land_ability = state.objects[&land].abilities[0].clone();

assert!(
!is_blocked_by_cant_be_activated(&state, PlayerId(1), land, &land_ability),
"reach-guard: an uncoated land must not be blocked by Karn"
);

let ability = ResolvedAbility::new(
Effect::GenericEffect {
static_abilities: vec![StaticDefinition::new(StaticMode::Continuous)
.affected(TargetFilter::ParentTarget)
.modifications(vec![ContinuousModification::AddType {
core_type: CoreType::Artifact,
}])],
duration: Some(crate::types::ability::Duration::UntilEndOfTurn),
target: Some(TargetFilter::Typed(TypedFilter::new(TypeFilter::Permanent))),
end_cost: None,
},
vec![TargetRef::Object(land)],
coating,
PlayerId(0),
);

let mut events = Vec::new();
crate::game::effects::effect::resolve(&mut state, &ability, &mut events).unwrap();
crate::game::layers::evaluate_layers(&mut state);

assert!(
state.objects[&land]
.card_types
.core_types
.contains(&CoreType::Artifact),
"Liquimetal Coating must make the land an artifact; core_types = {:?}",
state.objects[&land].card_types.core_types
);
assert!(
is_blocked_by_cant_be_activated(&state, PlayerId(1), land, &land_ability),
"Karn must block a Coating-turned-artifact land controlled by an opponent"
);
}

#[test]
fn karn_blocks_liquimetal_coated_forest_from_legal_mana_actions() {
// Issue #6469: the ROOT CAUSE — `land_mana_options`'s basic-land-subtype
// fallback (`mana_sources.rs`) fired whenever `scan_mana_abilities` came
// back empty, without checking WHY it was empty. Karn correctly filters
// the coated Forest's real {T}: Add {G} ability out of `scan_mana_abilities`
// (confirmed by the reach-guard below), but the fallback then mistook that
// legitimate filtering for "no mana ability exists" and re-added an
// unconditional `ability_index: None` option for it, letting the opponent
// tap the Karn-blocked land for mana anyway. Exercises
// `activatable_mana_actions_for_player`, the real legal-action surface
// behind both the manual "tap for mana" UI and AI candidate generation.
let mut state = setup_game_at_main_phase();

add_cant_be_activated_source(
&mut state,
PlayerId(0),
ProhibitionScope::AllPlayers,
TargetFilter::Typed(
TypedFilter::new(TypeFilter::Artifact).controller(ControllerRef::Opponent),
),
);

// A real Forest: Land + "Forest" subtype + an explicit {T}: Add {G} ability
// (mirrors card-data.json's actual parsed Forest, not a bare subtype).
let forest = create_object(
&mut state,
CardId(0xF0125),
PlayerId(1),
"Forest".to_string(),
Zone::Battlefield,
);
{
let obj = state.objects.get_mut(&forest).unwrap();
obj.card_types.core_types.push(CoreType::Land);
obj.card_types.subtypes.push("Forest".to_string());
obj.card_types.core_types.push(CoreType::Artifact); // Liquimetal Coating
obj.base_card_types = obj.card_types.clone();
obj.entered_battlefield_turn = Some(0);
Arc::make_mut(&mut obj.abilities).push(
crate::types::ability::AbilityDefinition::new(
crate::types::ability::AbilityKind::Activated,
crate::types::ability::Effect::Mana {
produced: crate::types::ability::ManaProduction::Fixed {
colors: vec![ManaColor::Green],
contribution: ManaContribution::Base,
},
restrictions: vec![],
grants: vec![],
expiry: None,
target: None,
},
)
.cost(AbilityCost::Tap),
);
}
let forest_ability = state.objects[&forest].abilities[0].clone();

assert!(
is_blocked_by_cant_be_activated(&state, PlayerId(1), forest, &forest_ability),
"reach-guard: Karn must block the coated Forest's own {{T}}: Add {{G}} ability"
);

let legal_actions =
crate::game::mana_sources::activatable_mana_actions_for_player(&state, PlayerId(1));
assert!(
!legal_actions
.iter()
.any(|action| action.source_object() == Some(forest)),
"Karn must remove the coated Forest from P1's legal mana actions, got {legal_actions:?}"
);
}

// === CR 605.1a: Pithing Needle mana-ability exemption gate ===

/// Build a Llanowar-Elves-style mana ability: `{T}: Add {G}` (no targets, produces mana).
Expand Down
21 changes: 19 additions & 2 deletions crates/engine/src/game/mana_sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2348,8 +2348,25 @@ fn land_mana_options(
gates,
);

// Legacy fallback for basic-land subtype-only objects (no explicit mana ability).
if options.is_empty() {
// CR 602.5: Legacy fallback for basic-land subtype-only objects that
// carry NO EXPLICIT mana ability at all (a nonbasic granted a basic land
// type by Urborg/Blood Moon-class effects with no accompanying
// `Effect::Mana` grant). This must NOT fire merely because
// `scan_mana_abilities` came back empty — it can be empty because a REAL
// `Effect::Mana` ability exists but was just filtered out by a legality
// gate (CantBeActivated, CantActivateDuring, an unsatisfied activation
// condition). Falling back to unconditional subtype-inferred production
// in that case would silently defeat the gate that filtered it (issue
// #6469: Karn, the Great Creator's "activated abilities of artifacts your
// opponents control can't be activated" stopped blocking a Liquimetal-
// Coating-turned-artifact land's own {T}: Add mana ability, because the
// ability's legitimate absence from `options` was mistaken for "no
// ability exists").
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
let has_explicit_mana_ability = obj
.abilities
.iter()
.any(|ability| matches!(*ability.effect, Effect::Mana { .. }));
if options.is_empty() && !has_explicit_mana_ability {
if let Some(mana_type) = obj
.card_types
.subtypes
Expand Down
Loading