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 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 10:04:46 +01:00
1 parent 4d1bf30586
commit 6f037294c0
2 files changed
+54 -23

No files matched your search

+50 -15
View File
@@ -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()
+4 -8
View File
@@ -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));