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
4 changes: 4 additions & 0 deletions crates/engine/src/game/effects/cloak.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ pub fn resolve(
_ => return Err(EffectError::MissingParam("count".to_string())),
};

// CR 608.2c: this instruction owns the chain's referent slot from here on,
// including the arms where it produces nothing.
crate::game::morph::begin_face_down_referent_production(state);

let player = super::resolve_player_for_context_ref(state, ability, &target);
// CR 110.2a: resolve the cloaking-player override through the single
// canonical authority shared with ChangeZone/ChangeZoneAll/Manifest.
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/game/effects/discard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub(crate) fn complete_discard_to_graveyard(
controller_override: None,
enter_transformed: false,
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
enter_as_copy: None,
discard_frame,
applied,
Expand Down
4 changes: 4 additions & 0 deletions crates/engine/src/game/effects/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ pub fn resolve(
_ => return Err(EffectError::MissingParam("count".to_string())),
};

// CR 608.2c: this instruction owns the chain's referent slot from here on,
// including the arms where it produces nothing.
crate::game::morph::begin_face_down_referent_production(state);

// `player` is the LIBRARY OWNER (whose top cards are manifested), resolved
// from `target`. `controller` is the optional CR 110.2a override for which
// player the cards enter the battlefield under ("under your control").
Expand Down
4 changes: 4 additions & 0 deletions crates/engine/src/game/effects/manifest_dread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ pub fn resolve(
events: &mut Vec<GameEvent>,
) -> Result<(), EffectError> {
let player = ability.controller;
// CR 608.2c: this instruction owns the chain's referent slot from here on,
// including the arm below where the library is empty and it produces
// nothing.
crate::game::morph::begin_face_down_referent_production(state);

let player_state = state
.players
Expand Down
2 changes: 2 additions & 0 deletions crates/engine/src/game/elimination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,7 @@ mod tests {
controller_override: None,
enter_with_counters: Vec::new(),
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
attach_to: None,
library_placement: None,
exile_duration: None,
Expand All @@ -1663,6 +1664,7 @@ mod tests {
controller_override: None,
enter_with_counters: Vec::new(),
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
attach_to: None,
library_placement: None,
exile_duration: None,
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/game/engine_debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,7 @@ pub fn route_debug_create_to_battlefield(
controller_override: None,
enter_transformed: false,
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
enter_as_copy: None,
discard_frame: None,
applied: HashSet::new(),
Expand Down
5 changes: 5 additions & 0 deletions crates/engine/src/game/engine_replacement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4411,6 +4411,7 @@ mod tests {
discard_frame: None,
applied: std::collections::HashSet::new(),
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
};
let mut events = Vec::new();
crate::game::sacrifice::apply_sacrifice_after_replacement(&mut state, event, &mut events);
Expand Down Expand Up @@ -6330,6 +6331,7 @@ mod tests {
discard_frame: None,
applied: std::collections::HashSet::new(),
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
};
let result = replacement_mod::replace_event(&mut state, proposed, &mut events);
let ReplacementResult::NeedsChoice(player) = result else {
Expand Down Expand Up @@ -6536,6 +6538,7 @@ mod tests {
discard_frame: None,
applied: std::collections::HashSet::new(),
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
};
let result = replacement_mod::replace_event(&mut state, proposed, &mut events);
let ReplacementResult::NeedsChoice(player) = result else {
Expand Down Expand Up @@ -6658,6 +6661,7 @@ mod tests {
discard_frame: None,
applied: std::collections::HashSet::new(),
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
};
let result = replacement_mod::replace_event(&mut state, proposed, &mut events);
let ReplacementResult::NeedsChoice(player) = result else {
Expand Down Expand Up @@ -7115,6 +7119,7 @@ mod tests {
discard_frame: None,
applied: std::collections::HashSet::new(),
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
};
let result = replacement_mod::replace_event(&mut state, proposed, &mut events);
let ReplacementResult::Execute(event) = result else {
Expand Down
5 changes: 4 additions & 1 deletion crates/engine/src/game/engine_resolution_choices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1830,7 +1830,10 @@ pub(super) fn handle_resolution_choice(
Zone::Battlefield,
source_id,
)
.face_down(face_down),
// CR 608.2c + CR 701.62a: the same producer as `manifest_card`,
// reached through the two-card choice instead of synchronously.
.face_down(face_down)
.publishing_chain_referent(),
events,
) {
crate::game::zone_pipeline::ZoneMoveResult::Done => {}
Expand Down
49 changes: 48 additions & 1 deletion crates/engine/src/game/morph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,19 +547,66 @@ pub fn manifest_card(
// control instead of the library owner's (Cybership routes the damaged
// player's cards under the Cybership controller). The move is attributed to
// `source_id` (the manifesting spell/ability), not the moved object.
// CR 608.2c: manifest (CR 701.40a), manifest dread (CR 701.62a) and cloak
// (CR 701.58a) all reach the battlefield through this one request, and they
// are exactly the three effects the parser admits as chain-referent
// producers (`oracle_effect::lower::publishes_chain_created_referent`). The
// mark lives here, on the producer, rather than in a list of causes or call
// sites that a fourth face-down delivery could silently join.
let mut request =
super::zone_pipeline::ZoneMoveRequest::effect(object_id, Zone::Battlefield, source_id)
.face_down(profile);
.face_down(profile)
.publishing_chain_referent();
if let Some(controller) = controller {
request = request.under_control_of(controller);
}
match super::zone_pipeline::move_object(state, request, events) {
// Both arms are silent about the chain referent: the publish lives in
// `zone_pipeline::apply_face_down_entry_profile`, which the synchronous
// delivery and the CR 616.1 resume both run.
super::zone_pipeline::ZoneMoveResult::Done => Ok(()),
super::zone_pipeline::ZoneMoveResult::NeedsChoice(_)
| super::zone_pipeline::ZoneMoveResult::NeedsAuraAttachmentChoice => Ok(()),
}
}

/// CR 608.2c: Record a face-down entrant as the chain's most-recent created
/// referent, so a following "it" / "that creature" anaphor
/// (`TargetFilter::LastCreated`) binds to it — "manifest dread, then attach
/// this Equipment to that creature" (Conductive Machete, #7531).
///
/// This writes the SAME slot the token producer writes
/// (`engine_replacement.rs`, `state.last_created_token_ids`). The slot's name
/// says "token", but its job is the anaphor's referent, not a claim about
/// token-ness: CR 608.2c binds a demonstrative to the thing the previous
/// instruction produced, and a manifested card and a created token are the same
/// thing to that binding. The parser side keys on the same equivalence
/// (`publishes_chain_created_referent`).
///
/// Assignment, not append — mirroring the token producer. The slot names the
/// MOST RECENT producer, so a chain that manifests twice leaves the second
/// entrant as the referent, which is what "that creature" means after it.
pub(crate) fn publish_face_down_entry_referent(state: &mut GameState, object_id: ObjectId) {
state.last_created_token_ids = vec![object_id];
}

/// CR 608.2c: a chain-referent producer that is about to run clears the slot.
///
/// Without this, a producer that ends up producing NOTHING — manifest dread on
/// an empty library — would leave an EARLIER instruction's referent in place,
/// and the following demonstrative would bind to it. "Manifest dread, then
/// attach this Equipment to that creature" would equip a creature the sentence
/// never mentioned.
///
/// Mirrors the token producer, which assigns `last_created_token_ids =
/// created_ids` at its tail whether or not the count was zero. The manifest
/// family cannot assign at its tail — a CR 616.1 entry pause can park the
/// delivery past the end of the resolver — so the two halves are split: the
/// producer clears up front, the successful delivery publishes.
pub(crate) fn begin_face_down_referent_production(state: &mut GameState) {
state.last_created_token_ids.clear();
}

/// Find the object id of the top card of `player`'s library, if any.
pub(crate) fn top_library_object(
state: &GameState,
Expand Down
6 changes: 6 additions & 0 deletions crates/engine/src/game/replacement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,7 @@ fn discard_applier(
controller_override: None,
enter_transformed: false,
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
enter_as_copy: None,
discard_frame,
applied,
Expand Down Expand Up @@ -11790,6 +11791,7 @@ mod tests {
discard_frame: None,
applied: HashSet::new(),
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
};
let result = replace_event(&mut state, proposed, &mut events);
let ReplacementResult::Execute(event) = result else {
Expand Down Expand Up @@ -14019,6 +14021,7 @@ mod tests {
discard_frame: None,
applied: HashSet::new(),
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
};

let result = replace_event(&mut state, proposed.clone(), &mut events);
Expand Down Expand Up @@ -15238,6 +15241,7 @@ mod tests {
discard_frame: None,
applied: HashSet::new(),
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
};

let replaced = apply_single_replacement(
Expand Down Expand Up @@ -18397,6 +18401,7 @@ mod tests {
enter_transformed: false,
enter_as_copy: None,
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
discard_frame: None,
applied: HashSet::new(),
};
Expand Down Expand Up @@ -18444,6 +18449,7 @@ mod tests {
enter_transformed: false,
enter_as_copy: None,
face_down_profile: None,
chain_referent: crate::types::zones::ChainReferentIntent::Silent,
discard_frame: None,
applied: HashSet::new(),
};
Expand Down
Loading
Loading