Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 10 additions & 0 deletions crates/engine/src/parser/oracle_static/anthem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,16 @@ pub(crate) fn parse_continuous_gets_has(
);
}
}
// CR 205.1b + CR 604.1: also recover a trailing type-addition
// ("and is an Assassin in addition to its other types",
// Reaper's Scythe) or a trailing quoted-ability grant ("and has
// \"{T}, Sacrifice a creature: ...\"", Rakdos Riteknife) after the
// dynamic pump — the keyword path above only recovers trailing
// keywords. Both scanners no-op when their pattern is absent.
if let Some(type_mods) = parse_additive_type_clause_modifications(description) {
modifications.extend(type_mods);
}
modifications.extend(parse_quoted_ability_modifications(description));
return Some(
StaticDefinition::continuous()
.affected(affected)
Expand Down
67 changes: 67 additions & 0 deletions crates/engine/src/parser/oracle_static/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,73 @@ use crate::types::keywords::Keyword;
use crate::types::mana::ManaCost;
use crate::types::statics::{AdditionalCostTaxAction, CrewAction, CrewContributionKind};

// CR 205.1b + CR 604.1: a dynamic "gets +N/+M for each X" pump may carry a
// trailing conjunct the for-each path used to drop (it only recovered trailing
// keywords): "and is a <Subtype> in addition to its other types" (Reaper's
// Scythe) or "and has \"<quoted ability>\"" (Rakdos Riteknife).
#[test]
fn dynamic_for_each_pump_recovers_trailing_subtype_addition() {
let defs = parse_static_line_multi(
"Equipped creature gets +1/+1 for each soul counter on this Equipment and is an Assassin in addition to its other types.",
);
assert_eq!(defs.len(), 1);
let mods = &defs[0].modifications;
assert!(
mods.iter()
.any(|m| matches!(m, ContinuousModification::AddDynamicPower { .. })),
"dynamic pump lost: {mods:?}"
);
assert!(
mods.contains(&ContinuousModification::AddSubtype {
subtype: "Assassin".to_string()
}),
"trailing 'is an Assassin' dropped: {mods:?}"
);
}

#[test]
fn dynamic_for_each_pump_recovers_trailing_quoted_ability() {
let defs = parse_static_line_multi(
"Equipped creature gets +1/+0 for each blood counter on this Equipment and has \"{T}, Sacrifice a creature: Put a blood counter on this permanent.\"",
);
assert_eq!(defs.len(), 1);
let mods = &defs[0].modifications;
assert!(
mods.iter()
.any(|m| matches!(m, ContinuousModification::AddDynamicPower { .. })),
"dynamic pump lost: {mods:?}"
);
assert!(
mods.iter()
.any(|m| matches!(m, ContinuousModification::GrantAbility { .. })),
"trailing quoted ability dropped: {mods:?}"
);
}

// Regression: a for-each pump with only a trailing KEYWORD is unchanged and gains
// no spurious subtype/grant.
#[test]
fn dynamic_for_each_pump_trailing_keyword_unchanged() {
let defs = parse_static_line_multi(
"Enchanted creature gets +1/+0 for each Mountain you control and has first strike.",
);
assert_eq!(defs.len(), 1);
let mods = &defs[0].modifications;
assert!(mods
.iter()
.any(|m| matches!(m, ContinuousModification::AddDynamicPower { .. })));
assert!(mods.contains(&ContinuousModification::AddKeyword {
keyword: Keyword::FirstStrike
}));
assert!(
!mods.iter().any(|m| matches!(
m,
ContinuousModification::AddSubtype { .. } | ContinuousModification::GrantAbility { .. }
)),
"keyword-only pump must not gain spurious mods: {mods:?}"
);
}

/// CR 207.2c: an ability word is italic flavor with no rules meaning. A leading
/// ability-word label on a subject-anchored static must be stripped so the static
/// still parses; a leading label that is NOT a recognized ability word must be
Expand Down
Loading