Skip to main content

manabrew_engine/ability/effects/
venture_effect.rs

1//! Venture effect — venture into the dungeon.
2//!
3//! Ported from Java's `VentureEffect.java`.
4//! Venture into the dungeon: If you're not in a dungeon, choose one.
5//! Move to the next room and trigger its room ability.
6//!
7//! Dungeon state is tracked via svars on a dungeon card in the Command zone:
8//! - "CurrentRoom" → room name (empty = not started)
9//! - "DungeonName" → dungeon identifier
10//! The three standard dungeons are:
11//! - Dungeon of the Mad Mage (7 rooms)
12//! - Lost Mine of Phandelver (4 rooms + branches)
13//! - Tomb of Annihilation (4 rooms + branches)
14//! Plus Undercity from Baldur's Gate.
15
16use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
17
18use super::EffectContext;
19use crate::card::Card;
20use crate::event::RunParams;
21use crate::ids::{CardId, PlayerId};
22use crate::spellability::SpellAbility;
23use crate::trigger::TriggerType;
24
25/// Standard dungeon definitions: (name, rooms in order, last room name)
26/// For dungeons with branches, we list the linear path (agent auto-chooses).
27const DUNGEONS: &[(&str, &[&str])] = &[
28    (
29        "Dungeon of the Mad Mage",
30        &[
31            "Yawning Portal",
32            "Dungeon Level",
33            "Goblin Bazaar",
34            "Twisted Caverns",
35            "Lost Level",
36            "Runestone Caverns",
37            "Mad Wizard's Lair",
38        ],
39    ),
40    (
41        "Lost Mine of Phandelver",
42        &[
43            "Cave Entrance",
44            "Goblin Lair",
45            "Mine Tunnels",
46            "Temple of Dumathoin",
47        ],
48    ),
49    (
50        "Tomb of Annihilation",
51        &[
52            "Trapped Entry",
53            "Veils of Fear",
54            "Sandfall Cell",
55            "Cradle of the Death God",
56        ],
57    ),
58    (
59        "Undercity",
60        &[
61            "Secret Entrance",
62            "Forge",
63            "Lost Well",
64            "Throne of the Dead Three",
65        ],
66    ),
67];
68
69/// Struct form of this effect so it can participate in the
70/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
71/// `VentureEffect` class extending `SpellAbilityEffect`.
72#[manabrew_engine_macros::spell_effect(VentureEffect)]
73fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
74    let controller = sa.activating_player;
75    let players = if let Some(def) = sa.defined_player() {
76        super::resolve_defined_players(def, controller, ctx.game)
77    } else if let Some(target_player) = sa.target_chosen.target_player {
78        vec![target_player]
79    } else {
80        vec![controller]
81    };
82
83    for pid in players {
84        if ctx.game.player(pid).has_lost {
85            continue;
86        }
87        venture_into_dungeon(ctx, sa, pid);
88    }
89}
90
91fn venture_into_dungeon(ctx: &mut EffectContext, sa: &SpellAbility, player: PlayerId) {
92    // Step 1: Find current dungeon in command zone
93    let current_dungeon = find_current_dungeon(ctx, player);
94
95    let dungeon_id = match current_dungeon {
96        Some((id, room, dungeon_name)) => {
97            // Check if dungeon is in last room — if so, complete it and start new
98            let rooms = get_dungeon_rooms(&dungeon_name);
99            if let Some(rooms) = rooms {
100                if !room.is_empty() && rooms.last().is_some_and(|last| *last == room) {
101                    // Complete the dungeon
102                    complete_dungeon(ctx, player, id);
103                    // Start a new dungeon
104                    create_dungeon(ctx, sa, player)
105                } else {
106                    // Continue current dungeon
107                    id
108                }
109            } else {
110                id
111            }
112        }
113        None => {
114            // No active dungeon — create one
115            create_dungeon(ctx, sa, player)
116        }
117    };
118
119    // Step 2: Advance to next room
120    let dungeon_name = ctx
121        .game
122        .card(dungeon_id)
123        .svars
124        .get("DungeonName")
125        .cloned()
126        .unwrap_or_default();
127    let current_room = ctx
128        .game
129        .card(dungeon_id)
130        .svars
131        .get("CurrentRoom")
132        .cloned()
133        .unwrap_or_default();
134
135    let rooms = get_dungeon_rooms(&dungeon_name);
136    let next_room = if let Some(rooms) = rooms {
137        if current_room.is_empty() {
138            // Enter first room
139            rooms.first().map(|s| s.to_string())
140        } else {
141            // Find current room index and advance
142            let idx = rooms.iter().position(|r| *r == current_room);
143            match idx {
144                Some(i) if i + 1 < rooms.len() => Some(rooms[i + 1].to_string()),
145                _ => None, // Already in last room
146            }
147        }
148    } else {
149        None
150    };
151
152    if let Some(next) = next_room {
153        ctx.game
154            .card_mut(dungeon_id)
155            .svars
156            .insert("CurrentRoom".to_string(), next.clone());
157
158        // Fire RoomEntered trigger
159        ctx.trigger_handler.run_trigger(
160            TriggerType::RoomEntered,
161            RunParams {
162                card: Some(dungeon_id),
163                player: Some(player),
164                room_name: Some(next),
165                ..Default::default()
166            },
167            false,
168        );
169    }
170}
171
172fn find_current_dungeon(ctx: &EffectContext, player: PlayerId) -> Option<(CardId, String, String)> {
173    ctx.game
174        .cards
175        .iter()
176        .find(|c| {
177            c.zone == ZoneType::Command && c.owner == player && c.svars.contains_key("DungeonName")
178        })
179        .map(|c| {
180            let room = c.svars.get("CurrentRoom").cloned().unwrap_or_default();
181            let name = c.svars.get("DungeonName").cloned().unwrap_or_default();
182            (c.id, room, name)
183        })
184}
185
186fn create_dungeon(ctx: &mut EffectContext, sa: &SpellAbility, player: PlayerId) -> CardId {
187    // Choose dungeon — if sa specifies one, use it; otherwise auto-choose
188    let dungeon_name = sa.ir.dungeon_text.clone().unwrap_or_else(|| {
189        // Auto-choose first dungeon (agent would pick in full implementation)
190        let idx = ctx.rng.next_int(DUNGEONS.len() as i32) as usize % DUNGEONS.len();
191        DUNGEONS[idx].0.to_string()
192    });
193
194    let mut card = Card::new(
195        CardId(0),
196        dungeon_name.clone(),
197        player,
198        CardTypeLine::parse("Dungeon"),
199        ManaCost::parse(""),
200        ColorSet::COLORLESS,
201        None,
202        None,
203        vec![],
204        vec![],
205    );
206    card.set_controller(player);
207    card.set_s_var("DungeonName", dungeon_name);
208    card.set_s_var("CurrentRoom", String::new());
209
210    let id = ctx.game.create_card(card);
211    ctx.move_card(id, ZoneType::Command, player);
212    ctx.trigger_handler.register_active_trigger(ctx.game, id);
213
214    id
215}
216
217fn complete_dungeon(ctx: &mut EffectContext, player: PlayerId, dungeon_id: CardId) {
218    // Move completed dungeon out of command zone
219    let old_zone = ctx.game.card(dungeon_id).zone;
220    ctx.move_card(dungeon_id, ZoneType::Exile, player);
221    super::emit_zone_trigger(ctx.trigger_handler, dungeon_id, old_zone, ZoneType::Exile);
222
223    // Fire dungeon completed trigger
224    ctx.trigger_handler.run_trigger(
225        TriggerType::DungeonCompleted,
226        RunParams {
227            card: Some(dungeon_id),
228            player: Some(player),
229            ..Default::default()
230        },
231        false,
232    );
233}
234
235fn get_dungeon_rooms(name: &str) -> Option<&'static [&'static str]> {
236    DUNGEONS
237        .iter()
238        .find(|(n, _)| *n == name)
239        .map(|(_, rooms)| *rooms)
240}