Towers shoot enemy towers and the enemy gate; build areas use the floor under the pointer
- With no unit in reach, towers fire at opposing towers (both sides) and the player's towers at the gate of the order / hero gate (line of sight needed, walls block) - Rooms, bridges, sell, flag and towers pick the floor tile under the pointer; only digging and the hand still aim at walls - Test for tower duels and gate fire Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
09e22b009e
commit
bd8f53e8a1
2 files changed
+162
-6
No files matched your search
+143
-1
@@ -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<u32>)> {
|
||||
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<u32>))> = 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<u32>)) {
|
||||
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<f32> = 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) });
|
||||
|
||||
+19
-5
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user