Skip to content
104 changes: 101 additions & 3 deletions crates/engine/src/game/effects/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ use crate::game::conditions::{
use crate::game::filter;
use crate::game::speed::has_max_speed;
use crate::types::ability::{
AbilityCondition, AbilityCost, AbilityKind, CardPlayMode, CardTypeSetSource, ControllerRef,
CopyRetargetPermission, CostPaidObjectSnapshot, EachDamageRecipient, Effect, EffectError,
EffectKind, EffectOutcomeSignal, EffectResolutionResult, EffectScope, FilterProp,
AbilityCondition, AbilityCost, AbilityDefinition, AbilityKind, CardPlayMode, CardTypeSetSource,
ControllerRef, CopyRetargetPermission, CostPaidObjectSnapshot, EachDamageRecipient, Effect,
EffectError, EffectKind, EffectOutcomeSignal, EffectResolutionResult, EffectScope, FilterProp,
ManaProduction, OpponentMayScope, PlayerFilter, PlayerScope, QuantityExpr, QuantityRef,
RepeatContinuation, ResolvedAbility, RevealUntilDisposition, SacrificeCost,
SacrificeRequirement, SharedQuality, SharedQualityRelation, SiblingCondition, SubAbilityLink,
Expand Down Expand Up @@ -11613,6 +11613,27 @@ fn resolve_chain_body(
);
resolve_ability_chain(state, &trailing_resolved, events, depth + 1)?;
}
} else if ability.forward_result
&& forwarded_objects.is_empty()
&& ability_chain_refs_parent_target(sub)
{
// CR 608.2c: A forward-result continuation is anchored to the object
// moved by the preceding instruction. If no object moved, that
// instruction has no referent for dependent riders such as "it gains
// haste" or "sacrifice it"; do not let ParentTarget fall back to the
// original ability source. Walk past dependent sequential siblings
// and resume at the first independent sibling instead of terminating
// the entire printed instruction chain.
if let Some(mut remaining) = without_missing_forward_result_dependencies(sub) {
apply_parent_chain_context(
&mut remaining,
ability,
effect_context_object.as_ref(),
state,
);
resolve_ability_chain(state, &remaining, events, depth + 1)?;
}
return Ok(());
} else if !forwarded_objects.is_empty() {
let mut sub_with_context = sub.as_ref().clone();
// CR 707.10: `CopySpell { SelfRef }` copies the resolving spell
Expand Down Expand Up @@ -11943,6 +11964,83 @@ fn resolve_chain_body(
Ok(())
}

/// CR 608.2c + CR 603.7c: Detect a ParentTarget dependency anywhere in a
/// continuation, including a delayed-trigger payload whose AbilityDefinition
/// is nested inside the current effect. This keeps a missing forward-result
/// object from rebinding an inner rider to the original source while allowing
/// independent sequential siblings to continue.
fn ability_chain_refs_parent_target(ability: &ResolvedAbility) -> bool {
effect_chain_refs_parent_target(&ability.effect)
|| ability
.sub_ability
.as_deref()
.is_some_and(ability_chain_refs_parent_target)
|| ability
.else_ability
.as_deref()
.is_some_and(ability_chain_refs_parent_target)
}

fn ability_definition_chain_refs_parent_target(definition: &AbilityDefinition) -> bool {
effect_chain_refs_parent_target(&definition.effect)
|| definition
.sub_ability
.as_deref()
.is_some_and(ability_definition_chain_refs_parent_target)
|| definition
.else_ability
.as_deref()
.is_some_and(ability_definition_chain_refs_parent_target)
}

fn effect_chain_refs_parent_target(effect: &Effect) -> bool {
effect_refs_parent_target(effect)
|| matches!(
effect,
Effect::CreateDelayedTrigger { effect: definition, .. }
if ability_definition_chain_refs_parent_target(definition)
)
}

/// CR 608.2c: Remove only continuation nodes whose effects require the absent
/// forward-result object. Preserve independent instructions and both of their
/// continuation edges; when a dependent node is removed, resume at its next
/// `SequentialSibling` rather than treating a dependent `ContinuationStep` as
/// independently executable.
fn without_missing_forward_result_dependencies(
ability: &ResolvedAbility,
) -> Option<ResolvedAbility> {
if effect_chain_refs_parent_target(&ability.effect) {
return first_independent_forward_result_sibling(ability.sub_ability.as_deref());
}

let mut remaining = ability.clone();
remaining.sub_ability = ability
.sub_ability
.as_deref()
.and_then(without_missing_forward_result_dependencies)
.map(Box::new);
remaining.else_ability = ability
.else_ability
.as_deref()
.and_then(without_missing_forward_result_dependencies)
.map(Box::new);
Some(remaining)
}

fn first_independent_forward_result_sibling(
ability: Option<&ResolvedAbility>,
) -> Option<ResolvedAbility> {
let mut current = ability;
while let Some(sibling) = current {
if sibling.sub_link == SubAbilityLink::SequentialSibling {
return without_missing_forward_result_dependencies(sibling);
}
current = sibling.sub_ability.as_deref();
}
None
}

fn effect_depends_on_missing_chosen_player(ability: &ResolvedAbility) -> bool {
ability
.effect
Expand Down
Loading
Loading