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
|
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) {
|
fn update_towers(&mut self, dt: f32) {
|
||||||
for k in 0..self.towers.len() {
|
for k in 0..self.towers.len() {
|
||||||
if k >= self.towers.len() {
|
if k >= self.towers.len() {
|
||||||
@@ -3207,7 +3312,13 @@ impl Game {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let t = self.towers[k].clone();
|
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);
|
let s = tower_stats(t.kind);
|
||||||
if s.mana > 0.0 && !t.hero && self.mana < s.mana {
|
if s.mana > 0.0 && !t.hero && self.mana < s.mana {
|
||||||
self.hint(17, "hint.tower_mana");
|
self.hint(17, "hint.tower_mana");
|
||||||
@@ -3910,6 +4021,37 @@ mod tests {
|
|||||||
let _ = f;
|
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]
|
#[test]
|
||||||
fn vaults_are_claimed_and_looted() {
|
fn vaults_are_claimed_and_looted() {
|
||||||
let (mut g, _) = fort_game(|x, y| crate::fortress::FortSpec { vaults: 2, vault_value: 1800, ..spec(x, y) });
|
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)
|
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 {
|
fn area_tool(t: Tool) -> bool {
|
||||||
matches!(t, Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge)
|
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 {
|
if app.game.state != 0 {
|
||||||
return;
|
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 ground = app.cam.ground_point(mx, my);
|
||||||
let (wx, wy) = (ground.x, ground.y);
|
let (wx, wy) = (ground.x, ground.y);
|
||||||
match (ui.tool, tile) {
|
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)
|
// 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) {
|
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;
|
let (sx, sy) = start;
|
||||||
match tool {
|
match tool {
|
||||||
Tool::Dig => {
|
Tool::Dig => {
|
||||||
@@ -406,8 +421,7 @@ async fn main() {
|
|||||||
let in_menu = menu.open();
|
let in_menu = menu.open();
|
||||||
let playing = !in_menu && !dialog;
|
let playing = !in_menu && !dialog;
|
||||||
let over_ui = ui.over_ui(ux, uy);
|
let over_ui = ui.over_ui(ux, uy);
|
||||||
let picked = app.cam.pick_tile(&app.game.map, mx, my);
|
let hover_tile = tool_tile(&app.cam, &app.game.map, ui.tool, mx, my);
|
||||||
let hover_tile = picked.map(|(x, z, _)| (x, z));
|
|
||||||
let ground = app.cam.ground_point(mx, my);
|
let ground = app.cam.ground_point(mx, my);
|
||||||
let (wx, wy) = (ground.x, ground.y);
|
let (wx, wy) = (ground.x, ground.y);
|
||||||
let keys = app.settings.clone();
|
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 (ux, uy) = (mx * ui::dpr(), my * ui::dpr());
|
||||||
let blocked = menu.open() || ui.show_help || ui.show_langsel;
|
let blocked = menu.open() || ui.show_help || ui.show_langsel;
|
||||||
let over_ui = blocked || ui.over_ui(ux, uy);
|
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);
|
let ground = app.cam.ground_point(mx, my);
|
||||||
clear_background(Color::from_rgba(4, 3, 5, 255));
|
clear_background(Color::from_rgba(4, 3, 5, 255));
|
||||||
let (color, radius) = match ui.tool {
|
let (color, radius) = match ui.tool {
|
||||||
|
|||||||
Reference in new issue
Block a user