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
7 changes: 7 additions & 0 deletions crates/engine/src/game/ability_rw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2158,6 +2158,9 @@ fn legacy_object_scope(s: &ObjectScope) -> bool {
// Source-persistent exile-pile member read (not one of the retained
// legacy refs), mirroring the OtherRevealedCard precedent.
| ObjectScope::OwnedLinkedExileCard
// CR 120.1: the per-iteration batch source is resolution-local, not one
// of the retained legacy refs (mirrors EventTarget).
| ObjectScope::BatchSource
| ObjectScope::EventTarget => false,
}
}
Expand Down Expand Up @@ -3646,6 +3649,10 @@ fn read_object_scope(scope: &ObjectScope, kind: StateKind) -> RwProfile {
// `ObjectScope::Recipient` and the `LastRevealed => empty` classification;
// contributes no observable `reads_board`/`reads_src`.
ObjectScope::OtherRevealedCard => RwProfile::empty(),
// CR 120.1 + CR 208.3: the per-iteration batch source reads the batch
// member's live power — a mutable board characteristic (mirrors
// `Target`/`Anaphoric`/`Demonstrative`).
ObjectScope::BatchSource => reads_board_of(kind),
// D5 carrier: `CostPaidObject` is one of the 12 retained refs.
ObjectScope::CostPaidObject => legacy_ref(),
}
Expand Down
3 changes: 3 additions & 0 deletions crates/engine/src/game/ability_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3115,6 +3115,9 @@ fn scan_object_scope(x: &ObjectScope) -> Axes {
// CR 607.2a: source-persistent exile-pile member read — no event/sibling
// projected axis (mirrors AmassedArmy).
ObjectScope::OwnedLinkedExileCard => Axes::NONE,
// CR 120.1: per-iteration batch source — a resolution-filtered object
// with no event/sibling axis (mirrors Source/Target).
ObjectScope::BatchSource => Axes::NONE,
ObjectScope::EventTarget => Axes {
event: true,
sibling: false,
Expand Down
15 changes: 15 additions & 0 deletions crates/engine/src/game/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,7 @@ fn fmt_quantity_ref(qty: &QuantityRef) -> String {
ObjectScope::OtherRevealedCard => "other revealed card",
ObjectScope::OwnedLinkedExileCard => "owned linked-exiled card",
ObjectScope::AmassedArmy => "amassed Army",
ObjectScope::BatchSource => "batch source",
};
match counter_type {
Some(ct) => format!("{} counters on {scope_str}", ct.as_str()),
Expand Down Expand Up @@ -1384,6 +1385,7 @@ fn fmt_quantity_ref(qty: &QuantityRef) -> String {
ObjectScope::OtherRevealedCard => "other revealed card's power".into(),
ObjectScope::OwnedLinkedExileCard => "owned linked-exiled card's power".into(),
ObjectScope::AmassedArmy => "amassed Army's power".into(),
ObjectScope::BatchSource => "batch source's power".into(),
},
QuantityRef::Toughness { scope } => match scope {
ObjectScope::Source | ObjectScope::Anaphoric | ObjectScope::Demonstrative => {
Expand All @@ -1397,6 +1399,7 @@ fn fmt_quantity_ref(qty: &QuantityRef) -> String {
ObjectScope::OtherRevealedCard => "other revealed card's toughness".into(),
ObjectScope::OwnedLinkedExileCard => "owned linked-exiled card's toughness".into(),
ObjectScope::AmassedArmy => "amassed Army's toughness".into(),
ObjectScope::BatchSource => "batch source's toughness".into(),
},
QuantityRef::ObjectManaValue { scope } => match scope {
ObjectScope::Source | ObjectScope::Anaphoric | ObjectScope::Demonstrative => {
Expand All @@ -1410,6 +1413,7 @@ fn fmt_quantity_ref(qty: &QuantityRef) -> String {
ObjectScope::OtherRevealedCard => "other revealed card's mana value".into(),
ObjectScope::OwnedLinkedExileCard => "owned linked-exiled card's mana value".into(),
ObjectScope::AmassedArmy => "amassed Army's mana value".into(),
ObjectScope::BatchSource => "batch source's mana value".into(),
},
QuantityRef::TargetObjectManaValue { .. } => "target object's mana value".into(),
QuantityRef::ObjectColorCount { scope } => match scope {
Expand All @@ -1424,6 +1428,7 @@ fn fmt_quantity_ref(qty: &QuantityRef) -> String {
ObjectScope::OtherRevealedCard => "other revealed card's colors".into(),
ObjectScope::OwnedLinkedExileCard => "owned linked-exiled card's colors".into(),
ObjectScope::AmassedArmy => "amassed Army's colors".into(),
ObjectScope::BatchSource => "batch source's colors".into(),
},
QuantityRef::ObjectTypelineComponentCount { scope } => match scope {
ObjectScope::Source | ObjectScope::Anaphoric | ObjectScope::Demonstrative => {
Expand All @@ -1439,6 +1444,7 @@ fn fmt_quantity_ref(qty: &QuantityRef) -> String {
"typeline components on owned linked-exiled card".into()
}
ObjectScope::AmassedArmy => "typeline components on amassed Army".into(),
ObjectScope::BatchSource => "typeline components on batch source".into(),
},
QuantityRef::ObjectNameWordCount { scope } => match scope {
ObjectScope::Source | ObjectScope::Anaphoric | ObjectScope::Demonstrative => {
Expand All @@ -1452,6 +1458,7 @@ fn fmt_quantity_ref(qty: &QuantityRef) -> String {
ObjectScope::OtherRevealedCard => "words in other revealed card's name".into(),
ObjectScope::OwnedLinkedExileCard => "words in owned linked-exiled card's name".into(),
ObjectScope::AmassedArmy => "words in amassed Army's name".into(),
ObjectScope::BatchSource => "words in batch source's name".into(),
},
QuantityRef::ManaSymbolsInManaCost { scope, color } => {
let scope_str = match scope {
Expand All @@ -1464,6 +1471,7 @@ fn fmt_quantity_ref(qty: &QuantityRef) -> String {
ObjectScope::OtherRevealedCard => "other revealed card",
ObjectScope::OwnedLinkedExileCard => "owned linked-exiled card",
ObjectScope::AmassedArmy => "amassed Army",
ObjectScope::BatchSource => "batch source",
};
match color {
Some(c) => format!("{c:?} mana symbols in {scope_str}'s mana cost"),
Expand Down Expand Up @@ -7945,6 +7953,7 @@ fn quantity_ref_feature(qref: &QuantityRef) -> (&'static str, FeatureSupport) {
ObjectScope::OtherRevealedCard => ("OtherRevealedCardPower", Handled),
ObjectScope::OwnedLinkedExileCard => ("OwnedLinkedExileCardPower", Handled),
ObjectScope::AmassedArmy => ("AmassedArmyPower", Handled),
ObjectScope::BatchSource => ("BatchSourcePower", Handled),
},
QuantityRef::Toughness { scope } => match scope {
ObjectScope::Source | ObjectScope::Anaphoric | ObjectScope::Demonstrative => {
Expand All @@ -7958,6 +7967,7 @@ fn quantity_ref_feature(qref: &QuantityRef) -> (&'static str, FeatureSupport) {
ObjectScope::OtherRevealedCard => ("OtherRevealedCardToughness", Handled),
ObjectScope::OwnedLinkedExileCard => ("OwnedLinkedExileCardToughness", Handled),
ObjectScope::AmassedArmy => ("AmassedArmyToughness", Handled),
ObjectScope::BatchSource => ("BatchSourceToughness", Handled),
},
QuantityRef::ObjectManaValue { scope } => match scope {
ObjectScope::Source | ObjectScope::Anaphoric | ObjectScope::Demonstrative => {
Expand All @@ -7971,6 +7981,7 @@ fn quantity_ref_feature(qref: &QuantityRef) -> (&'static str, FeatureSupport) {
ObjectScope::OtherRevealedCard => ("OtherRevealedCardManaValue", Handled),
ObjectScope::OwnedLinkedExileCard => ("OwnedLinkedExileCardManaValue", Handled),
ObjectScope::AmassedArmy => ("AmassedArmyManaValue", Handled),
ObjectScope::BatchSource => ("BatchSourceManaValue", Handled),
},
QuantityRef::TargetObjectManaValue { .. } => ("TargetObjectManaValue", Handled),
QuantityRef::ObjectColorCount { scope } => match scope {
Expand All @@ -7985,6 +7996,7 @@ fn quantity_ref_feature(qref: &QuantityRef) -> (&'static str, FeatureSupport) {
ObjectScope::OtherRevealedCard => ("OtherRevealedCardColorCount", Handled),
ObjectScope::OwnedLinkedExileCard => ("OwnedLinkedExileCardColorCount", Handled),
ObjectScope::AmassedArmy => ("AmassedArmyObjectColorCount", Handled),
ObjectScope::BatchSource => ("BatchSourceObjectColorCount", Handled),
},
QuantityRef::ObjectNameWordCount { scope } => match scope {
ObjectScope::Source | ObjectScope::Anaphoric | ObjectScope::Demonstrative => {
Expand All @@ -7998,6 +8010,7 @@ fn quantity_ref_feature(qref: &QuantityRef) -> (&'static str, FeatureSupport) {
ObjectScope::OtherRevealedCard => ("OtherRevealedCardNameWordCount", Handled),
ObjectScope::OwnedLinkedExileCard => ("OwnedLinkedExileCardNameWordCount", Handled),
ObjectScope::AmassedArmy => ("AmassedArmyObjectNameWordCount", Handled),
ObjectScope::BatchSource => ("BatchSourceObjectNameWordCount", Handled),
},
QuantityRef::ObjectTypelineComponentCount { scope } => match scope {
ObjectScope::Source | ObjectScope::Anaphoric | ObjectScope::Demonstrative => {
Expand All @@ -8013,6 +8026,7 @@ fn quantity_ref_feature(qref: &QuantityRef) -> (&'static str, FeatureSupport) {
("OwnedLinkedExileCardTypelineComponentCount", Handled)
}
ObjectScope::AmassedArmy => ("AmassedArmyObjectTypelineComponentCount", Handled),
ObjectScope::BatchSource => ("BatchSourceObjectTypelineComponentCount", Handled),
},
QuantityRef::ManaSymbolsInManaCost { scope, .. } => match scope {
ObjectScope::Source | ObjectScope::Anaphoric | ObjectScope::Demonstrative => {
Expand All @@ -8028,6 +8042,7 @@ fn quantity_ref_feature(qref: &QuantityRef) -> (&'static str, FeatureSupport) {
("OwnedLinkedExileCardManaSymbolsInManaCost", Handled)
}
ObjectScope::AmassedArmy => ("AmassedArmyManaSymbolsInManaCost", Handled),
ObjectScope::BatchSource => ("BatchSourceManaSymbolsInManaCost", Handled),
},
QuantityRef::SelfManaValue => ("SelfManaValue", Handled),
QuantityRef::Aggregate { .. } => ("Aggregate", Handled),
Expand Down
68 changes: 47 additions & 21 deletions crates/engine/src/game/effects/deal_damage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2486,8 +2486,19 @@ pub fn resolve_each_source_deals_damage(
}
};

// CR 608.2: the amount is uniform across every source — resolve it once.
let amt = resolve_quantity_with_targets(state, amount, ability).max(0) as u32;
// CR 608.2: the amount is uniform across every source — resolve it once —
// UNLESS the amount reads the per-source `BatchSource` scope (CR 120.1:
// "deals damage equal to its power" resolves per source object, never
// against the ability source).
let per_source = crate::game::quantity::quantity_expr_contains_scope(
amount,
crate::types::ability::ObjectScope::BatchSource,
);
let amt_uniform = if per_source {
0
} else {
resolve_quantity_with_targets(state, amount, ability).max(0) as u32
};

// CR 608.2 + CR 120.1: evaluate the source class against the battlefield at
// resolution (mirrors `resolve_all`). Each matching object is an independent
Expand Down Expand Up @@ -2516,27 +2527,42 @@ pub fn resolve_each_source_deals_damage(
// recipient so combined lethal/excess is computed once all sources have
// marked). Each source carries its OWN `DamageContext` (CR 120.1 identity).
let mut entries: Vec<(ObjectId, DamageContext, TargetRef, u32)> = Vec::new();
if amt > 0 {
for &source_id in &source_ids {
let ctx = DamageContext::from_source(state, source_id)
.unwrap_or_else(|| DamageContext::fallback(source_id, ability.controller));
match recipient {
EachDamageRecipient::Shared(_) => {
for recip in &shared_recipients {
entries.push((source_id, ctx, recip.clone(), amt));
}
}
// CR 109.4 + CR 120.3a: each source deals to the player that
// controls it.
EachDamageRecipient::EachController => {
let controller = state
.objects
.get(&source_id)
.map(|obj| obj.controller)
.unwrap_or(ctx.controller);
entries.push((source_id, ctx, TargetRef::Player(controller), amt));
for &source_id in &source_ids {
// CR 120.1 + CR 608.2h: each batch member deals its OWN characteristic;
// resolved per source (live object, LKI fallback via the new scope's
// resolve arms — the same instant semantics as the one-time uniform
// resolution, but read against each batch member). Zero/sourceless
// members deal nothing (skip) — entry-set-equivalent to the uniform
// path's `if amt > 0` gate.
let amt = if per_source {
crate::game::quantity::resolve_quantity_with_targets_and_damage_source(
state, amount, ability, source_id,
)
.max(0) as u32
} else {
amt_uniform
};
if amt == 0 {
continue;
}
let ctx = DamageContext::from_source(state, source_id)
.unwrap_or_else(|| DamageContext::fallback(source_id, ability.controller));
match recipient {
EachDamageRecipient::Shared(_) => {
for recip in &shared_recipients {
entries.push((source_id, ctx, recip.clone(), amt));
}
}
// CR 109.4 + CR 120.3a: each source deals to the player that
// controls it.
EachDamageRecipient::EachController => {
let controller = state
.objects
.get(&source_id)
.map(|obj| obj.controller)
.unwrap_or(ctx.controller);
entries.push((source_id, ctx, TargetRef::Player(controller), amt));
}
}
}

Expand Down
6 changes: 4 additions & 2 deletions crates/engine/src/game/effects/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11738,7 +11738,8 @@ pub(crate) fn evaluate_condition(
| crate::types::ability::ObjectScope::OtherRevealedCard
| crate::types::ability::ObjectScope::OwnedLinkedExileCard
| crate::types::ability::ObjectScope::EventTarget
| crate::types::ability::ObjectScope::AmassedArmy => false,
| crate::types::ability::ObjectScope::AmassedArmy
| crate::types::ability::ObjectScope::BatchSource => false,
},
AbilityCondition::AlternativeManaCostPaid => ability.context.alternative_mana_cost_paid,
AbilityCondition::EffectOutcome {
Expand Down Expand Up @@ -11964,7 +11965,8 @@ pub(crate) fn evaluate_condition(
| crate::types::ability::ObjectScope::OtherRevealedCard
| crate::types::ability::ObjectScope::OwnedLinkedExileCard
| crate::types::ability::ObjectScope::EventTarget
| crate::types::ability::ObjectScope::AmassedArmy => None,
| crate::types::ability::ObjectScope::AmassedArmy
| crate::types::ability::ObjectScope::BatchSource => None,
};
object_id
.and_then(|id| state.objects.get(&id))
Expand Down
4 changes: 3 additions & 1 deletion crates/engine/src/game/layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,7 @@ fn evaluate_condition_with_context(
trigger_source: None,
recipient: recipient_id,
scoped_player: None,
damage_source: None,
},
)
};
Expand Down Expand Up @@ -1683,7 +1684,8 @@ fn evaluate_condition_with_context(
| crate::types::ability::ObjectScope::OtherRevealedCard
| crate::types::ability::ObjectScope::OwnedLinkedExileCard
| crate::types::ability::ObjectScope::Demonstrative
| crate::types::ability::ObjectScope::AmassedArmy => false,
| crate::types::ability::ObjectScope::AmassedArmy
| crate::types::ability::ObjectScope::BatchSource => false,
},
// CR 702.171b + CR 110.5d: off-battlefield permanents have no saddled designation.
StaticCondition::SourceIsSaddled => state.objects.get(&source_id).is_some_and(|obj| {
Expand Down
Loading
Loading