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
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("WaitingFor handler parity", () => {
it("registers both interactive meld waiting states", () => {
expect(HANDLED_WAITING_FOR_TYPES.has("MeldPairChoice")).toBe(true);
expect(HANDLED_WAITING_FOR_TYPES.has("MeldAttackTargetChoice")).toBe(true);
expect(HANDLED_WAITING_FOR_TYPES.has("EntryAttackTargetChoice")).toBe(true);
});

it("every engine WaitingFor variant has a frontend UI handler", () => {
Expand Down
1 change: 1 addition & 0 deletions client/src/adapter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,7 @@ export type WaitingFor =
| { type: "Priority"; data: { player: PlayerId } }
| { type: "MeldPairChoice"; data: { player: PlayerId; choices: MeldSelection[] } }
| { type: "MeldAttackTargetChoice"; data: { player: PlayerId; context: MeldSelection; valid_targets: AttackTarget[] } }
| { type: "EntryAttackTargetChoice"; data: { player: PlayerId; object_id: ObjectId; valid_targets: AttackTarget[] } }
| { type: "ActivationCostOneOfChoice"; data: { player: PlayerId; costs: SerializedAbilityCost[]; pending_cast: PendingCast } }
| {
type: "MulliganDecision";
Expand Down
1 change: 1 addition & 0 deletions client/src/game/waitingForRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const HANDLED_WAITING_FOR_TYPES: ReadonlySet<WaitingFor["type"]> =
// CR 701.42 / CR 508.4: meld pair and attacking-entry destination dialogs.
"MeldPairChoice",
"MeldAttackTargetChoice",
"EntryAttackTargetChoice",
// Cast / activation chain — ManaPayment + PhyrexianPayment share ManaPaymentUI.
...MANA_PAYMENT_WAITING_FOR_TYPES,
"ManaSourceSelection",
Expand Down
5 changes: 4 additions & 1 deletion client/src/pages/GamePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3669,7 +3669,10 @@ function MeldChoiceModal() {
);
}

if (waitingFor?.type === "MeldAttackTargetChoice") {
if (
waitingFor?.type === "MeldAttackTargetChoice" ||
waitingFor?.type === "EntryAttackTargetChoice"
) {
const targets = waitingFor.data.valid_targets;
return (
<ChoiceModal
Expand Down
11 changes: 8 additions & 3 deletions crates/engine/src/ai_support/candidates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,11 @@ pub fn candidate_actions_exact(state: &GameState) -> Vec<CandidateAction> {
player,
valid_targets,
..
}
| WaitingFor::EntryAttackTargetChoice {
player,
valid_targets,
..
} => valid_targets
.iter()
.map(|target| {
Expand Down Expand Up @@ -840,9 +845,9 @@ pub fn candidate_actions_broad_with_probe(
probe: Option<&casting::PriorityCastProbe>,
) -> Vec<CandidateAction> {
let actions = match &state.waiting_for {
WaitingFor::MeldPairChoice { .. } | WaitingFor::MeldAttackTargetChoice { .. } => {
candidate_actions_exact(state)
}
WaitingFor::MeldPairChoice { .. }
| WaitingFor::MeldAttackTargetChoice { .. }
| WaitingFor::EntryAttackTargetChoice { .. } => candidate_actions_exact(state),
WaitingFor::Priority { player } => priority_actions_with_probe(state, *player, probe),
WaitingFor::ChooseAnnouncingOpponent {
player, candidates, ..
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/database/hideaway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ fn hideaway_trigger(n: u32) -> TriggerDefinition {
// CR 701.20e: the cards are looked at privately, not revealed.
reveal: false,
enter_tapped: false,
enters_attacking: false,
source: DigSource::Library,
},
)
Expand Down
2 changes: 2 additions & 0 deletions crates/engine/src/game/ability_rw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4598,6 +4598,7 @@ fn rw_effect(
enter_with_counters,
enters_under: _,
enter_tapped: _,
enters_attacking: _,
face_down_profile: _,
library_position: _,
random_order: _,
Expand Down Expand Up @@ -4686,6 +4687,7 @@ fn rw_effect(
rest_order: _,
reveal: _,
enter_tapped: _,
enters_attacking: _,
source: _,
} => {
let mut p = ext_write(StateKind::SetMembership);
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/game/ability_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,7 @@ fn scan_effect(x: &Effect, mode: ScanMode) -> Axes {
rest_order: _,
reveal: _,
enter_tapped: _,
enters_attacking: _,
source: _,
keep_count_expr,
} => {
Expand Down
3 changes: 3 additions & 0 deletions crates/engine/src/game/ability_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8711,6 +8711,7 @@ mod tests {
},
enters_under: None,
enter_tapped: crate::types::zones::EtbTapState::Unspecified,
enters_attacking: false,
enter_with_counters: vec![],
face_down_profile: None,
library_position: None,
Expand Down Expand Up @@ -13340,6 +13341,7 @@ mod tests {
target: TargetFilter::Player,
enters_under: None,
enter_tapped: crate::types::zones::EtbTapState::Unspecified,
enters_attacking: false,
enter_with_counters: vec![],
face_down_profile: None,
library_position: None,
Expand Down Expand Up @@ -13378,6 +13380,7 @@ mod tests {
target: TargetFilter::Player,
enters_under: None,
enter_tapped: crate::types::zones::EtbTapState::Unspecified,
enters_attacking: false,
enter_with_counters: vec![],
face_down_profile: None,
library_position: None,
Expand Down
45 changes: 45 additions & 0 deletions crates/engine/src/game/combat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,18 @@ pub fn enter_attacking(
push_attacker_and_journal(state, object_id, defending_player, attack_target);
}

/// CR 508.4: Seat a creature that entered the battlefield attacking against an
/// explicitly chosen legal defender. Unlike Ninjutsu and Sneak, this does not
/// tap the creature: entering attacking alone is not a declaration.
pub fn enter_attacking_at_target(
state: &mut GameState,
object_id: ObjectId,
defending_player: PlayerId,
attack_target: AttackTarget,
) {
push_attacker_and_journal(state, object_id, defending_player, attack_target);
}

/// CR 508.4 + CR 733: seat `object_id` as an attacking creature against an
/// already-decided defender and journal the settled pair.
///
Expand Down Expand Up @@ -648,6 +660,39 @@ pub fn place_attacking_alongside(
push_attacker_and_journal(state, object_id, defending_player, attack_target);
}

/// CR 508.4a: seat an entering creature against its sole legal defender, or
/// park the controller's required destination choice when several are legal.
/// Returns the chooser only when resolution must pause.
pub fn choose_entry_attack_target_or_enter(
state: &mut GameState,
object_id: ObjectId,
controller: PlayerId,
) -> Option<PlayerId> {
let valid_targets = valid_entry_attack_targets(
state,
controller,
&crate::types::ability::EntryAttackDestination::AnyDefender,
);
match valid_targets.as_slice() {
[] => None,
[target] => {
if let Some(defending_player) = entry_attack_target_defender(state, controller, *target)
{
enter_attacking_at_target(state, object_id, defending_player, *target);
}
None
}
_ => {
state.waiting_for = crate::types::game_state::WaitingFor::EntryAttackTargetChoice {
player: controller,
object_id,
valid_targets,
};
Some(controller)
}
}
}

/// CR 509.1g + CR 506.3e + CR 509.1h: Put a permanent onto the battlefield as a
/// blocking creature for `attacker_id`. Used by effects that create or place a
/// creature already "blocking that creature" (Mirror Match's copy tokens).
Expand Down
4 changes: 4 additions & 0 deletions crates/engine/src/game/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2802,6 +2802,7 @@ fn effect_details(effect: &Effect) -> Vec<(String, String)> {
target,
enters_under,
enter_tapped,
enters_attacking,
enter_with_counters,
face_down_profile,
library_position,
Expand All @@ -2821,6 +2822,9 @@ fn effect_details(effect: &Effect) -> Vec<(String, String)> {
if !matches!(enter_tapped, EtbTapState::Unspecified) {
d.push(("enter_tapped".into(), format!("{enter_tapped:?}")));
}
if *enters_attacking {
d.push(("enters_attacking".into(), "true".into()));
}
if !enter_with_counters.is_empty() {
d.push((
"enter_with_counters".into(),
Expand Down
Loading
Loading