Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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