Rivers and bridges: water and lava tiles, bridge tool, burning in lava
- New tile kinds (appended for the savegame codec): water, lava, wooden bridge, stone bridge, core shard - Sunken river beds with banks, bridge decks with rails, lava glow and lights - Bridge tool (key U, rooms category): bridges grow from own ground, 30 gold/tile over water, 75 over lava - Selling a bridge drops the units on it into the river (water: swim to the bank, lava: burn) - Cannon blasts and punches knock units back; into lava = burnt (heroes still pay their bounty) - Heroes cannot path through rivers; texts EN/DE; tests for bridges and lava Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
af6be62805
commit
75dc445d3b
9 files changed
+536
-22
No files matched your search
+218
-8
@@ -341,6 +341,9 @@ pub const ROOM_KINDS: [Kind; 7] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind:
|
||||
pub const ROOM_COST: [i32; 7] = [60, 80, 100, 140, 160, 200, 180];
|
||||
pub const ROOM_NAMES: [&str; 7] = ["room.treasury", "room.lair", "room.farm", "room.training", "room.library", "room.torture", "room.temple"];
|
||||
|
||||
// bridge cost per tile: over water, over lava
|
||||
pub const BRIDGE_COST: [i32; 2] = [30, 75];
|
||||
|
||||
pub fn room_index(k: Kind) -> Option<usize> {
|
||||
ROOM_KINDS.iter().position(|&r| r == k)
|
||||
}
|
||||
@@ -1298,6 +1301,133 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
// falls into lava: dies at once (no capture), heroes still pay their bounty
|
||||
pub fn burn(&mut self, j: usize) {
|
||||
let e = &mut self.ents[j];
|
||||
if !e.alive {
|
||||
return;
|
||||
}
|
||||
e.alive = false;
|
||||
e.held = false;
|
||||
let (id, kind, hero, x, y) = (e.id, e.kind, e.hero, e.x, e.y);
|
||||
for o in self.ents.iter_mut() {
|
||||
if o.captive == 2 && o.carrier == id {
|
||||
o.captive = 1;
|
||||
o.carrier = 0;
|
||||
}
|
||||
}
|
||||
self.release_all(id);
|
||||
self.hand.retain(|&h| h != id);
|
||||
self.fx(3, x, y, 0.0, 0.0, 1.0, 0xff2060ff);
|
||||
self.fx(2, x, y, 0.0, 0.0, 0.5, 0xff30a0ff);
|
||||
self.sfx(Sfx::Death, x, y);
|
||||
let s = st(kind);
|
||||
if hero {
|
||||
let got = self.add_gold(s.bounty as f32);
|
||||
self.msg("msg.hero_burned", vec![format!("@{}", s.name), (got as i32).to_string()]);
|
||||
} else {
|
||||
self.msg("msg.creature_burned", vec![format!("@{}", s.name)]);
|
||||
}
|
||||
}
|
||||
|
||||
// pushes a unit; into lava = burnt, never into water or walls
|
||||
pub fn knock(&mut self, j: usize, dx: f32, dy: f32) {
|
||||
let e = &self.ents[j];
|
||||
let (nx, ny) = (e.x + dx, e.y + dy);
|
||||
let Some(t) = self.map.get(nx as i32, ny as i32).copied() else { return };
|
||||
if t.kind == Kind::Lava {
|
||||
let e = &mut self.ents[j];
|
||||
e.x = nx;
|
||||
e.y = ny;
|
||||
self.burn(j);
|
||||
} else if t.walkable() {
|
||||
let e = &mut self.ents[j];
|
||||
e.x = nx;
|
||||
e.y = ny;
|
||||
e.path.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// the ground of tile i turned into river (bridge removed): units on it fall in
|
||||
fn ground_lost(&mut self, i: usize) {
|
||||
let lava = self.map.t[i].kind == Kind::Lava;
|
||||
let (x, y) = xy(i);
|
||||
for j in 0..self.ents.len() {
|
||||
let e = &self.ents[j];
|
||||
if !e.alive || e.held || e.tile() != i {
|
||||
continue;
|
||||
}
|
||||
if lava {
|
||||
self.burn(j);
|
||||
continue;
|
||||
}
|
||||
// water: swim to the nearest bank, otherwise drown
|
||||
let bank = DIR8.iter().map(|&(dx, dy)| (x + dx, y + dy)).find(|&(bx, by)| self.map.walk(bx, by));
|
||||
match bank {
|
||||
Some((bx, by)) => {
|
||||
let e = &mut self.ents[j];
|
||||
e.x = bx as f32 + 0.5;
|
||||
e.y = by as f32 + 0.5;
|
||||
e.path.clear();
|
||||
}
|
||||
None => self.burn(j),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bridges grow from own ground into the river: every pass adds the river tiles of the
|
||||
// rectangle that touch own walkable ground (including bridges built in the pass before)
|
||||
pub fn build_bridges(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
|
||||
let (mut built, mut broke, mut any_river) = (0, false, false);
|
||||
loop {
|
||||
let mut add = Vec::new();
|
||||
for y in y0.min(y1)..=y0.max(y1) {
|
||||
for x in x0.min(x1)..=x0.max(x1) {
|
||||
if !inb(x, y) || !self.map.t[idx(x, y)].liquid() {
|
||||
continue;
|
||||
}
|
||||
any_river = true;
|
||||
let own = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == Owner::Player && n.walkable()));
|
||||
if own {
|
||||
add.push(idx(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
if add.is_empty() {
|
||||
break;
|
||||
}
|
||||
let mut progress = false;
|
||||
for i in add {
|
||||
let lava = self.map.t[i].kind == Kind::Lava;
|
||||
let cost = BRIDGE_COST[lava as usize] as f32;
|
||||
if self.gold < cost {
|
||||
broke = true;
|
||||
continue;
|
||||
}
|
||||
self.gold -= cost;
|
||||
let t = &mut self.map.t[i];
|
||||
t.kind = if lava { Kind::LavaBridge } else { Kind::Bridge };
|
||||
t.owner = Owner::Player;
|
||||
built += 1;
|
||||
progress = true;
|
||||
let (x, y) = xy(i);
|
||||
self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.5, 0xff40d0ff);
|
||||
if built == 1 {
|
||||
self.sfx(Sfx::Build, x as f32 + 0.5, y as f32 + 0.5);
|
||||
}
|
||||
}
|
||||
if !progress {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if broke {
|
||||
self.msg("msg.no_gold", vec![]);
|
||||
self.sfx_ui(Sfx::Error);
|
||||
} else if built == 0 {
|
||||
self.hint(21, if any_river { "hint.bridge_where" } else { "hint.bridge_river" });
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- pit imps (workers)
|
||||
|
||||
fn dig_neighbor(&self, i: usize, me: u32) -> Option<usize> {
|
||||
@@ -1345,7 +1475,7 @@ impl Game {
|
||||
|
||||
fn claimable(&self, i: usize, me: u32) -> bool {
|
||||
let t = &self.map.t[i];
|
||||
if t.owner != Owner::None || !matches!(t.kind, Kind::Floor | Kind::Portal) {
|
||||
if t.owner != Owner::None || !matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard) {
|
||||
return false;
|
||||
}
|
||||
if t.reserved != 0 && t.reserved != me {
|
||||
@@ -2466,6 +2596,16 @@ impl Game {
|
||||
continue;
|
||||
}
|
||||
let t = &mut self.map.t[i];
|
||||
if t.owner == Owner::Player && t.bridge() {
|
||||
let lava = t.kind == Kind::LavaBridge;
|
||||
t.kind = if lava { Kind::Lava } else { Kind::Water };
|
||||
t.owner = Owner::None;
|
||||
t.reserved = 0;
|
||||
refund += BRIDGE_COST[lava as usize] as f32 * 0.5;
|
||||
self.ground_lost(i);
|
||||
continue;
|
||||
}
|
||||
let t = &mut self.map.t[i];
|
||||
if t.owner == Owner::Player {
|
||||
if let Some(r) = room_index(t.kind) {
|
||||
t.kind = Kind::Floor;
|
||||
@@ -2678,14 +2818,10 @@ impl Game {
|
||||
// knockback away from the impact, only onto walkable ground
|
||||
let e = &self.ents[o];
|
||||
let away = if dist > 0.05 { norm(e.x - ex, e.y - ey) } else { norm(e.x - px, e.y - py) };
|
||||
let (nx, ny) = (e.x + away.0 * 0.7, e.y + away.1 * 0.7);
|
||||
if self.map.walk(nx as i32, ny as i32) {
|
||||
let e = &mut self.ents[o];
|
||||
e.x = nx;
|
||||
e.y = ny;
|
||||
e.path.clear();
|
||||
self.knock(o, away.0 * 0.7, away.1 * 0.7);
|
||||
if self.ents[o].alive {
|
||||
self.tower_damage(o, d);
|
||||
}
|
||||
self.tower_damage(o, d);
|
||||
}
|
||||
}
|
||||
TowerKind::Magic => {
|
||||
@@ -2879,6 +3015,14 @@ impl Game {
|
||||
}
|
||||
self.punch_ready = self.time + 0.5;
|
||||
self.ents[j].stun = 1.0;
|
||||
// the punch throws the hero back (into lava = burnt)
|
||||
let back = -self.ents[j].face;
|
||||
self.knock(j, back * 0.8, 0.0);
|
||||
if !self.ents[j].alive {
|
||||
self.fx(2, x, y, 0.0, 0.0, 0.35, 0xff40e0ff);
|
||||
self.sfx(Sfx::Punch, x, y);
|
||||
return 2;
|
||||
}
|
||||
self.fx(2, x, y, 0.0, 0.0, 0.35, 0xff40e0ff);
|
||||
self.fx(7, x, y, 0.0, 0.0, 0.4, 0xff40e0ff);
|
||||
self.sfx(Sfx::Punch, x, y);
|
||||
@@ -3149,6 +3293,72 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// a vertical river right next to the start area (east of it)
|
||||
fn river_game(lava: bool) -> (Game, i32) {
|
||||
let mut g = Game::new(7, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
let rx = cx + 6; // the start area reaches to cx + 5
|
||||
for y in cy - 6..=cy + 8 {
|
||||
for x in rx..rx + 3 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = if lava { Kind::Lava } else { Kind::Water };
|
||||
t.owner = Owner::None;
|
||||
}
|
||||
}
|
||||
(g, rx)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridges_grow_from_own_ground() {
|
||||
let (mut g, rx) = river_game(false);
|
||||
let (_, cy) = g.map.core;
|
||||
g.gold = 1000.0;
|
||||
// not touching own ground: nothing happens
|
||||
g.build_bridges(rx + 1, cy, rx + 2, cy);
|
||||
assert_eq!(g.map.t[idx(rx + 1, cy)].kind, Kind::Water);
|
||||
// from the bank across the river: three tiles at 30 gold
|
||||
g.build_bridges(rx - 1, cy, rx + 2, cy);
|
||||
for x in rx..rx + 3 {
|
||||
let t = g.map.t[idx(x, cy)];
|
||||
assert_eq!(t.kind, Kind::Bridge);
|
||||
assert_eq!(t.owner, Owner::Player);
|
||||
assert!(t.walkable());
|
||||
}
|
||||
assert_eq!(g.gold, 1000.0 - 3.0 * BRIDGE_COST[0] as f32);
|
||||
// the far bank can be reached on foot now
|
||||
let (cx, _) = g.map.core;
|
||||
assert!(g.map.path_to(idx(cx + 4, cy), idx(rx + 3, cy)).is_some() || !g.map.walk(rx + 3, cy));
|
||||
// selling a bridge tile: units on it swim back to a bank, half the cost is refunded
|
||||
let id = g.spawn(EK::Kobold, false, rx as f32 + 1.5, cy as f32 + 0.5, 1);
|
||||
g.sell_rect(rx + 1, cy, rx + 1, cy);
|
||||
assert_eq!(g.map.t[idx(rx + 1, cy)].kind, Kind::Water);
|
||||
let e = g.ents.iter().find(|e| e.id == id).unwrap();
|
||||
assert!(e.alive && g.map.walk(e.x as i32, e.y as i32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lava_burns_and_needs_stone_bridges() {
|
||||
let (mut g, rx) = river_game(true);
|
||||
let (_, cy) = g.map.core;
|
||||
g.gold = 1000.0;
|
||||
g.build_bridges(rx - 1, cy, rx, cy);
|
||||
assert_eq!(g.map.t[idx(rx, cy)].kind, Kind::LavaBridge);
|
||||
assert_eq!(g.gold, 1000.0 - BRIDGE_COST[1] as f32);
|
||||
// a hero pushed off the bridge into the lava burns, the bounty is paid
|
||||
let id = g.spawn(EK::Knight, true, rx as f32 + 0.5, cy as f32 + 0.5, 1);
|
||||
let j = g.ents.iter().position(|e| e.id == id).unwrap();
|
||||
let gold = g.gold;
|
||||
g.knock(j, 0.0, 1.0);
|
||||
assert!(!g.ents[j].alive);
|
||||
assert!(g.gold > gold);
|
||||
// removing the stone bridge under a unit burns it as well
|
||||
let id = g.spawn(EK::Kobold, false, rx as f32 + 0.5, cy as f32 + 0.5, 1);
|
||||
g.sell_rect(rx, cy, rx, cy);
|
||||
assert!(!g.ents.iter().any(|e| e.id == id && e.alive));
|
||||
// heroes cannot path through lava
|
||||
assert_eq!(g.map.hero_cost()(idx(rx + 1, cy)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn some_imps_always_claim() {
|
||||
// lots of digging work: still at least 10% (min. one) of the imps claim the dug-out floor
|
||||
|
||||
Reference in new issue
Block a user