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:
1 parent
c11d281281
commit
29e4970ab1
23 files changed
+356
-17
No files matched your search
+248
@@ -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);
|
||||
}
|
||||
}
|
||||
+46
-17
@@ -207,6 +207,7 @@ pub enum Job {
|
||||
Brawl(u32), // two creatures that cannot stand each other scuffle (never to the death)
|
||||
Guard(usize), // holds a guard post
|
||||
Spar(usize), // sparring in the arena
|
||||
MendBridge(usize), // imp mends a worn bridge tile
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@@ -583,6 +584,8 @@ pub struct Game {
|
||||
#[serde(default)]
|
||||
pub dwarf_caves: Vec<crate::dwarves::DwarfCave>,
|
||||
#[serde(default)]
|
||||
pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges
|
||||
#[serde(default)]
|
||||
pub dwarf_t: f32,
|
||||
#[serde(default)]
|
||||
pub rune_free: u32, // rune forge towers that can be built for free (from chests)
|
||||
@@ -627,7 +630,7 @@ pub struct Game {
|
||||
#[serde(skip)]
|
||||
pub debug_markers: bool, // show debug-only command markers
|
||||
#[serde(skip, default = "hint_times")]
|
||||
hint_t: [f32; 32], // last time each hint slot was shown
|
||||
hint_t: [f32; 48], // last time each hint slot was shown
|
||||
}
|
||||
|
||||
pub const MARK_DIG: u8 = 0;
|
||||
@@ -660,8 +663,8 @@ pub struct Mark {
|
||||
pub fixed: bool, // guard/attack/gather order flags are shown without the debug setting
|
||||
}
|
||||
|
||||
fn hint_times() -> [f32; 32] {
|
||||
[-1000.0; 32]
|
||||
fn hint_times() -> [f32; 48] {
|
||||
[-1000.0; 48]
|
||||
}
|
||||
|
||||
// every simulation step has the same length: on every client of a match the same commands are
|
||||
@@ -898,6 +901,7 @@ impl Game {
|
||||
stuck: Vec::new(),
|
||||
vrng: crate::particles::vrng(),
|
||||
dwarf_caves: Vec::new(),
|
||||
bridgework: Default::default(),
|
||||
dwarf_t: 30.0,
|
||||
rune_free: 0,
|
||||
rune_final: 0,
|
||||
@@ -1650,6 +1654,7 @@ impl Game {
|
||||
self.update_rivals(dt);
|
||||
}
|
||||
self.update_dwarves(dt);
|
||||
self.update_bridges(dt);
|
||||
// fallen hearts of a match are shared state: every client marks them in the same step
|
||||
self.update_fallen_masters();
|
||||
// the end of the game and the fog are about the master at this computer
|
||||
@@ -2339,7 +2344,7 @@ impl Game {
|
||||
Some((x as f32 + 0.5, y as f32 + 0.5))
|
||||
};
|
||||
let target = match e.job {
|
||||
Job::Dig(t) | Job::Reinforce(t) | Job::Help(t) | Job::TearBridge(t) => tile(t),
|
||||
Job::Dig(t) | Job::Reinforce(t) | Job::Help(t) | Job::TearBridge(t) | Job::MendBridge(t) => tile(t),
|
||||
Job::Attack(id) | Job::Torment(id) | Job::Carry(id) | Job::Brawl(id) => self.find(id).map(|j| (self.ents[j].x, self.ents[j].y)),
|
||||
Job::Repair(id) | Job::AttackTower(id) => self.towers.iter().find(|t| t.id == id).map(|t| t.pos()),
|
||||
_ => None,
|
||||
@@ -2625,6 +2630,10 @@ impl Game {
|
||||
}
|
||||
}
|
||||
if e.kind == EK::Priestess || e.kind == EK::Wyrm || e.kind == EK::BattleMage {
|
||||
// fire on a wooden bridge sets it alight now and then
|
||||
if self.rng.f() < 0.25 {
|
||||
self.ignite_at(tx, ty);
|
||||
}
|
||||
// fireball / fire breath: half damage to enemies around the target
|
||||
let me = e.side();
|
||||
let splash: Vec<usize> = self
|
||||
@@ -3074,21 +3083,14 @@ impl Game {
|
||||
e.path.clear();
|
||||
e.working = true;
|
||||
e.face = sgn(bx as f32 + 0.5 - e.x);
|
||||
// the bridge wears down step by step; imps can mend it while it stands
|
||||
e.timer += dt;
|
||||
if e.timer >= 4.0 {
|
||||
e.timer = 0.0;
|
||||
let lava = t.kind == Kind::LavaBridge;
|
||||
let tl = &mut self.map.t[i];
|
||||
tl.kind = if lava { Kind::Lava } else { Kind::Water };
|
||||
tl.owner = Owner::None;
|
||||
tl.reserved = 0;
|
||||
self.ground_lost(i);
|
||||
self.fx(6, bx as f32 + 0.5, by as f32 + 0.5, 0.0, 0.0, 0.6, 0xff3a5a7a);
|
||||
self.sfx(Sfx::DigDone, bx as f32 + 0.5, by as f32 + 0.5);
|
||||
self.msg("msg.bridge_torn", vec![]);
|
||||
return false;
|
||||
if e.timer >= 1.0 {
|
||||
e.timer -= 1.0;
|
||||
self.sfx(Sfx::MeleeHit, bx as f32 + 0.5, by as f32 + 0.5);
|
||||
self.fx(2, bx as f32 + 0.5, by as f32 + 0.5, 0.0, 0.0, 0.2, 0xff3a5a7a);
|
||||
}
|
||||
return true;
|
||||
return self.wear_bridge(i, dt);
|
||||
}
|
||||
if e.path.is_empty() && e.repath <= 0.0 {
|
||||
e.repath = 1.5;
|
||||
@@ -3322,6 +3324,22 @@ impl Game {
|
||||
self.sfx(Sfx::Reinforce, tx, ty);
|
||||
}
|
||||
}
|
||||
Job::MendBridge(b) => {
|
||||
let (bx, by) = xy(b);
|
||||
let (tx, ty) = (bx as f32 + 0.5, by as f32 + 0.5);
|
||||
if (tx - e.x).abs() > 1.6 || (ty - e.y).abs() > 1.6 || self.burning(b) {
|
||||
e.job = Job::None;
|
||||
return;
|
||||
}
|
||||
e.face = sgn(tx - e.x);
|
||||
e.working = true;
|
||||
if !self.mend_bridge(b, dt) {
|
||||
e.job = Job::None;
|
||||
}
|
||||
if (e.anim * 3.0).fract() < dt * 3.0 {
|
||||
self.sfx(Sfx::Reinforce, tx, ty);
|
||||
}
|
||||
}
|
||||
Job::Help(t) => {
|
||||
// the task is done or nobody works on it any more: back to thinking
|
||||
if self.task_worker(t).is_none() || self.helpers(t) > HELPERS_MAX {
|
||||
@@ -3488,6 +3506,15 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// ... and so are bridges the knights worked on
|
||||
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.worn_bridge_near(i).is_some(), true) {
|
||||
let end = p.last().copied().unwrap_or(cur);
|
||||
if let Some(b) = self.worn_bridge_near(end) {
|
||||
e.path = p;
|
||||
e.job = Job::MendBridge(b);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.claimable(i, me), true) {
|
||||
let end = p.last().copied().unwrap_or(cur);
|
||||
self.map.t[end].reserved = me;
|
||||
@@ -5368,6 +5395,7 @@ impl Game {
|
||||
self.mana -= COST;
|
||||
self.sfx(Sfx::Lightning, wx, wy);
|
||||
self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa);
|
||||
self.ignite_at(wx, wy);
|
||||
let mut kills = Vec::new();
|
||||
for (j, e) in self.ents.iter_mut().enumerate() {
|
||||
if e.alive && !e.held && e.active_hero() && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 2.6 {
|
||||
@@ -5551,6 +5579,7 @@ pub fn job_name(e: &Ent) -> &'static str {
|
||||
Job::Leave => "job.leave",
|
||||
Job::Reinforce(_) => "job.reinforce",
|
||||
Job::Repair(_) => "job.repair",
|
||||
Job::MendBridge(_) => "job.mend_bridge",
|
||||
Job::Carry(_) => "job.carry",
|
||||
Job::Pray(_) => "job.pray",
|
||||
Job::HealCore => "job.heal_core",
|
||||
|
||||
@@ -30,6 +30,7 @@ mod save;
|
||||
#[cfg(test)]
|
||||
mod scenes;
|
||||
mod blocks;
|
||||
mod bridges;
|
||||
mod mscript;
|
||||
mod settings;
|
||||
mod sfx;
|
||||
|
||||
@@ -1138,6 +1138,12 @@ impl R3d {
|
||||
}
|
||||
}
|
||||
}
|
||||
// burning bridges flicker
|
||||
for &(i, _) in g.bridgework.burning.iter().filter(|b| g.map.shown(b.0)) {
|
||||
let (x, z) = xy(i);
|
||||
let fl = 0.8 + 0.2 * (t * 11.0 + i as f32).sin();
|
||||
push(vec3(x as f32 + 0.5, 0.6, z as f32 + 0.5), 4.5, vec3(1.0, 0.5, 0.15), 1.6 * fl, 5.0);
|
||||
}
|
||||
let pulse = 0.5 + 0.5 * (t * 4.0).sin();
|
||||
for &(ex, ey) in &g.planned_entries {
|
||||
push(vec3(ex as f32 + 0.5, 1.2, ey as f32 + 0.5), 5.0, vec3(1.0, 0.12, 0.08), 0.6 + 1.0 * pulse, 30.0);
|
||||
|
||||
@@ -529,6 +529,7 @@ impl Game {
|
||||
self.traps[k].sprung = true;
|
||||
self.traps[k].aim = a;
|
||||
let (dx, dy) = (a.cos(), a.sin());
|
||||
self.ignite_line(cx, cy, dx, dy, 3.2);
|
||||
for o in near(3.4, &self.ents) {
|
||||
let (rx, ry) = (self.ents[o].x - cx, self.ents[o].y - cy);
|
||||
let along = rx * dx + ry * dy;
|
||||
|
||||
Reference in new issue
Block a user