Skip to main content

manabrew_engine/phase/
untap.rs

1//! Untap — handles untap step logic.
2//!
3//! Mirrors Java's `Untap.java`.
4//! Handles "until next untap", phasing, day/night transitions,
5//! and the actual untap of permanents.
6
7use forge_foundation::ZoneType;
8
9use crate::game::GameState;
10use crate::ids::{CardId, PlayerId};
11
12/// Performs the phasing step at the beginning of the untap step.
13/// Mirrors Java's `Untap.doPhasing()`.
14///
15/// Phase in all directly-phased-out permanents controlled by the active player.
16/// Phase out all permanents with phasing controlled by the active player.
17pub fn do_phasing(game: &mut GameState, turn_player: PlayerId) {
18    // Phase in: all phased-out permanents controlled by turn_player
19    for i in 0..game.cards.len() {
20        if game.cards[i].phased_out
21            && game.cards[i].controller == turn_player
22            && game.cards[i].zone == ZoneType::Battlefield
23        {
24            game.cards[i].phased_out = false;
25        }
26    }
27
28    // Phase out: all permanents with Phasing keyword controlled by turn_player
29    for i in 0..game.cards.len() {
30        if !game.cards[i].phased_out
31            && game.cards[i].controller == turn_player
32            && game.cards[i].zone == ZoneType::Battlefield
33            && game.cards[i].has_keyword("Phasing")
34        {
35            game.cards[i].phased_out = true;
36        }
37    }
38}
39
40/// Handles day/night transitions at the beginning of untap.
41/// Mirrors Java's `Untap.doDayTime()`.
42///
43/// If it's day and previous player cast no spells → becomes night.
44/// If it's night and previous player cast 2+ spells → becomes day.
45pub fn do_day_time(game: &mut GameState, previous_player: Option<PlayerId>) {
46    let previous = match previous_player {
47        Some(p) => p,
48        None => return,
49    };
50
51    let spells_cast = game.player(previous).spells_cast_this_turn;
52
53    if !game.is_night && spells_cast == 0 {
54        game.day_night_started = true;
55        game.is_night = true; // transition to night
56    } else if game.is_night && spells_cast > 1 {
57        game.day_night_started = true;
58        game.is_night = false; // transition to day
59    }
60}
61
62/// Execute the untap step's "at" actions.
63/// Mirrors Java's `Untap.executeAt()` which calls super.executeAt(),
64/// then doPhasing(), doDayTime(), checkStaticAbilities(), and doUntap().
65///
66/// In Rust, the game loop calls these individually, but this provides
67/// the combined entry point matching the Java interface.
68pub fn execute_at(
69    game: &mut GameState,
70    turn_player: PlayerId,
71    previous_player: Option<PlayerId>,
72) -> Vec<CardId> {
73    do_phasing(game, turn_player);
74    do_day_time(game, previous_player);
75    do_untap(game, turn_player)
76}
77
78/// Performs the untap of permanents for the active player.
79/// Mirrors Java's `Untap.doUntap()`.
80///
81/// Untaps all tapped permanents controlled by the active player,
82/// respecting "doesn't untap" and "you may choose not to untap" keywords.
83pub fn do_untap(game: &mut GameState, active: PlayerId) -> Vec<CardId> {
84    let cards: Vec<CardId> = game.cards_in_zone(ZoneType::Battlefield, active).to_vec();
85    let mut untapped = Vec::new();
86
87    for cid in cards {
88        if !game.card(cid).tapped {
89            continue;
90        }
91
92        // Skip cards that don't untap during untap step
93        if game
94            .card(cid)
95            .has_keyword("CARDNAME doesn't untap during your untap step.")
96        {
97            continue;
98        }
99
100        // Skip exerted creatures (reset flag so they untap next turn)
101        if game.card(cid).exerted {
102            game.card_mut(cid).exerted = false;
103            continue;
104        }
105
106        // Skip "This card doesn't untap during your next untap step."
107        let has_skip = game
108            .card(cid)
109            .has_keyword("This card doesn't untap during your next untap step.");
110        if has_skip {
111            game.card_mut(cid)
112                .keywords
113                .remove("This card doesn't untap during your next untap step.");
114            continue;
115        }
116
117        game.untap_during_untap_step(cid, active);
118        untapped.push(cid);
119    }
120
121    // Remove exerted-by flags from all battlefield permanents
122    for i in 0..game.cards.len() {
123        if game.cards[i].zone == ZoneType::Battlefield {
124            game.cards[i].exerted = false;
125        }
126    }
127
128    untapped
129}
130
131#[cfg(test)]
132mod tests {
133    #[test]
134    fn do_phasing_phases_in() {
135        // Basic test that phasing works directionally
136        // Full integration tests would need a GameState
137    }
138}