diff --git a/crates/engine/src/parser/oracle_effect/sequence.rs b/crates/engine/src/parser/oracle_effect/sequence.rs index 0377061aa4..bde36dd394 100644 --- a/crates/engine/src/parser/oracle_effect/sequence.rs +++ b/crates/engine/src/parser/oracle_effect/sequence.rs @@ -1306,6 +1306,21 @@ pub(super) fn split_clause_sequence(text: &str) -> Vec { chunks } +/// CR 114.1: True when the clause-so-far begins with the emblem-creation head +/// (`you get an emblem with "…"` or the subject-stripped `get an emblem with +/// "…"`). Combinator-only dispatch mirroring `try_parse_emblem_creation`'s prefix +/// so the clause splitter treats an emblem's granted-ability quote as a +/// self-contained sentence. +fn clause_is_emblem_creation_head(current: &str) -> bool { + let is_emblem_head = alt(( + tag_no_case::<_, _, OracleError<'_>>("you get an emblem with \""), + tag_no_case("get an emblem with \""), + )) + .parse(current.trim_start()) + .is_ok(); + is_emblem_head +} + fn quote_closes_sentence_before_sequence(current: &str, remainder: &str) -> bool { let quoted_text_ends_sentence = current .chars() @@ -1316,6 +1331,19 @@ fn quote_closes_sentence_before_sequence(current: &str, remainder: &str) -> bool return false; } + // CR 114.1: An emblem is a self-contained command-zone object with no board + // presence, so a sentence following its granted-ability quote can never be an + // anaphor referring back to it (unlike a token/permanent granted-ability + // quote, where a trailing "It becomes …" refers to the created object). When + // the clause-so-far is the emblem head (`you get an emblem with "…"`), the + // sentence-ending close quote always closes the sentence, so the emblem's + // sibling effects split into their own clauses — Nissa, Who Shakes the World's + // "… Search your library …" would otherwise be swallowed into the emblem's + // static text (issue #5282). + if clause_is_emblem_creation_head(current) { + return true; + } + let trimmed = remainder.trim_start(); let trimmed_lower = trimmed.to_ascii_lowercase(); if alt(( diff --git a/crates/engine/tests/integration/issue_5282_nissa_ultimate_emblem_search.rs b/crates/engine/tests/integration/issue_5282_nissa_ultimate_emblem_search.rs new file mode 100644 index 0000000000..7babb0fcfa --- /dev/null +++ b/crates/engine/tests/integration/issue_5282_nissa_ultimate_emblem_search.rs @@ -0,0 +1,81 @@ +//! Issue #5282 — Nissa, Who Shakes the World's [−8] ultimate must both create +//! an emblem AND let you search your library for Forest cards. +//! +//! The ultimate reads: +//! "You get an emblem with \"Lands you control have indestructible.\" Search +//! your library for any number of Forest cards, put them onto the battlefield +//! tapped, then shuffle." +//! +//! The emblem's granted static ends in a sentence-final close quote +//! (`indestructible."`). The clause splitter did not treat that close quote as a +//! sentence boundary, so the following "Search your library …" sentence was +//! glued onto the emblem clause and swallowed into the emblem's static text — +//! the ability never produced an `Effect::SearchLibrary`, so activating the +//! ultimate resolved without searching the library (the reported bug). +//! +//! This pins the fix: an emblem's sentence-ending close quote closes the +//! sentence, so the sibling search clause parses on its own and surfaces a +//! `SearchChoice` at resolution (CR 701.23). + +use engine::game::scenario::{GameScenario, P0}; +use engine::types::card_type::CoreType; +use engine::types::counter::CounterType; +use engine::types::game_state::WaitingFor; +use engine::types::phase::Phase; + +const NISSA_ORACLE: &str = "\ +Whenever you tap a Forest for mana, add an additional {G}.\n\ +[+1]: Put three +1/+1 counters on up to one target noncreature land you control. Untap it. It becomes a 0/0 Elemental creature with vigilance and haste that's still a land.\n\ +[−8]: You get an emblem with \"Lands you control have indestructible.\" Search your library for any number of Forest cards, put them onto the battlefield tapped, then shuffle."; + +#[test] +fn nissa_who_shakes_the_world_ultimate_searches_library_for_forests() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + + let nissa = scenario + .add_creature(P0, "Nissa, Who Shakes the World", 0, 0) + .from_oracle_text(NISSA_ORACLE) + .id(); + + // A Forest in the library gives the "any number of Forest cards" search a + // legal card to find. + let forest = scenario.add_card_to_library_top(P0, "Forest"); + + let mut runner = scenario.build(); + { + let state = runner.state_mut(); + // CR 306.5b: a planeswalker's loyalty is its loyalty-counter count; seed + // 8 so the [−8] (CR 606.6: can't drop loyalty below 0) is legal. + let obj = state.objects.get_mut(&nissa).expect("nissa"); + obj.card_types.core_types = vec![CoreType::Planeswalker]; + obj.base_card_types = obj.card_types.clone(); + obj.loyalty = Some(8); + obj.counters.insert(CounterType::Loyalty, 8); + + // Give the library card real Forest characteristics so the + // Typed[Land, Subtype(Forest)] search filter matches it. + let land = state.objects.get_mut(&forest).expect("forest"); + land.card_types.core_types = vec![CoreType::Land]; + land.card_types.subtypes = vec!["Forest".to_string()]; + land.base_card_types = land.card_types.clone(); + } + + // Activate [−8]: ability index 1 ([+1] is index 0; the "Whenever you tap a + // Forest for mana" line is a triggered ability, not an activated one). + let outcome = runner.activate(nissa, 1).resolve(); + + // CR 701.23: The ultimate's second sentence is a library search. Before the + // fix it was swallowed into the emblem's static text and no search ran, so + // the chain resolved straight to a priority window. With the fix the + // `Effect::SearchLibrary` resolves and the driver pauses on the interactive + // `SearchChoice` for the controller. + assert!( + matches!( + outcome.final_waiting_for(), + WaitingFor::SearchChoice { player, .. } if *player == P0 + ), + "Nissa's [−8] must search the library (surface SearchChoice); got {:?}", + outcome.final_waiting_for() + ); +} diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index 84b4925fbd..a8022b25cd 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -429,6 +429,7 @@ mod issue_4921_skullscorch_unless_deal_damage; mod issue_4962_volo_guide_to_monsters; mod issue_5145_violent_eruption_choose_target_distribution; mod issue_5159_attacks_alone_investigate; +mod issue_5282_nissa_ultimate_emblem_search; mod issue_5328_attacks_alone_observer; mod issue_5335_stonehoof_mass_attack; mod issue_5336_kodama_mana_value_filter;