// Bridges under strain: wooden bridges catch fire (fire traps, fire breath, fireballs, lightning), // the fire spreads along the planks and the bridge collapses into the river; knights wear bridges // down step by step, pit imps mend them again. Stone bridges over lava never burn. // Drawbridges are raised and lowered by hand or rise by themselves when enemies come near. // Frost (the frost glow trap, the frost spell) cools lava into walkable obsidian for a while. use crate::game::*; use crate::map::*; use crate::particles::*; use serde::{Deserialize, Serialize}; // seconds a burning plank holds before it falls into the river pub const BURN_TIME: f32 = 6.0; // a burning plank sets a neighbouring wooden plank alight with this chance per second pub const BURN_SPREAD: f32 = 0.35; // fire damage per second to whoever stands on a burning plank pub const BURN_DPS: f32 = 6.0; // wear a knight does to a bridge tile per second (1.0 = it falls) pub const TEAR_RATE: f32 = 0.12; // wear an imp mends per second pub const MEND_RATE: f32 = 0.25; // an automatic drawbridge rises when an enemy comes this close (tiles) pub const DRAWBRIDGE_WATCH: f32 = 6.0; // seconds the frost spell keeps lava cooled; the last seconds it steams pub const FREEZE_TIME: f32 = 40.0; pub const MELT_WARN: f32 = 5.0; #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct BridgeWork { pub burning: Vec<(usize, f32)>, // tile, seconds it burns already pub wear: Vec<(usize, f32)>, // tile, damage 0..1 #[serde(default)] pub cooled: Vec<(usize, f32)>, // obsidian tile, seconds until it melts #[serde(default)] pub watch_t: f32, // automatic drawbridges look around twice a second } impl Game { // a wooden bridge (or wooden drawbridge) that can burn pub fn wooden_bridge(&self, i: usize) -> bool { self.map.t.get(i).map_or(false, |t| t.kind == Kind::Bridge || (t.kind == Kind::Drawbridge && t.res & DB_LAVA == 0)) } pub fn burning(&self, i: usize) -> bool { self.bridgework.burning.iter().any(|b| b.0 == i) } pub fn wear_of(&self, i: usize) -> f32 { self.bridgework.wear.iter().find(|w| w.0 == i).map_or(0.0, |w| w.1) } // fire reaches tile (x, y): a wooden bridge there starts to burn pub fn ignite_at(&mut self, x: f32, y: f32) { let (tx, ty) = (x.floor() as i32, y.floor() as i32); if !inb(tx, ty) { return; } let i = idx(tx, ty); if !self.wooden_bridge(i) || self.burning(i) { return; } self.bridgework.burning.push((i, 0.0)); self.fx(3, tx as f32 + 0.5, ty as f32 + 0.5, 0.0, 0.0, 0.5, 0xff2080ff); if self.map.t[i].owner == self.me { self.msg("msg.bridge_fire", vec![]); self.hint(32, "hint.bridge_fire"); } } // fire along a jet from (x, y) in direction (dx, dy) over `len` tiles pub fn ignite_line(&mut self, x: f32, y: f32, dx: f32, dy: f32, len: f32) { let steps = (len * 3.0) as usize; for s in 1..=steps { let f = s as f32 / 3.0; self.ignite_at(x + dx * f, y + dy * f); } } // the shared part of every step: fire, collapse, drawbridges, melting obsidian pub(crate) fn update_bridges(&mut self, dt: f32) { self.update_drawbridges(dt); self.update_obsidian(dt); if self.bridgework.burning.is_empty() { return; } let mut k = 0; while k < self.bridgework.burning.len() { let (i, t0) = self.bridgework.burning[k]; // the plank is gone already (torn down, sold, rebuilt) if !self.wooden_bridge(i) { self.bridgework.burning.swap_remove(k); continue; } let t = t0 + dt; self.bridgework.burning[k].1 = t; let (x, y) = xy(i); let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5); // flames and smoke (looks only) if self.vrng.f() < dt * 14.0 { let (ox, oy) = (self.vrng.f() - 0.5, self.vrng.f() - 0.5); let v = (self.vrng.f() * 0.4 - 0.2, self.vrng.f() * 0.4 - 0.2, 0.9 + self.vrng.f() * 0.8); self.parts.push(Part { kind: EMBER, x: fx + ox * 0.9, y: fy + oy * 0.9, h: 0.15, vx: v.0, vy: v.1, vh: v.2, t: 0.0, max: 0.7, col: abgr(255, 120 + (self.vrng.f() * 80.0) as u8, 30), size: 0.08 }); } if self.vrng.f() < dt * 3.0 { self.parts.push(Part { kind: SMOKE, x: fx + self.vrng.f() - 0.5, y: fy + self.vrng.f() - 0.5, h: 0.5, vx: 0.0, vy: 0.0, vh: 0.6, t: 0.0, max: 2.2, col: abgr(40, 34, 30), size: 0.18 }); } // whoever stands in the flames gets burnt for j in 0..self.ents.len() { let e = &self.ents[j]; if e.alive && !e.held && !flies(e.kind) && e.tile() == i { self.ents[j].hp -= BURN_DPS * dt; if self.ents[j].hp <= 0.0 { self.kill(j); } } } // the fire spreads along the planks (deterministic: the shared random numbers) if t > 1.5 && (t / 1.0).floor() != ((t - dt) / 1.0).floor() { for &(dx, dy) in DIR4.iter() { if !inb(x + dx, y + dy) { continue; } let n = idx(x + dx, y + dy); if self.wooden_bridge(n) && !self.burning(n) && self.rng.f() < BURN_SPREAD { self.bridgework.burning.push((n, 0.0)); } } } if t >= BURN_TIME { self.bridgework.burning.swap_remove(k); self.bridge_falls(i); continue; } k += 1; } } // a bridge tile falls into its river: whoever stands there goes into the water (or lava) pub(crate) fn bridge_falls(&mut self, i: usize) { let t = self.map.t[i]; if !t.bridge() { return; } let mine = t.owner == self.me; let tl = &mut self.map.t[i]; tl.kind = if t.river() == Some(Kind::Lava) { Kind::Lava } else { Kind::Water }; tl.owner = Owner::None; tl.reserved = 0; tl.res = 0; self.bridgework.wear.retain(|w| w.0 != i); self.bridgework.burning.retain(|w| w.0 != i); self.ground_lost(i); let (x, y) = xy(i); self.fx(6, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.6, 0xff3a5a7a); for _ in 0..6 { let v = (self.vrng.f() * 2.0 - 1.0, self.vrng.f() * 2.0 - 1.0, 0.6 + self.vrng.f()); self.parts.push(Part { kind: DEBRIS, x: x as f32 + 0.5, y: y as f32 + 0.5, h: 0.2, vx: v.0, vy: v.1, vh: v.2, t: 0.0, max: 1.2, col: abgr(90, 62, 34), size: 0.08 }); } self.sfx(Sfx::DigDone, x as f32 + 0.5, y as f32 + 0.5); if mine { self.msg("msg.bridge_torn", vec![]); } } // a knight works on a bridge tile: true while it still stands pub(crate) fn wear_bridge(&mut self, i: usize, dt: f32) -> bool { let w = self.wear_of(i) + TEAR_RATE * dt; if w >= 1.0 { self.bridge_falls(i); return false; } match self.bridgework.wear.iter_mut().find(|x| x.0 == i) { Some(x) => x.1 = w, None => self.bridgework.wear.push((i, w)), } true } // an imp mends a worn bridge tile: true while there is more to do pub(crate) fn mend_bridge(&mut self, i: usize, dt: f32) -> bool { let Some(k) = self.bridgework.wear.iter().position(|x| x.0 == i) else { return false }; self.bridgework.wear[k].1 -= MEND_RATE * dt; if self.bridgework.wear[k].1 <= 0.0 { self.bridgework.wear.swap_remove(k); return false; } true } // a worn (not burning) bridge tile of the master being processed next to or at tile i pub(crate) fn worn_bridge_near(&self, i: usize) -> Option { if self.bridgework.wear.is_empty() { return None; } let (x, y) = xy(i); std::iter::once((0, 0)).chain(DIR4.iter().copied()).find_map(|(dx, dy)| { let n = idx(x + dx, y + dy); let ok = inb(x + dx, y + dy) && self.map.t[n].bridge() && self.map.t[n].owner == self.me && self.wear_of(n) > 0.0 && !self.burning(n); ok.then_some(n) }) } } // ------------------------------------------------------------------ drawbridges and frost impl Game { // the drawbridge tiles connected to tile i (same owner) pub fn drawbridge_group(&self, i: usize) -> Vec { let owner = self.map.t[i].owner; let mut group = vec![i]; let mut k = 0; while k < group.len() { let (x, y) = xy(group[k]); for &(dx, dy) in DIR4.iter() { if !inb(x + dx, y + dy) { continue; } let n = idx(x + dx, y + dy); let t = &self.map.t[n]; if t.kind == Kind::Drawbridge && t.owner == owner && !group.contains(&n) { group.push(n); } } k += 1; } group } // the drawbridge tool: a click on an own drawbridge switches it (down -> up -> automatic -> down), // anything else builds drawbridges like bridges pub fn drawbridge_order(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) { if x0 == x1 && y0 == y1 && inb(x0, y0) { let i = idx(x0, y0); let t = self.map.t[i]; if t.kind == Kind::Drawbridge && t.owner == self.me { let state = t.res & (DB_RAISED | DB_AUTO); let next = match state { 0 => DB_RAISED, s if s & DB_AUTO != 0 => 0, _ => DB_AUTO, }; self.set_drawbridge(i, next); let key = match next { DB_RAISED => "msg.drawbridge_up", DB_AUTO => "msg.drawbridge_auto", _ => "msg.drawbridge_down", }; self.msg(key, vec![]); self.sfx_ui(Sfx::Click); return; } } self.build_bridges_of(x0, y0, x1, y1, true); } // sets the state bits (DB_RAISED, DB_AUTO) of the whole drawbridge at tile i pub fn set_drawbridge(&mut self, i: usize, state: u8) { let group = self.drawbridge_group(i); let raise = state & DB_RAISED != 0 && !self.map.t[i].raised(); let lower = state & DB_RAISED == 0 && self.map.t[i].raised(); for &g in &group { let t = &mut self.map.t[g]; t.res = (t.res & DB_LAVA) | state; } if raise { // whoever stands on it jumps to the bank (or falls) for &g in &group { self.ground_lost(g); } } if raise || lower { let (x, y) = xy(i); self.sfx(Sfx::Build, x as f32 + 0.5, y as f32 + 0.5); for &g in &group { let (gx, gy) = xy(g); self.fx(6, gx as f32 + 0.5, gy as f32 + 0.5, 0.0, 0.0, 0.3, 0xff506070); } } } // automatic drawbridges rise while an enemy of their master is near, and come down again fn update_drawbridges(&mut self, dt: f32) { self.bridgework.watch_t -= dt; if self.bridgework.watch_t > 0.0 { return; } self.bridgework.watch_t = 0.5; let mut seen: Vec = Vec::new(); for i in 0..self.map.t.len() { let t = self.map.t[i]; if t.kind != Kind::Drawbridge || t.res & DB_AUTO == 0 || seen.contains(&i) { continue; } let group = self.drawbridge_group(i); seen.extend(group.iter().copied()); let side = t.owner.side(); let r2 = DRAWBRIDGE_WATCH * DRAWBRIDGE_WATCH; let enemy = self.ents.iter().any(|e| { e.alive && !e.held && e.captive == 0 && !e.chained && e.side() != side && !self.allied(side, e.side()) && group.iter().any(|&g| { let (gx, gy) = xy(g); (e.x - gx as f32 - 0.5).powi(2) + (e.y - gy as f32 - 0.5).powi(2) < r2 }) }); if enemy != t.raised() { self.set_drawbridge(i, DB_AUTO | if enemy { DB_RAISED } else { 0 }); if enemy && t.owner == self.me { self.msg("msg.drawbridge_rises", vec![]); } } } } // frost on tile i: lava turns to obsidian (or stays it) for at least `secs` pub fn cool_lava(&mut self, i: usize, secs: f32) { let t = &mut self.map.t[i]; match t.kind { Kind::Lava => { t.kind = Kind::Obsidian; t.owner = Owner::None; self.bridgework.cooled.push((i, secs)); let (x, y) = xy(i); for _ in 0..3 { self.parts.push(Part { kind: SMOKE, x: x as f32 + self.vrng.f(), y: y as f32 + self.vrng.f(), h: 0.1, vx: 0.0, vy: 0.0, vh: 0.5, t: 0.0, max: 1.6, col: abgr(200, 210, 220), size: 0.16 }); } } Kind::Obsidian => { if let Some(c) = self.bridgework.cooled.iter_mut().find(|c| c.0 == i) { c.1 = c.1.max(secs); } } _ => {} } } // frost in a circle around (x, y) pub fn cool_lava_around(&mut self, x: f32, y: f32, r: f32, secs: f32) { let ri = r.ceil() as i32; let (cx, cy) = (x.floor() as i32, y.floor() as i32); for ty in cy - ri..=cy + ri { for tx in cx - ri..=cx + ri { if inb(tx, ty) && (tx as f32 + 0.5 - x).powi(2) + (ty as f32 + 0.5 - y).powi(2) <= r * r { self.cool_lava(idx(tx, ty), secs); } } } } // obsidian melts again: it steams before, and whoever still stands there is burnt fn update_obsidian(&mut self, dt: f32) { let mut k = 0; while k < self.bridgework.cooled.len() { let (i, left) = self.bridgework.cooled[k]; if self.map.t[i].kind != Kind::Obsidian { self.bridgework.cooled.swap_remove(k); continue; } let left = left - dt; self.bridgework.cooled[k].1 = left; let (x, y) = xy(i); if left < MELT_WARN && self.vrng.f() < dt * 4.0 { self.parts.push(Part { kind: SMOKE, x: x as f32 + self.vrng.f(), y: y as f32 + self.vrng.f(), h: 0.05, vx: 0.0, vy: 0.0, vh: 0.4, t: 0.0, max: 1.4, col: abgr(150, 150, 160), size: 0.14 }); } if left <= 0.0 { self.bridgework.cooled.swap_remove(k); self.map.t[i].kind = Kind::Lava; self.ground_lost(i); continue; } k += 1; } } } #[cfg(test)] mod tests { use super::*; // a wooden bridge of the player across a strip of water, 6 tiles long fn bridge_game() -> (Game, Vec) { let mut g = Game::new(5, 64, 64); let (cx, cy) = g.map.core; let y = cy + 6; let mut tiles = Vec::new(); for x in cx - 3..cx + 3 { let i = idx(x, y); g.map.t[i].kind = Kind::Bridge; g.map.t[i].owner = Owner::Player; for dy in [-1, 1] { let t = &mut g.map.t[idx(x, y + dy)]; t.kind = Kind::Water; t.owner = Owner::None; } tiles.push(i); } (g, tiles) } #[test] fn a_burning_bridge_spreads_and_falls() { let (mut g, tiles) = bridge_game(); let (x, y) = xy(tiles[2]); g.ignite_at(x as f32 + 0.5, y as f32 + 0.5); assert!(g.burning(tiles[2])); for _ in 0..(20 * 20) { g.update(0.05); } assert_eq!(g.map.t[tiles[2]].kind, Kind::Water, "the burnt plank fell"); assert!(tiles.iter().filter(|&&i| g.map.t[i].kind == Kind::Water).count() >= 2, "the fire spread"); assert!(g.bridgework.burning.iter().all(|b| g.wooden_bridge(b.0))); } #[test] fn stone_bridges_do_not_burn() { let (mut g, tiles) = bridge_game(); g.map.t[tiles[0]].kind = Kind::LavaBridge; let (x, y) = xy(tiles[0]); g.ignite_at(x as f32 + 0.5, y as f32 + 0.5); assert!(!g.burning(tiles[0])); } #[test] fn worn_bridges_fall_or_are_mended() { let (mut g, tiles) = bridge_game(); assert!(g.wear_bridge(tiles[1], 2.0)); assert!(g.wear_of(tiles[1]) > 0.2); assert_eq!(g.worn_bridge_near(tiles[1]), Some(tiles[1])); while g.mend_bridge(tiles[1], 0.5) {} assert_eq!(g.wear_of(tiles[1]), 0.0); while g.wear_bridge(tiles[1], 1.0) {} assert_eq!(g.map.t[tiles[1]].kind, Kind::Water); } #[test] fn drawbridges_switch_and_rise_by_themselves() { let (mut g, tiles) = bridge_game(); for &i in &tiles { g.map.t[i].kind = Kind::Drawbridge; g.map.t[i].res = 0; } let (x, y) = xy(tiles[2]); g.drawbridge_order(x, y, x, y); assert!(tiles.iter().all(|&i| g.map.t[i].raised()), "the whole bridge goes up"); assert!(!g.map.walk(x, y)); g.drawbridge_order(x, y, x, y); assert!(tiles.iter().all(|&i| g.map.t[i].res & DB_AUTO != 0 && !g.map.t[i].raised()), "automatic"); // a knight comes near: it rises let id = g.spawn(EK::Knight, true, x as f32 + 0.5, y as f32 + 3.5, 1); for _ in 0..20 { g.update(0.05); } assert!(g.map.t[tiles[2]].raised(), "risen for the knight"); g.ents.retain(|e| e.id != id); for _ in 0..20 { g.update(0.05); } assert!(!g.map.t[tiles[2]].raised(), "down again"); g.drawbridge_order(x, y, x, y); assert_eq!(g.map.t[tiles[2]].res & (DB_RAISED | DB_AUTO), 0, "back to manual, down"); } #[test] fn frost_cools_lava_for_a_while() { let mut g = Game::new(5, 64, 64); let (cx, cy) = g.map.core; let i = idx(cx + 6, cy + 6); g.map.t[i].kind = Kind::Lava; g.cool_lava(i, 2.0); assert_eq!(g.map.t[i].kind, Kind::Obsidian); assert!(g.map.t[i].walkable()); for _ in 0..(3 * 20) { g.update(0.05); } assert_eq!(g.map.t[i].kind, Kind::Lava, "melted again"); } }