diff --git a/src/game.rs b/src/game.rs index 898088f..584b5a6 100644 --- a/src/game.rs +++ b/src/game.rs @@ -3195,6 +3195,111 @@ impl Game { best } + // an enemy tower or the gate of the order within reach: (x, y, height, tower id or None = gate) + fn tower_structure_target(&self, t: &Tower) -> Option<(f32, f32, f32, Option)> { + let (px, py) = t.pos(); + let r = t.range(); + let minr = tower_stats(t.kind).min_range; + let in_reach = |x: f32, y: f32| { + let d = (x - px).powi(2) + (y - py).powi(2); + d <= r * r && d >= minr * minr + }; + let mut best: Option<(f32, (f32, f32, f32, Option))> = None; + for o in self.towers.iter().filter(|o| o.hero != t.hero) { + let (ox, oy) = o.pos(); + if in_reach(ox, oy) && self.los_to_tile(px, py, o.tile) { + let d = (ox - px).powi(2) + (oy - py).powi(2); + if best.map_or(true, |b| d < b.0) { + best = Some((d, (ox, oy, o.top() * 0.6, Some(o.id)))); + } + } + } + if best.is_none() && !t.hero && self.has_gate() && self.gate_hp > 0.0 { + // the nearest tile of the gate counts + let (gx, gy) = self.map.gate; + let (nx, ny) = ((px - 0.5).clamp(gx as f32, gx as f32 + 2.0) + 0.5, (py - 0.5).clamp(gy as f32, gy as f32 + 2.0) + 0.5); + if in_reach(nx, ny) && self.los_to_tile(px, py, idx(nx as i32, ny as i32)) { + best = Some((0.0, (nx, ny, 1.0, None))); + } + } + best.map(|b| b.1) + } + + // line of sight from a tower to a target tile: walls and other towers block, + // the tower's own tile and the target tile do not + fn los_to_tile(&self, x0: f32, y0: f32, target: usize) -> bool { + let start = idx(x0 as i32, y0 as i32); + let (tx, ty) = xy(target); + let (x1, y1) = (tx as f32 + 0.5, ty as f32 + 0.5); + let n = (((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt() * 3.0).ceil().max(1.0) as i32; + for k in 1..n { + let f = k as f32 / n as f32; + let i = idx((x0 + (x1 - x0) * f) as i32, (y0 + (y1 - y0) * f) as i32); + if i == start || i == target { + continue; + } + let tl = &self.map.t[i]; + if tl.solid() || tl.kind == Kind::Tower { + return false; + } + } + true + } + + fn tower_fire_at_structure(&mut self, k: usize, (ex, ey, eh, target): (f32, f32, f32, Option)) { + let t = self.towers[k].clone(); + let s = tower_stats(t.kind); + if s.mana > 0.0 && !t.hero && self.mana < s.mana { + self.towers[k].cd = 1.0; + return; + } + if !t.hero { + self.mana -= s.mana; + } + let (px, py) = t.pos(); + let aim = (ey - py).atan2(ex - px); + let tw = &mut self.towers[k]; + tw.cd = s.interval; + tw.flash = 0.15; + tw.aim = aim; + let (mx, my, mh) = match t.kind { + TowerKind::Arrow => (px, py, t.top() + 0.25), + TowerKind::Cannon => (px + aim.cos() * 0.55, py + aim.sin() * 0.55, t.top() + 0.2), + TowerKind::Magic => (px, py, t.top() + 0.45), + }; + let col = match t.kind { + TowerKind::Arrow => 0xffd0e0f0, + TowerKind::Cannon => 0xff303030, + TowerKind::Magic => 0xffff60d0, + }; + self.shot(mx, my, mh, ex, ey, eh, 0.3, col); + self.fx_at(2, ex, ey, eh, 0.3, 0xffffffff); + self.sfx(match t.kind { + TowerKind::Arrow => Sfx::Shoot, + TowerKind::Cannon => Sfx::Cannon, + TowerKind::Magic => Sfx::Magic, + }, px, py); + let dmg = t.dmg(); + match target { + Some(id) => { + if let Some(o) = self.towers.iter().position(|o| o.id == id) { + self.towers[o].hp -= dmg; + if self.towers[o].hp <= 0.0 { + self.destroy_tower(o); + } + } + } + None => { + self.gate_hp -= dmg; + self.gate_hit_t = self.time; + self.sfx(Sfx::GateHit, ex, ey); + if self.mission.as_ref().map_or(true, |m| m.gate_defenders) { + self.check_gate_stage(); + } + } + } + } + fn update_towers(&mut self, dt: f32) { for k in 0..self.towers.len() { if k >= self.towers.len() { @@ -3207,7 +3312,13 @@ impl Game { continue; } let t = self.towers[k].clone(); - let Some(j) = self.tower_target(&t) else { continue }; + let Some(j) = self.tower_target(&t) else { + // no unit in reach: enemy towers and the enemy gate are shot at + if let Some(target) = self.tower_structure_target(&t) { + self.tower_fire_at_structure(k, target); + } + continue; + }; let s = tower_stats(t.kind); if s.mana > 0.0 && !t.hero && self.mana < s.mana { self.hint(17, "hint.tower_mana"); @@ -3910,6 +4021,37 @@ mod tests { let _ = f; } + #[test] + fn towers_fight_towers_and_the_gate() { + // a tower of the player in the courtyard of a fortress, no units around + let (mut g, _) = fort_game(|x, y| crate::fortress::FortSpec { towers: vec![TowerKind::Arrow], ..spec(x, y) }); + g.ents.clear(); + let (kx, ky) = xy(g.towers[0].tile); + let (px, py) = (kx + 3, ky + 2); + g.map.t[idx(px, py)].owner = Owner::Player; + g.gold = 5000.0; + g.build_tower(TowerKind::Arrow, px, py); + assert_eq!(g.towers.len(), 2); + let hp: Vec = g.towers.iter().map(|t| t.hp).collect(); + for _ in 0..100 { + g.update(0.05); + } + assert!(g.towers.iter().zip(&hp).all(|(t, h)| t.hp < *h), "both towers are hit"); + // free game: a tower in reach of the hero gate shoots at the gate + let mut g = Game::new(5, 64, 64); + g.ents.clear(); + let (gx, gy) = g.map.gate; + let (tx, ty) = (gx - 2, gy + 1); + g.map.t[idx(tx, ty)].kind = Kind::Floor; + g.map.t[idx(tx, ty)].owner = Owner::Player; + g.gold = 5000.0; + g.build_tower(TowerKind::Arrow, tx, ty); + for _ in 0..60 { + g.update(0.05); + } + assert!(g.gate_hp < GATE_HP, "the gate is shot at"); + } + #[test] fn vaults_are_claimed_and_looted() { let (mut g, _) = fort_game(|x, y| crate::fortress::FortSpec { vaults: 2, vault_value: 1800, ..spec(x, y) }); diff --git a/src/main.rs b/src/main.rs index 763a03f..6e6a0a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,6 +64,21 @@ struct Input { inspect: Option<(f32, f32, f64)>, // long press: show information for this point (CSS pixels) } +// Tile under the pointer for a tool: digging (and the hand) aim at walls too, +// building and everything else only at the floor below the pointer +fn tool_tile(cam: &Cam, map: &map::Map, tool: Tool, mx: f32, my: f32) -> Option<(i32, i32)> { + if matches!(tool, Tool::Dig | Tool::Hand) { + return cam.pick_tile(map, mx, my).map(|(x, z, _)| (x, z)); + } + let p = cam.ground_point(mx, my); + let (x, z) = (p.x.floor() as i32, p.y.floor() as i32); + if inb(x, z) { + Some((x, z)) + } else { + None + } +} + fn area_tool(t: Tool) -> bool { matches!(t, Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge) } @@ -74,7 +89,7 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32, if app.game.state != 0 { return; } - let tile = app.cam.pick_tile(&app.game.map, mx, my).map(|(x, z, _)| (x, z)); + let tile = tool_tile(&app.cam, &app.game.map, ui.tool, mx, my); let ground = app.cam.ground_point(mx, my); let (wx, wy) = (ground.x, ground.y); match (ui.tool, tile) { @@ -101,7 +116,7 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32, // Finish a selection rectangle at the tile under (mx, my) fn finish_rect(app: &mut App, tool: Tool, start: (i32, i32), mx: f32, my: f32) { - let Some((ex, ey, _)) = app.cam.pick_tile(&app.game.map, mx, my) else { return }; + let Some((ex, ey)) = tool_tile(&app.cam, &app.game.map, tool, mx, my) else { return }; let (sx, sy) = start; match tool { Tool::Dig => { @@ -406,8 +421,7 @@ async fn main() { let in_menu = menu.open(); let playing = !in_menu && !dialog; let over_ui = ui.over_ui(ux, uy); - let picked = app.cam.pick_tile(&app.game.map, mx, my); - let hover_tile = picked.map(|(x, z, _)| (x, z)); + let hover_tile = tool_tile(&app.cam, &app.game.map, ui.tool, mx, my); let ground = app.cam.ground_point(mx, my); let (wx, wy) = (ground.x, ground.y); let keys = app.settings.clone(); @@ -866,7 +880,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M let (ux, uy) = (mx * ui::dpr(), my * ui::dpr()); let blocked = menu.open() || ui.show_help || ui.show_langsel; let over_ui = blocked || ui.over_ui(ux, uy); - let hover_tile = app.cam.pick_tile(&app.game.map, mx, my).map(|(x, z, _)| (x, z)); + let hover_tile = tool_tile(&app.cam, &app.game.map, ui.tool, mx, my); let ground = app.cam.ground_point(mx, my); clear_background(Color::from_rgba(4, 3, 5, 255)); let (color, radius) = match ui.tool {