From bd3f42b8b8a8f42e9ca61427739b3a45f42a19ce Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Fri, 6 Nov 2026 16:08:28 +0100 Subject: [PATCH] Hero waves break in where the warning shows: they round the realm to the marked entry; the entry tile glows red Co-Authored-By: Claude Opus 5.5 --- src/balance.rs | 38 ++++++++++++++++++++++++++++++++++++++ src/game.rs | 28 +++++++++++++++++++++++++--- src/render3d.rs | 11 ++++++++++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/balance.rs b/src/balance.rs index 55923ae..12fe79f 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -209,3 +209,41 @@ fn balance_waves() { } } } + +// where the heroes of a wave really break in, against the marked entry points +#[test] +#[ignore] +fn balance_entry_points() { + use crate::map::*; + println!("\n== planned entry vs. where the heroes step onto the master's ground =="); + for seed in seeds() { + let mut g = Game::new(seed, 96, 96); + pilot(&mut g, 1, None); + let mut planned: Vec<(i32, i32)> = Vec::new(); + let mut first_in: Option<(i32, i32)> = None; + let mut wave = 0; + for _ in 0..((20.0 * 60.0 / STEP) as usize) { + g.update(STEP); + if g.state != 0 { + break; + } + if !g.planned_entries.is_empty() { + planned = g.planned_entries.clone(); + } + if g.wave != wave { + wave = g.wave; + first_in = None; + } + if first_in.is_none() { + if let Some(e) = g.ents.iter().find(|e| e.alive && e.active_hero() && g.map.get(e.x as i32, e.y as i32).map_or(false, |t| t.owner == Owner::Player)) { + first_in = Some((e.x as i32, e.y as i32)); + let d = planned.iter().map(|&(px, py)| (((px - e.x as i32).pow(2) + (py - e.y as i32).pow(2)) as f32).sqrt()).fold(f32::MAX, f32::min); + let wp = g.ents.iter().filter(|e| e.alive && e.active_hero()).filter(|e| e.waypoint.is_some()).count(); + let heroes = g.ents.iter().filter(|e| e.alive && e.active_hero()).count(); + let job = format!("{:?}", e.job); + println!("seed {} wave {}: planned {:?} first in at {:?} -> {:.1} tiles off; heroes with waypoint {}/{}; its job {}", seed, wave, planned, first_in.unwrap(), d, wp, heroes, job); + } + } + } + } +} diff --git a/src/game.rs b/src/game.rs index a43336b..67058fa 100644 --- a/src/game.rs +++ b/src/game.rs @@ -4903,6 +4903,13 @@ impl Game { if (e.x - wx as f32 - 0.5).abs() < 2.5 && (e.y - wy as f32 - 0.5).abs() < 2.5 { e.waypoint = None; // broke into the realm: now for the core e.path.clear(); + } else if let Some(&next) = e.path.first() { + // the realm grew into the way (imps claim fast): round it again + let realm = self.map.t[idx(wx, wy)].owner; + if self.map.t[next].owner == realm && next != idx(wx, wy) && self.map.t[e.tile()].owner != realm { + e.path.clear(); + e.repath = 0.0; + } } } if e.path.is_empty() && e.repath <= 0.0 { @@ -4910,7 +4917,8 @@ impl Game { let path = match e.waypoint { Some((wx, wy)) => { let goal = idx(wx, wy); - let p = self.map.search(cur, self.map.hero_cost(), |i| i == goal, true); + // round the realm from outside: in through the marked entry, not before + let p = self.entry_path(cur, goal).or_else(|| self.map.search(cur, self.map.hero_cost(), |i| i == goal, true)); if p.is_none() { e.waypoint = None; } @@ -4975,14 +4983,28 @@ impl Game { scored.push(((x, y), score)); } scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); - let first = scored[0].0; + // only places the heroes can reach from the gate without crossing the realm first + let gate = idx(gx, gy); + let mut tried = 0; + let mut ok = |g: &Game, p: (i32, i32)| { + tried += 1; + tried > 12 || g.entry_path(gate, idx(p.0, p.1)).is_some() + }; + let Some(first) = scored.iter().map(|s| s.0).find(|&p| ok(self, p)) else { return }; self.planned_entries.push(first); // second entry point at least 10 tiles away (used by waves of 6+ heroes) - if let Some(&(second, _)) = scored.iter().find(|((x, y), _)| (x - first.0).pow(2) + (y - first.1).pow(2) >= 100) { + if let Some(second) = scored.iter().map(|s| s.0).filter(|&(x, y)| (x - first.0).pow(2) + (y - first.1).pow(2) >= 100).find(|&p| ok(self, p)) { self.planned_entries.push(second); } } + // a hero's way to an entry tile that stays off the realm (the master's ground) until the entry + pub(crate) fn entry_path(&self, from: usize, goal: usize) -> Option> { + let owner = self.map.t[goal].owner; + let cost = self.map.hero_cost(); + self.map.search(from, |i| if i != goal && i != from && self.map.t[i].owner == owner { None } else { cost(i) }, |i| i == goal, true) + } + // compass direction of a tile as seen from the core (language key) pub fn direction_key(&self, (x, y): (i32, i32)) -> &'static str { let (cx, cy) = (self.map.core.0 + 1, self.map.core.1 + 1); diff --git a/src/render3d.rs b/src/render3d.rs index 2c8b43e..a123173 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -3377,12 +3377,21 @@ fn draw_entry_warnings(g: &Game, t: f32, n: usize) { let line = [255, 90, 60, (120.0 + 120.0 * p) as u8]; let mut mb = MB::new(); for &(ex, ey) in &g.planned_entries { + let realm = g.map.get(ex, ey).map_or(Owner::Player, |t| t.owner); + // the entry tile itself: a pulsing red plate (also where an open corridor leads in) + let c = vec3(ex as f32 + 0.5, 0.03, ey as f32 + 0.5); + mb.quad( + [c + vec3(-0.5, 0.0, -0.5), c + vec3(0.5, 0.0, -0.5), c + vec3(0.5, 0.0, 0.5), c + vec3(-0.5, 0.0, 0.5)], + [Vec2::ZERO; 4], + [col(vec3(1.0, 0.16, 0.12) * (0.6 + 0.6 * p), 0.45 + 0.35 * p); 4], + [Vec3::Y; 4], + ); for dy in -2..=2 { for dx in -2..=2 { let (x, y) = (ex + dx, ey + dy); let Some(tile) = g.map.get(x, y) else { continue }; // walls of the realm's border near the entry point (not rock) - let border = DIR4.iter().any(|&(ax, ay)| g.map.get(x + ax, y + ay).map_or(false, |o| o.owner == Owner::Player && !o.solid())); + let border = DIR4.iter().any(|&(ax, ay)| g.map.get(x + ax, y + ay).map_or(false, |o| o.owner == realm && !o.solid())); if tile.diggable() && border && dx.abs() + dy.abs() <= 3 && mb.v.len() < 30000 { block_shell(&mut mb, &g.map, x, y, n, fill, line, 1.0, 0.04); }