Rework arrows falling into water and lava
Hits no longer splash; glancing and spent arrows splash where they land, drift burning on lava, and arrows on a removed bridge sink. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5adf3d0e00
commit
f0eb33a6b1
3 files changed
+135
-17
No files matched your search
+1
-1
@@ -4888,7 +4888,7 @@ impl Game {
|
||||
let dir = norm(ex - px, ey - py);
|
||||
let (endx, endy, wall) = self.arrow_end(px, py, dir.0, dir.1, t.range());
|
||||
self.shot(mx, my, mh, endx, endy, 0.45, 0.3, 0xffd0e0f0);
|
||||
self.style_last_fx(if wall { crate::particles::ST_ARROW_WALL } else { crate::particles::ST_ARROW });
|
||||
self.style_last_fx(if wall { crate::particles::ST_ARROW_WALL } else { crate::particles::ST_ARROW_FALL });
|
||||
self.sfx(Sfx::Shoot, px, py);
|
||||
let mut hits = vec![(j, dmg)];
|
||||
for (o, e) in self.ents.iter().enumerate() {
|
||||
|
||||
+116
-15
@@ -24,6 +24,7 @@ pub const SCORCH: u8 = 7; // dark mark on the floor
|
||||
pub const FLASH: u8 = 8; // expanding fireball
|
||||
pub const RING: u8 = 9; // shock wave
|
||||
pub const SPLASH: u8 = 10; // liquid droplets, bounce on the river surface
|
||||
pub const BURNING_ARROW: u8 = 11; // arrow drifting on lava until it burns up (vx, vy = its direction)
|
||||
|
||||
// projectile styles (Fx::style)
|
||||
pub const ST_PLAIN: u8 = 0;
|
||||
@@ -36,6 +37,7 @@ pub const ST_ARCANE: u8 = 6;
|
||||
pub const ST_RUNE: u8 = 7;
|
||||
pub const ST_MAGIC: u8 = 8;
|
||||
pub const ST_ARROW_WALL: u8 = 9; // an arrow that ends in a wall
|
||||
pub const ST_ARROW_FALL: u8 = 10; // an arrow at the end of its range: it falls a little further
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Part {
|
||||
@@ -70,7 +72,7 @@ pub fn fx_pos(f: &Fx, p: f32) -> (f32, f32, f32) {
|
||||
let arc = match f.style {
|
||||
ST_CANNON => 0.22,
|
||||
ST_FIRE => 0.12,
|
||||
ST_ARROW | ST_ARROW_WALL => 0.04,
|
||||
ST_ARROW | ST_ARROW_WALL | ST_ARROW_FALL => 0.04,
|
||||
_ => 0.0,
|
||||
} * dist;
|
||||
let h = f.h + (f.h2 - f.h) * p + arc * (p * std::f32::consts::PI).sin();
|
||||
@@ -167,7 +169,7 @@ impl Game {
|
||||
ST_RUNE => (45.0, SPARK, abgr(90, 200, 255), 0.03, 0.7),
|
||||
ST_MAGIC => (45.0, EMBER, abgr(255, 90, 220), 0.035, 0.9),
|
||||
ST_CANNON => (30.0, SMOKE, abgr(70, 70, 76), 0.1, 0.9),
|
||||
ST_ARROW | ST_ARROW_WALL => (0.0, EMBER, 0, 0.0, 0.0),
|
||||
ST_ARROW | ST_ARROW_WALL | ST_ARROW_FALL => (0.0, EMBER, 0, 0.0, 0.0),
|
||||
_ => (12.0, EMBER, abgr(180, 210, 230), 0.03, 0.6),
|
||||
};
|
||||
let n = rate * dt * size;
|
||||
@@ -207,6 +209,11 @@ impl Game {
|
||||
p.h += p.vh * dt;
|
||||
}
|
||||
FLASH | RING => {}
|
||||
BURNING_ARROW => {
|
||||
// drifts slowly with the lava
|
||||
p.x += p.vx * 0.05 * dt;
|
||||
p.y += p.vy * 0.05 * dt;
|
||||
}
|
||||
SPLASH => {
|
||||
p.vh -= GRAVITY * 0.75 * dt;
|
||||
p.x += p.vx * dt;
|
||||
@@ -247,9 +254,25 @@ impl Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
// burning arrows on lava: embers and smoke while they burn, a puff when they are gone
|
||||
let burning: Vec<(f32, f32, bool)> = self.parts.iter().filter(|p| p.kind == BURNING_ARROW).map(|p| (p.x, p.y, p.t >= p.max)).collect();
|
||||
for (x, y, gone) in burning {
|
||||
let at = (x, y, LIQ_H + 0.06);
|
||||
if gone {
|
||||
self.burst(EMBER, at, 5, 0.5, 1.2, 0.7, abgr(255, 150, 50), 0.03);
|
||||
self.part(SMOKE, at, (0.0, 0.0, 0.5), 0.8, abgr(70, 45, 35), 0.08);
|
||||
} else if self.vrng.f() < 9.0 * dt {
|
||||
self.burst(EMBER, at, 1, 0.2, 0.9, 0.6, abgr(255, 140, 40), 0.025);
|
||||
if self.vrng.f() < 0.35 {
|
||||
self.part(SMOKE, at, (0.0, 0.0, 0.45), 0.7, abgr(90, 60, 45), 0.06);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.parts.retain(|p| p.t < p.max);
|
||||
// stuck arrows fall out when their wall is dug away; everything fades in the end
|
||||
// stuck arrows fall out when their wall is dug away; arrows and scorch marks on the floor
|
||||
// sink when the ground under them turns into river (a bridge torn down); everything fades in the end
|
||||
let map = &self.map;
|
||||
let mut sunk: Vec<(Kind, f32, f32, f32, f32)> = Vec::new();
|
||||
for p in self.stuck.iter_mut() {
|
||||
p.t += dt;
|
||||
if p.kind == ARROW && p.h > 0.1 {
|
||||
@@ -257,9 +280,17 @@ impl Game {
|
||||
if !map.get(tx, ty).map_or(false, |t| t.solid()) {
|
||||
p.t = p.max;
|
||||
}
|
||||
} else if let Some(t) = map.get(p.x as i32, p.y as i32).filter(|t| t.liquid()) {
|
||||
if p.kind == ARROW {
|
||||
sunk.push((t.kind, p.x, p.y, p.vx, p.vy));
|
||||
}
|
||||
p.t = p.max;
|
||||
}
|
||||
}
|
||||
self.stuck.retain(|p| p.t < p.max);
|
||||
for (kind, x, y, dx, dy) in sunk {
|
||||
self.liquid_impact(kind, x, y, 0.5, (dx, dy));
|
||||
}
|
||||
}
|
||||
|
||||
fn impact(&mut self, f: &Fx) {
|
||||
@@ -287,25 +318,39 @@ impl Game {
|
||||
self.burst(SMOKE, (x, y, 0.45), 10, 0.8, 0.6, 2.4, abgr(80, 80, 86), 0.23);
|
||||
self.stick(Part { kind: SCORCH, x, y, h: 0.0, vx: 0.0, vy: 0.0, vh: 0.0, t: 0.0, max: 20.0, col: 0, size: 0.6 });
|
||||
}
|
||||
ST_ARROW | ST_ARROW_WALL => {
|
||||
if let Some(kind) = self.liquid_kind_at(x, y) {
|
||||
self.liquid_impact(kind, x, y, size);
|
||||
return;
|
||||
}
|
||||
ST_ARROW | ST_ARROW_WALL | ST_ARROW_FALL => {
|
||||
let (dx, dy) = (f.x2 - f.x, f.y2 - f.y);
|
||||
let l = (dx * dx + dy * dy).sqrt().max(0.01);
|
||||
let (dx, dy) = (dx / l, dy / l);
|
||||
if f.style == ST_ARROW_WALL {
|
||||
// ends in a wall (also above a river)
|
||||
self.burst(DEBRIS, (x, y, f.h2), 3, 0.8, 1.0, 0.8, abgr(110, 90, 80), 0.04);
|
||||
let h = f.h2 + self.vrng.f() * 0.2;
|
||||
self.stick(Part { kind: ARROW, x, y, h, vx: dx, vy: dy, vh: -0.08, t: 0.0, max: STUCK_LIFE, col: 0, size: 1.0 });
|
||||
} else if self.vrng.f() < 0.35 {
|
||||
// glanced off: stuck in the floor next to the target
|
||||
let (ox, oy) = (self.vrng.f() * 0.7 - 0.35, self.vrng.f() * 0.7 - 0.35);
|
||||
let tilt = -0.6 - self.vrng.f() * 0.6;
|
||||
let n = (1.0 + tilt * tilt).sqrt();
|
||||
self.stick(Part { kind: ARROW, x: x + ox, y: y + oy, h: 0.04, vx: dx / n, vy: dy / n, vh: tilt / n, t: 0.0, max: STUCK_LIFE, col: 0, size: 1.0 });
|
||||
return;
|
||||
}
|
||||
let spot = if f.style == ST_ARROW_FALL {
|
||||
// spent at the end of its range: it sails on a little and comes down there
|
||||
// (a wall in the way stops it in front of the wall)
|
||||
let reach = 0.35 + f.h2.max(0.0) * 1.5;
|
||||
let (fx, fy) = (x + dx * reach, y + dy * reach);
|
||||
if self.map.get(fx as i32, fy as i32).map_or(true, |t| t.solid()) { (x, y) } else { (fx, fy) }
|
||||
} else if self.vrng.f() < 0.35 {
|
||||
// a hit that glanced off: it comes down next to the target
|
||||
(x + self.vrng.f() * 0.7 - 0.35, y + self.vrng.f() * 0.7 - 0.35)
|
||||
} else {
|
||||
return; // a hit: the arrow stays in the target
|
||||
};
|
||||
if let Some(kind) = self.liquid_kind_at(spot.0, spot.1) {
|
||||
self.liquid_impact(kind, spot.0, spot.1, size, (dx, dy));
|
||||
return;
|
||||
}
|
||||
if self.map.get(spot.0 as i32, spot.1 as i32).map_or(true, |t| t.solid()) {
|
||||
return;
|
||||
}
|
||||
let tilt = -0.6 - self.vrng.f() * 0.6;
|
||||
let n = (1.0 + tilt * tilt).sqrt();
|
||||
self.stick(Part { kind: ARROW, x: spot.0, y: spot.1, h: 0.04, vx: dx / n, vy: dy / n, vh: tilt / n, t: 0.0, max: STUCK_LIFE, col: 0, size: 1.0 });
|
||||
}
|
||||
ST_SHADOW => self.burst(EMBER, (x, y, f.h2), 14, 1.6, 1.5, 0.9, abgr(190, 70, 255), 0.04),
|
||||
ST_HOLY => self.burst(MOTE, (x, y, f.h2), 12, 0.8, 0.8, 0.8, abgr(255, 240, 160), 0.035),
|
||||
@@ -319,14 +364,18 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
// open river under (x, y): bridges and their decks catch the arrow
|
||||
fn liquid_kind_at(&self, x: f32, y: f32) -> Option<Kind> {
|
||||
self.map.get(x as i32, y as i32).filter(|t| t.liquid()).map(|t| t.kind)
|
||||
}
|
||||
|
||||
fn liquid_impact(&mut self, kind: Kind, x: f32, y: f32, size: f32) {
|
||||
// an arrow comes down into a river: a splash; on lava it drifts on, burning, for a moment
|
||||
fn liquid_impact(&mut self, kind: Kind, x: f32, y: f32, size: f32, (dx, dy): (f32, f32)) {
|
||||
let at = (x, y, LIQ_H + 0.04);
|
||||
match kind {
|
||||
Kind::Lava => {
|
||||
let life = 1.0 + self.vrng.f() * 1.0;
|
||||
self.part(BURNING_ARROW, (x, y, LIQ_H + 0.02), (dx, dy, 0.0), life, abgr(255, 120, 30), 1.0);
|
||||
self.burst(SPLASH, at, 8.max((10.0 * size) as usize), 1.3, 1.9, 0.38, abgr(255, 110, 25), 0.045);
|
||||
self.burst(MOTE, at, 4, 0.7, 1.2, 0.7, abgr(255, 150, 45), 0.035);
|
||||
self.part(FLASH, at, (0.0, 0.0, 0.0), 0.12, abgr(255, 155, 45), 0.18 * size.max(0.7));
|
||||
@@ -440,4 +489,56 @@ mod tests {
|
||||
assert!(g.parts.len() < 5);
|
||||
assert!(g.stuck.iter().any(|p| p.kind == ARROW), "arrows stay for minutes");
|
||||
}
|
||||
|
||||
// a little lava and water east of the core
|
||||
fn river_spot(g: &mut Game, kind: Kind) -> (f32, f32) {
|
||||
let (cx, cy) = g.map.core;
|
||||
let (x, y) = (cx + 3, cy - 3);
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = kind;
|
||||
t.owner = Owner::None;
|
||||
(x as f32 + 0.5, y as f32 + 0.5)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spent_arrows_drift_burning_on_lava_and_hits_do_not_splash() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
let (lx, ly) = river_spot(&mut g, Kind::Lava);
|
||||
// a spent tower arrow comes down in the lava: it drifts there, burning, then is gone
|
||||
g.shot(lx - 2.0, ly, 0.5, lx - 0.4, ly, 0.0, 0.3, 0xffffffff);
|
||||
g.style_last_fx(ST_ARROW_FALL);
|
||||
for _ in 0..8 {
|
||||
g.update(0.05);
|
||||
}
|
||||
assert!(g.parts.iter().any(|p| p.kind == BURNING_ARROW), "burning on the lava");
|
||||
assert!(!g.stuck.iter().any(|p| p.kind == ARROW), "nothing stuck in the lava");
|
||||
for _ in 0..60 {
|
||||
g.update(0.05);
|
||||
}
|
||||
assert!(!g.parts.iter().any(|p| p.kind == BURNING_ARROW), "burnt up");
|
||||
// archer arrows that hit something above the lava mostly stay in the target: never a stuck arrow there
|
||||
for _ in 0..20 {
|
||||
g.shot(lx - 3.0, ly, 0.5, lx, ly, 0.8, 0.3, 0xffffffff);
|
||||
g.style_last_fx(ST_ARROW);
|
||||
}
|
||||
for _ in 0..8 {
|
||||
g.update(0.05);
|
||||
}
|
||||
let burning = g.parts.iter().filter(|p| p.kind == BURNING_ARROW).count();
|
||||
assert!(burning < 20, "not every hit falls into the lava: {}", burning);
|
||||
assert!(!g.stuck.iter().any(|p| g.map.t[idx(p.x as i32, p.y as i32)].liquid()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrows_on_a_bridge_sink_when_it_is_gone() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
let (wx, wy) = river_spot(&mut g, Kind::Bridge);
|
||||
g.stick(Part { kind: ARROW, x: wx, y: wy, h: 0.04, vx: 1.0, vy: 0.0, vh: -0.5, t: 0.0, max: STUCK_LIFE, col: 0, size: 1.0 });
|
||||
g.update(0.05);
|
||||
assert_eq!(g.stuck.len(), 1, "it stays on the deck");
|
||||
g.map.t[idx(wx as i32, wy as i32)].kind = Kind::Water;
|
||||
g.update(0.05);
|
||||
assert!(g.stuck.is_empty(), "sunk with the bridge");
|
||||
assert!(g.parts.iter().any(|p| p.kind == SPLASH));
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -2257,7 +2257,7 @@ fn draw_projectile(f: &Fx, p: f32, t: f32) {
|
||||
let c = rgba(f.col);
|
||||
let sz = f.size;
|
||||
match f.style {
|
||||
ST_ARROW | ST_ARROW_WALL => {
|
||||
ST_ARROW | ST_ARROW_WALL | ST_ARROW_FALL => {
|
||||
let back = at((p - 0.08).max(0.0));
|
||||
let dir = (head - back).normalize_or_zero();
|
||||
line3(head - dir * 0.3, head, 0.03, Color::from_rgba(120, 84, 48, 255));
|
||||
@@ -2504,6 +2504,23 @@ fn draw_parts_solid(g: &Game) {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// arrows drifting on lava: the shaft glows and chars, then crumbles
|
||||
for p in g.parts.iter().filter(|p| p.kind == BURNING_ARROW) {
|
||||
let life = (p.t / p.max).clamp(0.0, 1.0);
|
||||
let k = 1.0 - life * life;
|
||||
let bob = (p.t * 3.0 + p.x * 2.0).sin() * 0.01;
|
||||
let mid = vec3(p.x, -LIQ_D + 0.025 + bob, p.y);
|
||||
let dir = vec3(p.vx, 0.0, p.vy).normalize_or_zero();
|
||||
let half = dir * (0.17 * k);
|
||||
let wood = vec3(0.45, 0.31, 0.18);
|
||||
let glow = vec3(1.0, 0.45, 0.1);
|
||||
let char_ = vec3(0.08, 0.05, 0.04);
|
||||
let c = if life < 0.5 { wood.lerp(glow, life * 2.0) } else { glow.lerp(char_, (life - 0.5) * 2.0) };
|
||||
line3(mid - half, mid + half, 0.03, Color::new(c.x, c.y, c.z, 1.0));
|
||||
if life < 0.6 {
|
||||
draw_cube(mid + half, Vec3::splat(0.035), None, Color::from_rgba(200, 200, 210, 255));
|
||||
}
|
||||
}
|
||||
for p in g.stuck.iter().filter(|p| p.kind == ARROW) {
|
||||
let left = p.max - p.t;
|
||||
let k = if left < 10.0 { left / 10.0 } else { 1.0 };
|
||||
|
||||
Reference in new issue
Block a user