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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 10:32:40 +01:00
1 parent af6be62805
commit 75dc445d3b
9 files changed
+536 -22

No files matched your search

+218 -8
View File
@@ -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
+117 -1
View File
@@ -6,7 +6,7 @@ use macroquad::prelude::*;
pub const CELL: f32 = 16.0;
const ATLAS_COLS: u32 = 16;
const ATLAS_ROWS: u32 = 6;
const ATLAS_ROWS: u32 = 8;
// atlas indices (4 variants each)
pub const T_ROCK: u32 = 0;
@@ -48,6 +48,14 @@ pub const I_CANNON: u32 = 82;
pub const I_MAGIC: u32 = 83;
pub const T_TEMPLE: u32 = 84; // 4 variants
pub const D_ALTAR: u32 = 88;
pub const T_WATER: u32 = 96; // 4 variants
pub const T_LAVA: u32 = 100; // 4 variants
pub const T_BRIDGE: u32 = 104; // 2 variants: wooden planks
pub const T_STONEBRIDGE: u32 = 106; // 2 variants: stone slabs (over lava)
pub const T_FORTWALL: u32 = 108; // 4 variants: knight fortress masonry
pub const D_SHARD: u32 = 112; // shard of the soul core
pub const D_BANNER: u32 = 113; // knight banner
pub const I_BRIDGE: u32 = 114;
pub struct Gfx {
pub atlas: Texture2D,
@@ -533,6 +541,108 @@ fn gen_atlas() -> Image {
None
}
});
// ---- campaign terrain
for v in 0..4u32 {
// water: dark blue with lighter wave crests
cv.cell(T_WATER + v, |x, y| {
let wave = ((x as f32 * 0.8 + (y as f32 * 0.9 + v as f32 * 1.7).sin() * 2.2) as i32 + y * 2) % 9;
let d = (rnd(x, y, v + 120) * 10.0) as i32 - 5;
Some(match wave {
0 => shade((70, 120, 170), d),
1 => shade((50, 92, 140), d),
_ => shade((24, 50, 88), d),
})
});
// lava: glowing orange with dark crust plates
cv.cell(T_LAVA + v, |x, y| {
let crust = rnd(x / 3 + (y / 3) % 2, y / 3, v + 130);
let edge = rnd(x, y, v + 131) * 0.25;
if crust + edge > 0.85 {
Some(shade((60, 22, 14), (rnd(x, y, v + 132) * 14.0) as i32))
} else if crust + edge > 0.7 {
Some(c(200, 60, 20))
} else if hash(x, y, v + 133) % 11 == 0 {
Some(c(255, 230, 120))
} else {
Some(shade((250, 130, 30), (rnd(x, y, v + 134) * 30.0) as i32 - 15))
}
});
// knight fortress walls: pale stone blocks with blue-grey mortar
cv.cell(T_FORTWALL + v, |x, y| {
let row = y / 5;
let off = if row % 2 == 1 { 5 } else { 0 };
if y % 5 == 4 || (x + off) % 10 == 9 {
return Some(shade((70, 76, 92), (rnd(x, y, v + 140) * 8.0) as i32));
}
let blk = hash((x + off) / 10, row, v + 141) % 4;
Some(shade((150, 150, 160), (rnd(x, y, v + 142) * 12.0) as i32 - 6 + blk as i32 * 5 - 8 + if y % 5 == 0 { 10 } else { 0 }))
});
}
for v in 0..2u32 {
// wooden bridge: planks across, two rope rails at the sides
cv.cell(T_BRIDGE + v, |x, y| {
if x <= 1 || x >= 14 {
return Some(if (y + v as i32) % 4 == 0 { c(150, 120, 80) } else { c(96, 70, 42) });
}
if y % 4 == 0 {
return Some(c(40, 26, 16));
}
let plank = y / 4;
let off = (hash(plank, 1, v + 150) % 16) as i32;
if (x + off) % 13 == 0 && y % 4 == 2 {
return Some(c(60, 40, 24)); // nail
}
Some(shade((126, 88, 52), (rnd(x, y, v + 151) * 14.0) as i32 - 7 + (plank % 2) * 6))
});
// stone bridge over lava: big grey slabs, sooty edges
cv.cell(T_STONEBRIDGE + v, |x, y| {
if x <= 1 || x >= 14 {
return Some(shade((60, 56, 56), (rnd(x, y, v + 160) * 10.0) as i32));
}
Some(stones(x, y, v + 161, (104, 98, 96), (46, 40, 40), 12))
});
}
// shard of the soul core: a glowing red crystal
cv.cell(D_SHARD, |x, y| {
let (dx, dy) = ((x - 8).abs(), y);
if y >= 2 && y <= 14 && dx * 3 <= (if y < 7 { (y - 2) * 4 } else { (15 - y) * 2 }) {
Some(if dx == 0 || (x + y) % 5 == 0 { c(255, 170, 170) } else if x < 8 { c(230, 50, 60) } else { c(160, 20, 40) })
} else if dy == 15 && (4..=12).contains(&x) {
Some(c(80, 30, 40))
} else {
None
}
});
// knight banner: pole with a white-blue banner and a golden sun
cv.cell(D_BANNER, |x, y| {
if x == 3 {
return Some(c(170, 170, 180));
}
if (4..=13).contains(&x) && (1..=11).contains(&y) {
let (dx, dy) = (x - 9, y - 6);
if dx * dx + dy * dy <= 5 {
return Some(c(240, 200, 60));
}
return Some(if y == 11 && x % 2 == 0 { c(60, 90, 170) } else if x % 5 == 4 { c(200, 205, 220) } else { c(230, 232, 240) });
}
None
});
// bridge tool icon: an arch with planks
cv.cell(I_BRIDGE, |x, y| {
let (dx, dy) = (x as f32 - 7.5, y as f32 - 13.0);
let d = (dx * dx + dy * dy).sqrt();
if y <= 6 && y >= 5 && (1..=14).contains(&x) {
Some(if x % 3 == 0 { c(70, 46, 26) } else { c(150, 106, 60) })
} else if (6.0..7.5).contains(&d) && y >= 6 {
Some(c(120, 84, 48))
} else if y > 6 && (x == 1 || x == 14) {
Some(c(110, 76, 44))
} else if y >= 12 && d >= 7.5 && (2..=13).contains(&x) {
Some(c(60, 110, 170))
} else {
None
}
});
cv.img
}
@@ -896,6 +1006,7 @@ impl Gfx {
let v = var as u32 & 3;
match kind {
Kind::Rock => T_ROCK + v,
Kind::Earth if reinforced && hero => T_FORTWALL + v,
Kind::Earth if reinforced => T_REINF + v,
Kind::Earth => T_EARTH + v,
Kind::Gold => T_GOLD + v,
@@ -919,6 +1030,11 @@ impl Gfx {
Kind::Torture => T_TORTURE + v,
Kind::Temple => T_TEMPLE + v,
Kind::Tower => T_CLAIM + v,
Kind::Water => T_WATER + v,
Kind::Lava => T_LAVA + v,
Kind::Bridge => T_BRIDGE + (v & 1),
Kind::LavaBridge => T_STONEBRIDGE + (v & 1),
Kind::Shard => T_HEROFLOOR + v,
}
}
+4 -2
View File
@@ -63,7 +63,7 @@ struct Input {
}
fn area_tool(t: Tool) -> bool {
matches!(t, Tool::Dig | Tool::Room(_) | Tool::Sell)
matches!(t, Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge)
}
// Use the active tool on the world at (mx, my) (CSS pixels): a click, or a tap on touch screens
@@ -76,7 +76,7 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32,
let ground = app.cam.ground_point(mx, my);
let (wx, wy) = (ground.x, ground.y);
match (ui.tool, tile) {
(Tool::Dig | Tool::Room(_) | Tool::Sell, Some(t)) => inp.ldown = Some(t),
(Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge, Some(t)) => inp.ldown = Some(t),
(Tool::Hand, _) => {
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
if slap {
@@ -108,6 +108,7 @@ fn finish_rect(app: &mut App, tool: Tool, start: (i32, i32), mx: f32, my: f32) {
}
Tool::Room(r) => app.game.build_rect(r, sx, sy, ex, ey),
Tool::Sell => app.game.sell_rect(sx, sy, ex, ey),
Tool::Bridge => app.game.build_bridges(sx, sy, ex, ey),
_ => {}
}
}
@@ -812,6 +813,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
Tool::Lightning => (Color::new(1.0, 1.0, 0.6, 1.0), Some((ground.x, ground.y, 1.6))),
Tool::Heal => (Color::new(0.4, 1.0, 0.5, 1.0), Some((ground.x, ground.y, 2.55))),
Tool::Summon => (Color::new(0.8, 0.5, 1.0, 1.0), None),
Tool::Bridge => (Color::new(0.9, 0.7, 0.4, 1.0), None),
Tool::Tower(_) => (Color::new(1.0, 0.6, 0.25, 1.0), None),
};
let ov = Overlay {
+40 -3
View File
@@ -49,10 +49,15 @@ pub enum Kind {
Torture,
Tower, // floor tile occupied by a defence tower (not walkable)
Temple,
Water, // underground river: not walkable, needs a bridge
Lava, // lava river: not walkable, burns what falls in
Bridge, // wooden bridge over water (walkable)
LavaBridge, // stone bridge over lava (walkable)
Shard, // floor with a shard of the soul core (campaign)
}
// order matters for the packed savegame codec: only append new kinds
const KINDS: [Kind; 15] = [
const KINDS: [Kind; 20] = [
Kind::Rock,
Kind::Earth,
Kind::Gold,
@@ -68,6 +73,11 @@ const KINDS: [Kind; 15] = [
Kind::Torture,
Kind::Tower,
Kind::Temple,
Kind::Water,
Kind::Lava,
Kind::Bridge,
Kind::LavaBridge,
Kind::Shard,
];
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
@@ -117,9 +127,36 @@ impl Tile {
pub fn walkable(&self) -> bool {
matches!(
self.kind,
Kind::Floor | Kind::Treasury | Kind::Lair | Kind::Farm | Kind::Training | Kind::Library | Kind::Portal | Kind::Gate | Kind::Torture | Kind::Temple
Kind::Floor
| Kind::Treasury
| Kind::Lair
| Kind::Farm
| Kind::Training
| Kind::Library
| Kind::Portal
| Kind::Gate
| Kind::Torture
| Kind::Temple
| Kind::Bridge
| Kind::LavaBridge
| Kind::Shard
)
}
// river tiles (open, but nobody can walk there)
pub fn liquid(&self) -> bool {
matches!(self.kind, Kind::Water | Kind::Lava)
}
pub fn bridge(&self) -> bool {
matches!(self.kind, Kind::Bridge | Kind::LavaBridge)
}
// the river below a bridge or river tile
pub fn river(&self) -> Option<Kind> {
match self.kind {
Kind::Water | Kind::Bridge => Some(Kind::Water),
Kind::Lava | Kind::LavaBridge => Some(Kind::Lava),
_ => None,
}
}
}
pub fn idx(x: i32, y: i32) -> usize {
@@ -441,7 +478,7 @@ impl Map {
Kind::Earth if tl.reinforced => Some(70),
Kind::Earth => Some(7),
Kind::Gold => Some(12),
Kind::Rock | Kind::Core | Kind::Tower => None,
Kind::Rock | Kind::Core | Kind::Tower | Kind::Water | Kind::Lava => None,
_ => Some(1),
}
}
+94 -1
View File
@@ -14,7 +14,7 @@ pub const WALL_H: f32 = 0.95;
const CHUNK: i32 = 16;
pub const MAX_LIGHTS: usize = 24;
const ATLAS_W: f32 = 256.0;
const ATLAS_H: f32 = 96.0;
const ATLAS_H: f32 = 128.0;
const DISP_AMP: f32 = 0.11; // wall surface irregularity
// ------------------------------------------------------------------ camera
@@ -358,6 +358,9 @@ fn ambient_field(map: &Map) -> Vec<f32> {
(false, Owner::Hero) => 0.6,
_ => 0.0,
};
if t.river() == Some(Kind::Lava) {
f[i] = f[i].max(0.8); // lava lights up its surroundings
}
if t.kind == Kind::Core {
f[i] = 1.0;
}
@@ -401,6 +404,8 @@ fn solid_at(map: &Map, x: i32, z: i32) -> bool {
}
const OUTLINE_W: f32 = 0.035;
const LIQ_D: f32 = 0.3; // depth of the river surface below the floor
const DECK_Y: f32 = 0.03; // height of bridge decks
const OUTLINE_C: [u8; 4] = [8, 5, 6, 150];
// Displaced points along a straight edge (same displacement as the wall grids -> lines follow the surface)
@@ -487,6 +492,75 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas:
let tex = Gfx::tile_tex(t.kind, t.owner == Owner::Player, t.owner == Owner::Hero, t.var, t.reinforced);
let (ua, ub) = cell_uv(tex);
let (xf, zf) = (x as f32, z as f32);
if let Some(river) = t.river() {
// ---- river bed: sunken surface, banks where the ground or a wall begins
let lava = river == Kind::Lava;
let (la, lb) = cell_uv(if lava { T_LAVA } else { T_WATER } + (t.var as u32 & 3));
let a = amb_at(amb, xf + 0.5, zf + 0.5);
let lc = if lava { col(vec3(1.0, 0.95, 0.9), 1.0) } else { col(Vec3::splat(0.25 + a * 0.9), 1.0) };
mb.quad(
[vec3(xf, -LIQ_D, zf), vec3(xf + 1.0, -LIQ_D, zf), vec3(xf + 1.0, -LIQ_D, zf + 1.0), vec3(xf, -LIQ_D, zf + 1.0)],
[vec2(la.x, la.y), vec2(lb.x, la.y), vec2(lb.x, lb.y), vec2(la.x, lb.y)],
[lc; 4],
[Vec3::Y; 4],
);
let (fa, fb) = cell_uv(T_FACE_EARTH);
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
if map.get(x + dx, z + dz).map_or(false, |o| o.river().is_some()) {
continue;
}
// edge seen from inside the river: from a to b, facing back into this tile
let (p, q) = match d {
0 => (vec3(xf, 0.0, zf), vec3(xf + 1.0, 0.0, zf)),
1 => (vec3(xf + 1.0, 0.0, zf), vec3(xf + 1.0, 0.0, zf + 1.0)),
2 => (vec3(xf + 1.0, 0.0, zf + 1.0), vec3(xf, 0.0, zf + 1.0)),
_ => (vec3(xf, 0.0, zf + 1.0), vec3(xf, 0.0, zf)),
};
let inward = vec3(-dx as f32, 0.0, -dz as f32);
let bc = col(Vec3::splat(if lava { 0.7 } else { 0.18 + a * 0.5 }), 1.0);
mb.quad(
[p, q, q - Vec3::Y * LIQ_D, p - Vec3::Y * LIQ_D],
[vec2(fa.x, fa.y), vec2(fb.x, fa.y), vec2(fb.x, fb.y), vec2(fa.x, fb.y)],
[bc; 4],
[inward; 4],
);
}
if t.bridge() {
// deck: planks across the direction of the bridge, rails along it
let run_x = |dx: i32| map.get(x + dx, z).map_or(false, |o| o.bridge() || (o.walkable() && !o.bridge()));
let run_z = |dz: i32| map.get(x, z + dz).map_or(false, |o| o.bridge() || (o.walkable() && !o.bridge()));
let along_x = (run_x(-1) as i32 + run_x(1) as i32) > (run_z(-1) as i32 + run_z(1) as i32);
let (ba, bb) = cell_uv(tex);
let dc = col(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5).max(if lava { 0.6 } else { 0.0 })), 1.0);
let uv = if along_x {
[vec2(ba.x, ba.y), vec2(ba.x, bb.y), vec2(bb.x, bb.y), vec2(bb.x, ba.y)]
} else {
[vec2(ba.x, ba.y), vec2(bb.x, ba.y), vec2(bb.x, bb.y), vec2(ba.x, bb.y)]
};
mb.quad([vec3(xf, DECK_Y, zf), vec3(xf + 1.0, DECK_Y, zf), vec3(xf + 1.0, DECK_Y, zf + 1.0), vec3(xf, DECK_Y, zf + 1.0)], uv, [dc; 4], [Vec3::Y; 4]);
// deck edges above open river
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
if !map.get(x + dx, z + dz).map_or(false, |o| o.liquid()) {
continue;
}
let (p, q) = match d {
0 => (vec3(xf + 1.0, DECK_Y, zf), vec3(xf, DECK_Y, zf)),
1 => (vec3(xf + 1.0, DECK_Y, zf + 1.0), vec3(xf + 1.0, DECK_Y, zf)),
2 => (vec3(xf, DECK_Y, zf + 1.0), vec3(xf + 1.0, DECK_Y, zf + 1.0)),
_ => (vec3(xf, DECK_Y, zf), vec3(xf, DECK_Y, zf + 1.0)),
};
let out = vec3(dx as f32, 0.0, dz as f32);
let ec = col(Vec3::splat(0.3), 1.0);
mb.quad([p, q, q - Vec3::Y * 0.14, p - Vec3::Y * 0.14], [vec2(ba.x, ba.y), vec2(bb.x, ba.y), vec2(bb.x, ba.y + 0.002), vec2(ba.x, ba.y + 0.002)], [ec; 4], [out; 4]);
ln.strip(p + out * 0.01, q + out * 0.01, Vec3::Y, OUTLINE_W, OUTLINE_C);
}
}
continue;
}
if t.kind == Kind::Shard {
let dc = col(Vec3::splat(1.0), 1.0);
mb.cross(xf + 0.5, zf + 0.5, 0.7, 0.8, D_SHARD, dc);
}
if !t.solid() {
// floor with corner ambient occlusion
let corners = [(0, 0), (1, 0), (1, 1), (0, 1)];
@@ -807,6 +881,20 @@ impl R3d {
let (gx, gz) = g.gate_center();
push(vec3(gx, 1.2, gz), 7.0, vec3(0.75, 0.85, 1.0), 1.0, 50.0);
}
// glowing lava near the camera (every third tile)
let r = (cam.zoom * 0.9) as i32;
let (ccx, ccz) = (cam.tx as i32, cam.tz as i32);
for z in (ccz - r).max(0)..(ccz + r).min(mh() as i32) {
if z % 3 != 0 {
continue;
}
for x in (ccx - r).max(0)..(ccx + r).min(mw() as i32) {
if x % 3 == 0 && g.map.t[idx(x, z)].kind == Kind::Lava {
let fl = 0.8 + 0.2 * (t * 2.0 + (x * 7 + z * 3) as f32).sin();
push(vec3(x as f32 + 0.5, 0.3, z as f32 + 0.5), 4.0, vec3(1.0, 0.45, 0.12), 0.9 * fl, 0.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);
@@ -1453,5 +1541,10 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Kind::Torture => [120, 40, 40, 255],
Kind::Tower => [255, 140, 60, 255],
Kind::Temple => [200, 110, 50, 255],
Kind::Water => [40, 80, 150, 255],
Kind::Lava => [250, 110, 30, 255],
Kind::Bridge => [150, 110, 60, 255],
Kind::LavaBridge => [130, 120, 115, 255],
Kind::Shard => [255, 60, 120, 255],
}
}
+3 -3
View File
@@ -7,7 +7,7 @@ use std::collections::BTreeMap;
// Rebindable keyboard actions. Esc (menu) and the arrow keys (camera) are fixed.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Action {
Tool(usize), // 0..17, same order as ui::TOOLS
Tool(usize), // 0..18, same order as ui::TOOLS
CamForward,
CamBack,
CamLeft,
@@ -25,7 +25,7 @@ pub enum Action {
}
pub fn all_actions() -> Vec<Action> {
let mut v: Vec<Action> = (0..17).map(Action::Tool).collect();
let mut v: Vec<Action> = (0..18).map(Action::Tool).collect();
v.extend_from_slice(&[
Action::CamForward,
Action::CamBack,
@@ -70,7 +70,7 @@ impl Action {
fn default_key(self) -> KeyCode {
use KeyCode::*;
match self {
Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V, K, J, B, C, T][i.min(16)],
Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V, K, J, B, C, T, U][i.min(17)],
Action::CamForward => W,
Action::CamBack => S,
Action::CamLeft => A,
+22 -2
View File
@@ -155,10 +155,11 @@ pub enum Tool {
Heal,
Summon,
Tower(TowerKind),
Bridge,
}
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
pub const TOOLS: [Tool; 17] = [
pub const TOOLS: [Tool; 18] = [
Tool::Dig,
Tool::Hand,
Tool::Room(0),
@@ -176,6 +177,7 @@ pub const TOOLS: [Tool; 17] = [
Tool::Tower(TowerKind::Arrow),
Tool::Tower(TowerKind::Cannon),
Tool::Tower(TowerKind::Magic),
Tool::Bridge,
];
@@ -205,6 +207,7 @@ pub fn tool_info(t: Tool) -> ToolInfo {
Tool::Lightning => ("lightning", I_BOLT, None),
Tool::Heal => ("heal", I_HEAL, None),
Tool::Summon => ("summon", I_SUMMON, None),
Tool::Bridge => ("bridge", I_BRIDGE, None),
};
let name = match room {
Some(r) => tr(ROOM_NAMES[r]),
@@ -219,6 +222,7 @@ pub fn tool_cost(t: Tool, g: &Game) -> String {
Tool::Lightning => trf("cost.mana", &["50".into()]),
Tool::Heal => trf("cost.mana", &["60".into()]),
Tool::Summon => trf("cost.mana", &[(g.summon_cost() as i32).to_string()]),
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), BRIDGE_COST[1].to_string()]),
Tool::Tower(k) => {
let s = tower_stats(k);
let mut c = trf("cost.tower", &[s.cost[0].to_string(), s.cost[1].to_string(), s.cost[2].to_string()]);
@@ -235,7 +239,7 @@ pub fn tool_cost(t: Tool, g: &Game) -> String {
pub const CATS: [(&str, &[usize]); 6] = [
("cat.dig", &[0]),
("cat.hand", &[1]),
("cat.rooms", &[2, 3, 4, 5, 6, 12, 13]),
("cat.rooms", &[2, 3, 4, 5, 6, 12, 13, 17]),
("cat.towers", &[14, 15, 16]),
("cat.spells", &[9, 10, 11]),
("cat.orders", &[8, 7]),
@@ -992,6 +996,22 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
}
}
}
Kind::Water => {
out.push((tr("tile.water"), BLUE));
out.push((tr("tile.water_info"), DIM));
}
Kind::Lava => {
out.push((tr("tile.lava"), RED));
out.push((tr("tile.lava_info"), DIM));
}
Kind::Bridge | Kind::LavaBridge => {
out.push((tr(if t.kind == Kind::Bridge { "tile.bridge" } else { "tile.lavabridge" }), FRAME_HI));
out.push((tr(owner), DIM));
}
Kind::Shard => {
out.push((tr("tile.shard"), RED));
out.push((tr("tile.shard_info"), DIM));
}
Kind::Gate => {
out.push((tr("tile.gate"), BLUE));
out.push((trf("info.hp", &[(g.gate_hp as i32).to_string(), (GATE_HP as i32).to_string()]), BLUE));