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
4 changes: 3 additions & 1 deletion crates/engine/src/game/ability_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4643,6 +4643,7 @@ pub(crate) fn keyword_cost_reads_growing_class(kw: &Keyword) -> bool {
| Keyword::Delve
| Keyword::Craft { .. }
| Keyword::Emerge(_)
| Keyword::EmergeFromQuality(_)
| Keyword::Offering(_)
| Keyword::Bargain
| Keyword::Casualty(_)
Expand Down Expand Up @@ -4914,7 +4915,8 @@ fn scan_keyword(kw: &Keyword, mode: ScanMode) -> Axes {
| Keyword::Echo(_)
| Keyword::Buyback(_)
| Keyword::Cycling(_)
| Keyword::Flashback(_) => Axes::CONSERVATIVE,
| Keyword::Flashback(_)
| Keyword::EmergeFromQuality(_) => Axes::CONSERVATIVE,
// Every other keyword carries a read-free payload (unit / u32 / String /
// ManaCost / value tag): it reads nothing on any axis here. Its cost-read,
// if any, is already captured by `cost_read` above.
Expand Down
107 changes: 78 additions & 29 deletions crates/engine/src/game/casting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::types::ability::{
ModalSelectionCondition, ObjectScope, PlayerFilter, PlayerScope, ProhibitedActivity,
QuantityExpr, QuantityRef, ResolvedAbility, RestrictionExpiry, RestrictionPlayerScope,
StaticCondition, StaticDefinition, SubAbilityLink, TapCreaturesRequirement, TargetFilter,
TargetRef,
TargetRef, TypedFilter,
};
use crate::types::actions::{AlternativeCastDecision, GameAction};
use crate::types::card::LayoutKind;
Expand All @@ -21,7 +21,7 @@ use crate::types::game_state::{
TargetSelectionSlot, WaitingFor,
};
use crate::types::identifiers::{CardId, ObjectId, TrackedSetId};
use crate::types::keywords::{FlashbackCost, Keyword, KeywordKind};
use crate::types::keywords::{EmergeCost, FlashbackCost, Keyword, KeywordKind};
use crate::types::mana::{
ActivationManaColorConstraint, ManaColor, ManaCost, ManaCostShard, ManaSourceOutput,
ManaSourceSelection, ManaSpellGrant, ManaType, PaymentContext, SpecialAction, SpellMeta,
Expand Down Expand Up @@ -2547,6 +2547,22 @@ pub(crate) fn effective_spell_keywords(
effective_spell_keywords_for(state, caster, object_id, false)
}

/// CR 702.119a-b: The active Emerge keyword supplies the permanent quality for
/// its required sacrifice cost.
fn effective_emerge_sacrifice_filter(
state: &GameState,
caster: PlayerId,
object_id: ObjectId,
) -> Option<TargetFilter> {
effective_spell_keywords(state, caster, object_id)
.into_iter()
.find_map(|keyword| match keyword {
Keyword::Emerge(_) => Some(TargetFilter::Typed(TypedFilter::creature())),
Keyword::EmergeFromQuality(cost) => Some(cost.sacrifice_filter),
_ => None,
})
}

/// Fuse-aware sibling of [`effective_spell_keywords`]. `fused` projects a
/// pre-payment fused split spell with its COMBINED characteristics (CR 702.102b)
/// so `CastWithKeyword`-granted keywords keyed on mana value / colors are granted
Expand Down Expand Up @@ -5734,13 +5750,19 @@ fn casting_variant_candidates(
candidates.push(CastingVariant::Overload);
}

// CR 702.119a-c + CR 118.9: Emerge is a hand-zone alternative cost that
// requires sacrificing a creature and reducing the emerge cost by that
// creature's mana value.
// CR 702.119a-b + CR 118.9: Emerge is a hand-zone alternative cost that
// requires sacrificing its printed permanent quality and reducing the
// emerge cost by that permanent's mana value.
if obj.zone == Zone::Hand
&& effective_spell_keywords(state, player, object_id)
.iter()
.any(|k| matches!(k, crate::types::keywords::Keyword::Emerge(_)))
.any(|k| {
matches!(
k,
crate::types::keywords::Keyword::Emerge(_)
| crate::types::keywords::Keyword::EmergeFromQuality(_)
)
})
{
candidates.push(CastingVariant::Emerge);
}
Expand Down Expand Up @@ -6547,6 +6569,9 @@ fn prepare_spell_cast_with_variant_override_inner(
.iter()
.find_map(|k| match k {
crate::types::keywords::Keyword::Emerge(cost) => Some(cost.clone()),
crate::types::keywords::Keyword::EmergeFromQuality(cost) => {
Some(cost.mana_cost.clone())
}
_ => None,
})
} else {
Expand Down Expand Up @@ -11549,26 +11574,38 @@ pub fn handle_cast_spell_with_payment_mode(
}
}

// CR 702.119a-c: Emerge — when a hand card has Keyword::Emerge and both
// CR 702.119a-b: Emerge — when a hand card has Keyword::Emerge and both
// costs are affordable, present a choice. Emerge affordability includes a
// legal creature sacrifice and the reduced emerge cost after that
// sacrificed creature's mana value is subtracted.
// legal printed-quality sacrifice and the reduced emerge cost after that
// permanent's mana value is subtracted.
if let Some(obj) = state.objects.get(&object_id) {
if obj.zone == Zone::Hand {
if let Some(emerge_cost) = effective_spell_keywords(state, player, object_id)
.into_iter()
.find_map(|k| match k {
crate::types::keywords::Keyword::Emerge(cost) => Some(cost),
crate::types::keywords::Keyword::Emerge(cost) => {
Some(EmergeCost::creature(cost))
}
crate::types::keywords::Keyword::EmergeFromQuality(cost) => Some(cost),
_ => None,
})
{
let (normal_cost, normal_affordable) =
normal_cast_choice_cost_and_affordability(state, player, object_id, obj);
let emerge_cost_eff =
apply_cost_modifiers_to_base(state, player, object_id, emerge_cost.clone())
.unwrap_or_else(|| emerge_cost.clone());
let emerge_affordable =
casting_costs::can_pay_emerge_cost(state, player, object_id, &emerge_cost_eff);
let emerge_cost_eff = apply_cost_modifiers_to_base(
state,
player,
object_id,
emerge_cost.mana_cost.clone(),
)
.unwrap_or_else(|| emerge_cost.mana_cost.clone());
let emerge_affordable = casting_costs::can_pay_emerge_cost(
state,
player,
object_id,
&emerge_cost_eff,
&emerge_cost.sacrifice_filter,
);
if normal_affordable && emerge_affordable {
return Ok(WaitingFor::AlternativeCastChoice {
player,
Expand All @@ -11578,7 +11615,9 @@ pub fn handle_cast_spell_with_payment_mode(
keyword: crate::types::game_state::AlternativeCastKeyword::Emerge,
normal_cost,
alternative_cost: Some(emerge_cost_eff),
alternative_additional_cost: Some(casting_costs::emerge_sacrifice_cost()),
alternative_additional_cost: Some(casting_costs::emerge_sacrifice_cost(
emerge_cost.sacrifice_filter,
)),
});
}
if !normal_affordable && emerge_affordable {
Expand Down Expand Up @@ -12843,11 +12882,13 @@ fn continue_with_prepared(
));
}

// CR 702.119a-c + CR 601.2b/h: Emerge requires choosing which creature to
// sacrifice as the player chooses to pay the emerge cost, then sacrificing
// it as that cost is paid. Route this before any target selection so the
// required sacrifice is declared on the CR 601.2b axis.
// CR 702.119a-c + CR 601.2b/h: Emerge requires choosing the matching
// permanent to sacrifice as the player chooses to pay the emerge cost,
// then sacrificing it as that cost is paid. Route this before any target
// selection so the required sacrifice is declared on the CR 601.2b axis.
if prepared.casting_variant == CastingVariant::Emerge {
let sacrifice_filter = effective_emerge_sacrifice_filter(state, player, prepared.object_id)
.expect("Emerge casting variant requires an effective Emerge keyword");
return casting_costs::begin_required_cost_before_targets(
state,
player,
Expand All @@ -12856,7 +12897,7 @@ fn continue_with_prepared(
resolved,
prepared.mana_cost,
Some(prepared.base_mana_cost.clone()),
casting_costs::emerge_sacrifice_cost(),
casting_costs::emerge_sacrifice_cost(sacrifice_filter),
SpellCostSource::Emerge,
prepared.casting_variant,
prepared.casting_permission_index,
Expand Down Expand Up @@ -13358,6 +13399,8 @@ fn continue_with_no_ability(
player,
);
if prepared.casting_variant == CastingVariant::Emerge {
let sacrifice_filter = effective_emerge_sacrifice_filter(state, player, prepared.object_id)
.expect("Emerge casting variant requires an effective Emerge keyword");
return casting_costs::begin_required_cost_before_targets(
state,
player,
Expand All @@ -13366,7 +13409,7 @@ fn continue_with_no_ability(
placeholder,
prepared.mana_cost,
Some(prepared.base_mana_cost.clone()),
casting_costs::emerge_sacrifice_cost(),
casting_costs::emerge_sacrifice_cost(sacrifice_filter),
SpellCostSource::Emerge,
prepared.casting_variant,
prepared.casting_permission_index,
Expand Down Expand Up @@ -14239,16 +14282,22 @@ fn can_cast_prepared_now_with_probe(
return false;
}

// CR 702.119a-c: Emerge affordability is the reduced emerge cost after
// sacrificing a legal creature, not the unreduced `prepared.mana_cost`.
// CR 702.119a-b: Emerge affordability is the reduced emerge cost after
// sacrificing a legal matching permanent, not the unreduced
// `prepared.mana_cost`.
if prepared.casting_variant == CastingVariant::Emerge {
return (prepared.modal.is_some()
|| spell_has_legal_targets_with_probe(state, obj.id, player, probe))
&& casting_costs::can_pay_emerge_cost(
state,
player,
prepared.object_id,
&prepared.mana_cost,
&& effective_emerge_sacrifice_filter(state, player, prepared.object_id).is_some_and(
|sacrifice_filter| {
casting_costs::can_pay_emerge_cost(
state,
player,
prepared.object_id,
&prepared.mana_cost,
&sacrifice_filter,
)
},
);
}

Expand Down
73 changes: 41 additions & 32 deletions crates/engine/src/game/casting_costs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2906,7 +2906,7 @@ pub(crate) fn handle_sacrifice_for_cost(
{
Some(SpellCostSource::Offering)
} else if payment.source == SpellCostSource::Emerge
&& is_emerge_sacrifice_cost(payment.cost)
&& is_emerge_sacrifice_cost(state, player, pending.object_id, payment.cost)
{
Some(SpellCostSource::Emerge)
} else {
Expand Down Expand Up @@ -7786,52 +7786,61 @@ fn is_offering_sacrifice_cost(
)
}

fn emerge_sacrifice_filter() -> TargetFilter {
TargetFilter::Typed(TypedFilter::creature())
}

fn is_emerge_sacrifice_cost(cost: &AbilityCost) -> bool {
fn is_emerge_sacrifice_cost(
state: &GameState,
player: PlayerId,
object_id: ObjectId,
cost: &AbilityCost,
) -> bool {
let Some(sacrifice_filter) = super::casting::effective_spell_keywords(state, player, object_id)
.into_iter()
.find_map(|keyword| match keyword {
crate::types::keywords::Keyword::Emerge(_) => {
Some(TargetFilter::Typed(TypedFilter::creature()))
}
crate::types::keywords::Keyword::EmergeFromQuality(cost) => Some(cost.sacrifice_filter),
_ => None,
})
else {
return false;
};
matches!(
cost,
AbilityCost::Sacrifice(cost)
if cost.requirement == SacrificeRequirement::count(1)
&& cost.target == emerge_sacrifice_filter()
&& cost.target == sacrifice_filter
)
}

/// CR 702.119a-c: Build the required sacrifice component of Emerge's
/// alternative cost. The sacrificed creature's mana value is applied as a cost
/// reduction by `handle_sacrifice_for_cost` while the creature is still on the
/// CR 702.119a-b: Build Emerge's required sacrifice component from its printed
/// permanent-quality filter. The sacrificed permanent's mana value is applied
/// as a cost reduction by `handle_sacrifice_for_cost` while it remains on the
/// battlefield.
pub(super) fn emerge_sacrifice_cost() -> AbilityCost {
AbilityCost::Sacrifice(SacrificeCost::count(emerge_sacrifice_filter(), 1))
pub(super) fn emerge_sacrifice_cost(sacrifice_filter: TargetFilter) -> AbilityCost {
AbilityCost::Sacrifice(SacrificeCost::count(sacrifice_filter, 1))
}

/// CR 702.119a-c: Emerge can be paid only if a legal creature can be
/// CR 702.119a-b: Emerge can be paid only if a matching permanent can be
/// sacrificed and the resulting reduced emerge mana cost can be paid.
pub(super) fn can_pay_emerge_cost(
state: &GameState,
player: PlayerId,
object_id: ObjectId,
emerge_cost: &ManaCost,
sacrifice_filter: &TargetFilter,
) -> bool {
super::casting::find_eligible_sacrifice_targets(
state,
player,
object_id,
&emerge_sacrifice_filter(),
)
.into_iter()
.any(|creature| {
let mut reduced = emerge_cost.clone();
apply_emerge_cost_reduction(state, creature, &mut reduced);
// CR 601.2f + CR 702.119a: Affordability probes must include the
// final Trinisphere-class floor after Emerge's sacrifice reduction.
if !cost_has_x(&reduced) {
super::casting::apply_cost_floor(state, player, object_id, &mut reduced);
}
super::casting::can_pay_cost_after_auto_tap(state, player, object_id, &reduced)
})
super::casting::find_eligible_sacrifice_targets(state, player, object_id, sacrifice_filter)
.into_iter()
.any(|permanent| {
let mut reduced = emerge_cost.clone();
apply_emerge_cost_reduction(state, permanent, &mut reduced);
// CR 601.2f + CR 702.119a: Affordability probes must include the
// final Trinisphere-class floor after Emerge's sacrifice reduction.
if !cost_has_x(&reduced) {
super::casting::apply_cost_floor(state, player, object_id, &mut reduced);
}
super::casting::can_pay_cost_after_auto_tap(state, player, object_id, &reduced)
})
}

fn additional_cost_x_max(
Expand Down Expand Up @@ -8477,8 +8486,8 @@ pub(super) fn apply_offering_cost_reduction(
*spell_generic = spell_generic.saturating_sub(sac_generic);
}

/// CR 702.119a: Reduce the Emerge cost by generic mana equal to the sacrificed
/// creature's mana value. Colored pips in the Emerge cost are never reduced.
/// CR 702.119a-b: Reduce the Emerge cost by generic mana equal to the sacrificed
/// permanent's mana value. Colored pips in the Emerge cost are never reduced.
pub(super) fn apply_emerge_cost_reduction(
state: &GameState,
sacrifice_id: ObjectId,
Expand Down
Loading
Loading