Skip to content
Open
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
35 changes: 19 additions & 16 deletions crates/engine/src/parser/oracle_effect/assembly.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ use super::conditions::ability_condition_to_static_condition;
use super::lower::{
append_remember_card_to_standalone_exiled_choice, apply_where_x_ability_expression,
apply_where_x_to_latest_def, attach_alt_ability_cost_to_previous_play_from_exile,
attach_any_color_mana_rider_to_previous_play_from_exile,
attach_cast_cost_modifier_to_previous_play_from_exile,
attach_cast_cost_modifier_to_prior_cast_from_zone,
attach_graveyard_redirect_rider_to_prior_cast_from_zone,
Expand All @@ -53,8 +52,7 @@ use super::lower::{
fold_exile_resolving_rider, fold_search_choose_type_conditional_destination,
fold_token_it_has_grants_into_token_statics, gate_other_revealed_card_on_multiplayer_reveal,
gate_reflexive_rider_on_declined_optional_target, is_exile_until_cast_bottom_cleanup,
is_land_enters_tapped_rider, is_linked_exile_cast_bottom_cleanup,
is_spend_mana_as_any_color_rider, is_stable_branch_amount,
is_land_enters_tapped_rider, is_linked_exile_cast_bottom_cleanup, is_stable_branch_amount,
nest_whenever_this_turn_token_cleanup_delayed_trigger,
normalize_exile_until_cast_bottom_cleanup, normalize_linked_exile_cast_bottom_cleanup,
parse_controlled_by_different_players_target_constraint,
Expand All @@ -73,7 +71,8 @@ use super::sequence::{apply_clause_continuation, def_bears_retargetable_copy};
use super::{
append_to_deepest_sub_ability, apply_player_scope_rewrites,
attach_alt_cost_to_prior_cast_from_zone, attach_mana_retention_to_prior_mana,
attach_perpetual_keyword_grants, attach_repeat_process_keywords, attach_same_is_true_keywords,
attach_mana_spend_permission_to_prior_cast_grant, attach_perpetual_keyword_grants,
attach_repeat_process_keywords, attach_same_is_true_keywords,
bind_anaphoric_damage_subject_keep_recipient, collapse_ephemeral_color_choice_mana,
contains_explicit_tracked_set_pronoun, contains_implicit_tracked_set_pronoun,
def_is_damage_dealer, def_is_dig_look, def_is_dig_or_mill, def_is_generic_effect_head,
Expand Down Expand Up @@ -2223,6 +2222,22 @@ pub(crate) fn assemble_effect_chain(ir: &EffectChainIr) -> AbilityDefinition {
PriorModifier::ManaRetention(expiry) => {
attach_mana_retention_to_prior_mana(&mut defs, *expiry);
}
PriorModifier::ManaSpendPermission(permission) => {
// CR 609.4b: the rider was admitted only because the
// clause it follows grants a cast without a concession
// (`prior_clause_grants_a_cast_without_mana_spend_permission`),
// so the stamp lands on that grant — the last def.
let stamped = attach_mana_spend_permission_to_prior_cast_grant(
&mut defs,
*permission,
);
debug_assert!(
stamped,
"CR 609.4b: a mana rider admitted for the prior cast grant found \
no grant to stamp on the last def: {:?}",
defs.last()
);
}
PriorModifier::EntersTappedAttacking => {
// CR 508.4 / CR 614.1: Conditional enters-tapped-attacking modifier.
// U6-C2: LastEmitted + an EffectShape guard. A wrong-shaped prior
Expand Down Expand Up @@ -2589,18 +2604,6 @@ pub(crate) fn assemble_effect_chain(ir: &EffectChainIr) -> AbilityDefinition {
continue;
}

// CR 609.4b + CR 608.2c: Brainstealer/Daxos-class any-color mana
// riders may be split into their own sentence or comma sibling after a
// `PlayFromExile` grant. They scope the existing exile-play
// permission, so fold the rider into the prior grant instead of
// emitting a broad standalone `SpendManaAsAnyColor` effect.
if is_spend_mana_as_any_color_rider(clause_ir)
&& attach_any_color_mana_rider_to_previous_play_from_exile(&mut defs)
{
prev_boundary = clause_ir.boundary;
continue;
}

// CR 614.1a + CR 608.2g: An exact "if a spell cast this way would be
// put into a graveyard" rider scopes each cast in the immediately prior
// free-cast window. Absorb it before the legacy singular-spell route;
Expand Down
78 changes: 6 additions & 72 deletions crates/engine/src/parser/oracle_effect/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ use crate::types::ability::{
AttackSubject, CastCostModifier, CastFromZoneDriver, CastPermissionConstraint,
CastingPermission, Comparator, ConjureSource, ContinuousModification, ControllerRef,
DamageChannel, DamageSource, DelayedTriggerCondition, Duration, Effect, EffectScope,
ExiledSpellRider, FilterProp, GameRestriction, LibraryPosition, ManaSpendPermission,
MultiTargetSpec, ObjectScope, PermissionGrantee, PlayerFilter, PreventionAmount,
PreventionScope, PtValue, QuantityExpr, QuantityRef, RestrictionPlayerScope, RoundingMode,
SpellStackToGraveyardReplacement, StaticCondition, StaticDefinition, SubAbilityLink,
TargetChoiceTiming, TargetFilter, TypeFilter, TypedFilter,
ExiledSpellRider, FilterProp, GameRestriction, LibraryPosition, MultiTargetSpec, ObjectScope,
PermissionGrantee, PlayerFilter, PreventionAmount, PreventionScope, PtValue, QuantityExpr,
QuantityRef, RestrictionPlayerScope, RoundingMode, SpellStackToGraveyardReplacement,
StaticCondition, StaticDefinition, SubAbilityLink, TargetChoiceTiming, TargetFilter,
TypeFilter, TypedFilter,
};
use crate::types::counter::CounterType;
use crate::types::game_state::{DistributionUnit, TargetSelectionConstraint};
use crate::types::phase::Phase;
use crate::types::statics::{CostModifyMode, StaticMode};
use crate::types::statics::CostModifyMode;
use crate::types::zones::{EtbTapState, Zone};

// Parse-phase functions from the parent module (oracle_effect/mod.rs).
Expand Down Expand Up @@ -989,72 +989,6 @@ pub(super) fn normalize_exile_until_cast_bottom_cleanup(effect: &mut Effect) {
}
}

pub(super) fn is_spend_mana_as_any_color_rider(clause: &ClauseIr) -> bool {
let Effect::GenericEffect {
static_abilities, ..
} = &clause.parsed.effect
else {
return false;
};
if static_abilities.len() != 1
|| static_abilities[0].mode
!= (StaticMode::SpendManaAsAnyColor {
spell_filter: None,
activation_source_filter: None,
})
{
return false;
}

let lower = clause
.source
.fragment()
.unwrap_or_default()
.to_ascii_lowercase();
let parsed = all_consuming((
opt(alt((
tag::<_, _, OracleError<'_>>("if you cast a spell this way, "),
tag("if you cast it this way, "),
))),
tag("you may spend mana as though it were mana of any "),
alt((tag("color"), tag("type"))),
tag(" to cast "),
alt((
tag("it"),
tag("that spell"),
tag("a spell this way"),
tag("spells this way"),
tag("those spells"),
)),
opt(tag(".")),
))
.parse(lower.trim())
.is_ok();
parsed
}

pub(super) fn attach_any_color_mana_rider_to_previous_play_from_exile(
defs: &mut [AbilityDefinition],
) -> bool {
let Some(previous) = defs.last_mut() else {
return false;
};
let Effect::GrantCastingPermission {
permission:
CastingPermission::PlayFromExile {
mana_spend_permission,
..
},
..
} = previous.effect.as_mut()
else {
return false;
};

*mana_spend_permission = Some(ManaSpendPermission::AnyTypeOrColor);
true
}

/// CR 614.1a + CR 608.2n: Fold a "if that spell would be put into a graveyard,
/// [put it on the library / return it to its owner's hand] instead" rider onto
/// the immediately-preceding optional `CastFromZone` as its canonical
Expand Down
Loading