From 7b927b0df780db947ac92026f347a2a403a5f014 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Fri, 6 Nov 2026 09:32:40 +0100 Subject: [PATCH] Every master of a match gets an own portal beside the dungeon Co-Authored-By: Claude Opus 5.5 --- src/game.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/rival.rs | 20 +------------------- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/game.rs b/src/game.rs index 786bb92..9fe035b 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1597,6 +1597,8 @@ impl Game { continue; } self.carve_master_area(*side, cores[k]); + // every master gets a portal of his own (side 0 has the map's portal) + self.add_master_portal(*side, cores[k]); self.add_master(*side, cores[k]); self.switch_to(*side); let (cx, cy) = cores[k]; @@ -1610,6 +1612,30 @@ impl Game { true } + // a master's own portal right beside his starting area (if there is room) + pub(crate) fn add_master_portal(&mut self, side: u8, core: (i32, i32)) { + let owner = Owner::of_side(side); + let (px, py) = (core.0 + 6, core.1); + if px + 3 >= mw() as i32 - 2 || py < 2 || py + 3 >= mh() as i32 - 2 { + return; + } + for y in py..py + 3 { + for x in px..px + 3 { + let t = &mut self.map.t[idx(x, y)]; + if !matches!(t.kind, Kind::Core | Kind::Gate) { + t.kind = Kind::Portal; + t.owner = owner; + t.gold = 0; + t.marked = false; + t.reinforced = false; + } + } + } + if !self.map.portals.contains(&(px, py)) { + self.map.portals.push((px, py)); + } + } + // the starting area of a master: a 9x9 room of his own floor and his 3x3 soul core pub(crate) fn carve_master_area(&mut self, side: u8, core: (i32, i32)) { let owner = Owner::of_side(side); @@ -6831,6 +6857,27 @@ mod tests { assert!(g.ents.iter().filter(|e| e.kind == EK::Chainbreaker).count() <= 1); } + #[test] + fn every_master_of_a_coop_mission_has_an_own_portal() { + let m = crate::campaign::coop_mission_indexes()[2]; + let mut g = crate::campaign::start_mission(m, None); + g.coop = true; + assert!(g.start_masters(&[0, 2, 3], 0)); + for side in [2u8, 3] { + let owner = Owner::of_side(side); + let core = g.masters.iter().find(|m| m.side == side).unwrap().core; + let k = (0..g.map.portals.len()).find(|&k| g.map.portal_owner(k) == owner && g.map.portals[k] == (core.0 + 6, core.1)).expect("a portal of his own"); + let (px, py) = g.map.portals[k]; + let from = idx(core.0 + 1, core.1 + 4); + assert!(g.map.creature_path(false, from, idx(px, py + 1)).is_some(), "reachable from his dungeon"); + } + for n in crate::lobby::MATCH_SIZES { + let seats = crate::lobby::seats_for(n); + let mut m = Game::new(3, n, n); + assert!(m.start_masters(seats, 0), "a {}x{} match with {} seats", n, n, seats.len()); + } + } + #[test] fn a_coop_ai_seat_fights_beside_the_host_not_against_him() { let mut g = Game::new(11, 96, 128); diff --git a/src/rival.rs b/src/rival.rs index cea4959..89cd7d3 100644 --- a/src/rival.rs +++ b/src/rival.rs @@ -97,25 +97,7 @@ impl Game { pub fn add_rival(&mut self, side: u8, cx: i32, cy: i32, difficulty: u8) { let core = (cx - 1, cy - 1); self.carve_master_area(side, core); - // his own portal beside the dungeon (if there is room) - let owner = Owner::of_side(side); - let (px, py) = (core.0 + 6, core.1); - if px + 3 < mw() as i32 - 2 && py >= 2 && py + 3 < mh() as i32 - 2 { - for y in py..py + 3 { - for x in px..px + 3 { - let t = &mut self.map.t[idx(x, y)]; - if !matches!(t.kind, Kind::Core | Kind::Gate) { - t.kind = Kind::Portal; - t.owner = owner; - t.gold = 0; - t.reinforced = false; - } - } - } - if !self.map.portals.contains(&(px, py)) { - self.map.portals.push((px, py)); - } - } + self.add_master_portal(side, core); self.rivals.push(Rival { side, core, hp: RIVAL_CORE_HP, alive: true, radius: 5, grow_t: 20.0, spawn_t: 5.0, attack_t: 0.0, difficulty, power: 0.0, gift_t: PARTNER_GIFT_EVERY, think_t: 2.0, attacking: false, army0: 0.0 }); self.adopt_master(side, core, difficulty, true); }