From 6f037294c05e4fdbb53c8b606f1e7a8a2eb7c43e Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Tue, 3 Nov 2026 10:04:46 +0100 Subject: [PATCH] Tower shots start at the tower top (arrow battlements, cannon muzzle, magic orb) Effects now carry start/end heights; leftover 2D height offsets that moved projectiles and hit sparks sideways on the map are removed. Co-Authored-By: Claude Opus 5.5 --- src/game.rs | 65 +++++++++++++++++++++++++++++++++++++------------ src/render3d.rs | 12 +++------ 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/src/game.rs b/src/game.rs index 322d87c..69d50ae 100644 --- a/src/game.rs +++ b/src/game.rs @@ -268,6 +268,8 @@ pub struct Fx { pub t: f32, pub max: f32, pub col: u32, + pub h: f32, // height of the start point (3D) + pub h2: f32, // height of the end point (projectiles) } #[derive(Serialize, Deserialize)] @@ -587,8 +589,24 @@ impl Game { } pub fn fx(&mut self, kind: u8, x: f32, y: f32, x2: f32, y2: f32, max: f32, col: u32) { + let h = if kind == 0 { 0.45 } else { 0.4 }; if self.fx.len() < 400 { - self.fx.push(Fx { kind, x, y, x2, y2, t: 0.0, max, col }); + self.fx.push(Fx { kind, x, y, x2, y2, t: 0.0, max, col, h, h2: 0.45 }); + } + } + + // projectile from (x, y) at height h to (x2, y2) at height h2 + #[allow(clippy::too_many_arguments)] + pub fn shot(&mut self, x: f32, y: f32, h: f32, x2: f32, y2: f32, h2: f32, max: f32, col: u32) { + if self.fx.len() < 400 { + self.fx.push(Fx { kind: 0, x, y, x2, y2, t: 0.0, max, col, h, h2 }); + } + } + + // effect at a given height + pub fn fx_at(&mut self, kind: u8, x: f32, y: f32, h: f32, max: f32, col: u32) { + if self.fx.len() < 400 { + self.fx.push(Fx { kind, x, y, x2: 0.0, y2: 0.0, t: 0.0, max, col, h, h2: h }); } } @@ -915,7 +933,7 @@ impl Game { let (gx, gy) = self.gate_center(); if s.range > 1.5 { let col = if e.kind == EK::Mage { 0xffff44cc } else { 0xff70b0d0 }; - self.fx(0, e.x, e.y - 0.3, gx, gy, 0.25, col); + self.fx(0, e.x, e.y, gx, gy, 0.25, col); } else { let fx = (e.x + gx) * 0.5; let fy = (e.y + gy) * 0.5; @@ -1111,7 +1129,7 @@ impl Game { let (dx, dy) = norm(tx - e.x, ty - e.y); let (nx, ny) = (e.x + dx * 0.9, e.y + dy * 0.9); if self.map.walk(nx as i32, ny as i32) { - self.fx(0, tx, ty - 0.3, e.x, e.y - 0.3, 0.3, 0xff8090a0); + self.fx(0, tx, ty, e.x, e.y, 0.3, 0xff8090a0); self.sfx(Sfx::Capture, e.x, e.y); let t = &mut self.ents[j]; t.x = nx; @@ -1162,10 +1180,10 @@ impl Game { EK::Priestess => 0xff3080ff, _ => 0xff70b0d0, }; - self.fx(0, e.x, e.y - 0.3, tx, ty - 0.3, 0.25, col); + self.fx(0, e.x, e.y, tx, ty, 0.25, col); self.sfx(Sfx::Shoot, e.x, e.y); } else { - self.fx(2, tx, ty - 0.2, 0.0, 0.0, 0.25, 0xffffffff); + self.fx(2, tx, ty, 0.0, 0.0, 0.25, 0xffffffff); self.sfx(Sfx::MeleeHit, tx, ty); } e.xp += dmg * 0.6; @@ -1180,7 +1198,7 @@ impl Game { .map(|(o, _)| o); if let Some(o) = second { let (ox, oy) = (self.ents[o].x, self.ents[o].y); - self.fx(2, ox, oy - 0.2, 0.0, 0.0, 0.25, 0xff40a0ff); + self.fx(2, ox, oy, 0.0, 0.0, 0.25, 0xff40a0ff); self.ents[o].hp -= dmg * 0.5; self.ents[o].flash = 0.15; if self.ents[o].hp <= 0.0 { @@ -2603,14 +2621,21 @@ impl Game { self.towers[k].flash = 0.15; let (px, py) = t.pos(); let (ex, ey) = (self.ents[j].x, self.ents[j].y); - self.towers[k].aim = (ey - py).atan2(ex - px); + let aim = (ey - py).atan2(ex - px); + self.towers[k].aim = aim; + // where the shot leaves the tower (see the tower models in render3d.rs) + 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 dmg = t.dmg(); match t.kind { TowerKind::Arrow => { // the arrow pierces up to two more heroes behind the target let dir = norm(ex - px, ey - py); let end = (px + dir.0 * t.range(), py + dir.1 * t.range()); - self.fx(0, px, py - 0.6, end.0, end.1, 0.3, 0xffd0e0f0); + self.shot(mx, my, mh, end.0, end.1, 0.45, 0.3, 0xffd0e0f0); self.sfx(Sfx::Shoot, px, py); let mut hits = vec![(j, dmg)]; for (o, e) in self.ents.iter().enumerate() { @@ -2632,9 +2657,9 @@ impl Game { } TowerKind::Cannon => { // splash damage and knockback around the impact - self.fx(0, px, py - 0.8, ex, ey, 0.35, 0xff303030); + self.shot(mx, my, mh, ex, ey, 0.4, 0.35, 0xff303030); self.fx(3, ex, ey, 0.0, 0.0, 0.6, 0xff4070a0); - self.fx(2, ex, ey - 0.2, 0.0, 0.0, 0.3, 0xff60c0ff); + self.fx(2, ex, ey, 0.0, 0.0, 0.3, 0xff60c0ff); self.sfx(Sfx::Cannon, px, py); let mut hits = Vec::new(); for (o, e) in self.ents.iter().enumerate() { @@ -2665,7 +2690,7 @@ impl Game { } TowerKind::Magic => { // sure hit, slows, jumps to a second hero - self.fx(0, px, py - 1.0, ex, ey, 0.2, 0xffff60d0); + self.shot(mx, my, mh, ex, ey, 0.45, 0.2, 0xffff60d0); self.sfx(Sfx::Magic, px, py); self.ents[j].slow = 2.0; let chain = self @@ -2697,6 +2722,7 @@ impl Game { fn attack_tower(&mut self, e: &mut Ent, id: u32, dt: f32) -> bool { let Some(k) = self.towers.iter().position(|t| t.id == id) else { return false }; let (tx, ty) = self.towers[k].pos(); + let hit_h = self.towers[k].top() * 0.6; let s = st(e.kind); let d = ((tx - e.x).powi(2) + (ty - e.y).powi(2)).sqrt(); if d <= s.range + 0.75 && (s.range < 1.5 || self.los(e.x, e.y, tx, ty)) { @@ -2707,9 +2733,9 @@ impl Game { e.cd = s.interval; let dmg = e.atk() * (0.85 + self.rng.f() * 0.3); if s.range > 1.5 { - self.fx(0, e.x, e.y - 0.3, tx, ty - 0.5, 0.25, 0xff70b0d0); + self.shot(e.x, e.y, 0.45, tx, ty, hit_h, 0.25, 0xff70b0d0); } else { - self.fx(2, tx, ty - 0.4, 0.0, 0.0, 0.25, 0xffffffff); + self.fx_at(2, tx, ty, hit_h, 0.25, 0xffffffff); } self.sfx(Sfx::MeleeHit, tx, ty); let t = &mut self.towers[k]; @@ -2836,7 +2862,7 @@ impl Game { if matches!(e.job, Job::Sleep(_)) { e.job = Job::None; // woken up } - self.fx(2, x, y - 0.3, 0.0, 0.0, 0.3, 0xffffffff); + self.fx(2, x, y, 0.0, 0.0, 0.3, 0xffffffff); self.sfx(Sfx::Slap, x, y); return 1; } @@ -2853,7 +2879,7 @@ impl Game { } self.punch_ready = self.time + 0.5; self.ents[j].stun = 1.0; - self.fx(2, x, y - 0.3, 0.0, 0.0, 0.35, 0xff40e0ff); + self.fx(2, x, y, 0.0, 0.0, 0.35, 0xff40e0ff); self.fx(7, x, y, 0.0, 0.0, 0.4, 0xff40e0ff); self.sfx(Sfx::Punch, x, y); self.tower_damage(j, 15.0); @@ -2987,6 +3013,15 @@ impl Tower { let (x, y) = xy(self.tile); (x as f32 + 0.5, y as f32 + 0.5) } + + // height of the tower body (3D units); grows with the level + pub fn top(&self) -> f32 { + match self.kind { + TowerKind::Arrow => 1.0 + 0.3 * (self.level - 1) as f32, + TowerKind::Cannon => 0.6 + 0.15 * (self.level - 1) as f32, + TowerKind::Magic => 1.2 + 0.3 * (self.level - 1) as f32, + } + } // gold spent on this tower so far pub fn invested(&self) -> i32 { tower_stats(self.kind).cost[..self.level as usize].iter().sum() diff --git a/src/render3d.rs b/src/render3d.rs index b145df1..16f46bc 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -1099,11 +1099,7 @@ impl R3d { // ------------------------------------------------------------------ towers pub fn tower_height(t: &Tower) -> f32 { - match t.kind { - TowerKind::Arrow => 1.0 + 0.3 * (t.level - 1) as f32, - TowerKind::Cannon => 0.6 + 0.15 * (t.level - 1) as f32, - TowerKind::Magic => 1.2 + 0.3 * (t.level - 1) as f32, - } + t.top() } fn shade_c(r: u8, g: u8, b: u8, f: f32) -> Color { @@ -1301,11 +1297,11 @@ fn draw_fx(f: &Fx, t: f32) { let p = (f.t / f.max).clamp(0.0, 1.0); let c = rgba(f.col); let a = |alpha: f32| Color::new(c.r, c.g, c.b, alpha); - let at = vec3(f.x, 0.4, f.y); + let at = vec3(f.x, f.h, f.y); match f.kind { 0 => { - let from = vec3(f.x, 0.45, f.y); - let to = vec3(f.x2, 0.45, f.y2); + let from = vec3(f.x, f.h, f.y); + let to = vec3(f.x2, f.h2, f.y2); let head = from.lerp(to, p); let tail = from.lerp(to, (p - 0.35).max(0.0)); line3(tail, head, 0.06, a(0.8));