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
28 changes: 28 additions & 0 deletions crates/engine/src/parser/oracle_effect/sequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1306,6 +1306,21 @@ pub(super) fn split_clause_sequence(text: &str) -> Vec<ClauseChunk> {
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.
Comment thread
matthewevans marked this conversation as resolved.
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
}
Comment thread
matthewevans marked this conversation as resolved.

fn quote_closes_sentence_before_sequence(current: &str, remainder: &str) -> bool {
let quoted_text_ends_sentence = current
.chars()
Expand All @@ -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).
Comment thread
matthewevans marked this conversation as resolved.
if clause_is_emblem_creation_head(current) {
return true;
}

let trimmed = remainder.trim_start();
let trimmed_lower = trimmed.to_ascii_lowercase();
if alt((
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
);
}
1 change: 1 addition & 0 deletions crates/engine/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading