Wooden bridges burn; knights wear bridges down, imps mend them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 21:34:33 +01:00
1 parent c11d281281
commit 29e4970ab1
23 files changed
+356 -17

No files matched your search

+248
View File
@@ -0,0 +1,248 @@
// 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.
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;
#[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
}
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)
}
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
pub(crate) fn update_bridges(&mut self, dt: f32) {
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.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<usize> {
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)
})
}
}
#[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<usize>) {
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);
}
}