Drawbridges (down, up, automatic), frost spell and obsidian over cooled lava

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:52:12 +01:00
1 parent 76c23a06f0
commit 34b4640d85
32 files changed
+869 -31

No files matched your search

+234 -2
View File
@@ -1,6 +1,8 @@
// 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::*;
@@ -17,17 +19,26 @@ pub const BURN_DPS: f32 = 6.0;
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)
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 {
@@ -65,8 +76,10 @@ impl Game {
}
}
// the shared part of every step: fire, collapse
// 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;
}
@@ -135,6 +148,7 @@ impl Game {
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);
@@ -187,6 +201,181 @@ impl Game {
}
}
// ------------------------------------------------------------------ drawbridges and frost
impl Game {
// the drawbridge tiles connected to tile i (same owner)
pub fn drawbridge_group(&self, i: usize) -> Vec<usize> {
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<usize> = 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::*;
@@ -245,4 +434,47 @@ mod tests {
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");
}
}