Steel walls: plate earth walls next to own ground with steel

- New tool Steel (key O, orders category; campaign from mission 11): 80 gold per earth wall,
  40 for a reinforced wall; riveted iron plate texture
- Heroes dig through steel 20 times slower than through earth and avoid it (path cost 300)
- Selling turns a steel wall back into a reinforced wall and refunds half the plating
- Texts EN/DE, test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 13:10:04 +01:00
1 parent f298e13d6e
commit 8853d84564
10 files changed
+156 -16

No files matched your search

+2 -2
View File
@@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize};
// From which mission (0-based) on each tool is available (same order as ui::TOOLS):
// dig, hand, treasury, lair, farm, training, library, sell, flag, lightning, heal, imp,
// torture, temple, arrow tower, cannon tower, magic tower, bridge, aerie
pub const TOOL_UNLOCK: [usize; 19] = [0, 0, 0, 0, 0, 2, 4, 0, 0, 3, 4, 0, 6, 9, 3, 5, 9, 1, 6];
// torture, temple, arrow tower, cannon tower, magic tower, bridge, aerie, steel
pub const TOOL_UNLOCK: [usize; 20] = [0, 0, 0, 0, 0, 2, 4, 0, 0, 3, 4, 0, 6, 9, 3, 5, 9, 1, 6, 10];
pub struct FortDef {
pub at: (f32, f32), // centre as a fraction of the map size
+81 -1
View File
@@ -418,6 +418,9 @@ pub const ROOM_NAMES: [&str; 8] = ["room.treasury", "room.lair", "room.farm", "r
// bridge cost per tile: over water, over lava
pub const BRIDGE_COST: [i32; 2] = [30, 75];
// steel wall cost per tile: from earth, from a reinforced wall
pub const STEEL_COST: [i32; 2] = [80, 40];
pub const STEEL_HP: f32 = 40.0;
pub fn room_index(k: Kind) -> Option<usize> {
ROOM_KINDS.iter().position(|&r| r == k)
@@ -1399,7 +1402,13 @@ impl Game {
EK::Dwarf => 3.0,
EK::Paladin => 2.0,
_ => 1.0,
} * if tile.reinforced { 0.2 } else { 1.0 };
} * if tile.kind == Kind::Steel {
0.05
} else if tile.reinforced {
0.2
} else {
1.0
};
self.fx(6, tx as f32 + 0.5, ty as f32 + 0.5, 0.0, 0.0, 0.4, 0xff3a5a7a);
self.sfx(Sfx::Dig, tx as f32 + 0.5, ty as f32 + 0.5);
let t = &mut self.map.t[next];
@@ -1780,6 +1789,48 @@ impl Game {
// 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)
// Earth walls next to own ground become steel walls
pub fn build_steel(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
let (mut built, mut broke) = (0, false);
for y in y0.min(y1)..=y0.max(y1) {
for x in x0.min(x1)..=x0.max(x1) {
if !inb(x, y) {
continue;
}
let t = self.map.t[idx(x, y)];
if t.kind != Kind::Earth || t.owner == Owner::Hero {
continue;
}
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 {
continue;
}
let cost = STEEL_COST[t.reinforced as usize] as f32;
if self.gold < cost {
broke = true;
continue;
}
self.gold -= cost;
let tl = &mut self.map.t[idx(x, y)];
tl.kind = Kind::Steel;
tl.reinforced = true;
tl.marked = false;
tl.hp = STEEL_HP;
built += 1;
self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.5, 0xffd0d0d0);
if built == 1 {
self.sfx(Sfx::Reinforce, x as f32 + 0.5, y as f32 + 0.5);
}
}
}
if broke {
self.msg("msg.no_gold", vec![]);
self.sfx_ui(Sfx::Error);
} else if built == 0 {
self.hint(23, "hint.steel_where");
}
}
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 {
@@ -3361,6 +3412,13 @@ impl Game {
continue;
}
let t = &mut self.map.t[i];
if t.kind == Kind::Steel {
t.kind = Kind::Earth;
t.reinforced = true;
t.hp = 12.0;
refund += STEEL_COST[1] as f32 * 0.5;
continue;
}
if t.owner == Owner::Player && t.bridge() {
let lava = t.kind == Kind::LavaBridge;
t.kind = if lava { Kind::Lava } else { Kind::Water };
@@ -4637,6 +4695,28 @@ mod tests {
assert!(seen.iter().all(|&k| flies(k)), "no lair: only flyers come ({:?})", seen);
}
#[test]
fn steel_walls_hold_heroes_back() {
let mut g = Game::new(3, 64, 64);
let (cx, cy) = g.map.core;
let (wx, wy) = (cx + 6, (cy - 3..=cy + 5).find(|&y| g.map.t[idx(cx + 6, y)].kind == Kind::Earth).unwrap());
g.gold = 1000.0;
g.build_steel(wx, wy, wx, wy);
assert_eq!(g.map.t[idx(wx, wy)].kind, Kind::Steel);
assert_eq!(g.gold, 1000.0 - STEEL_COST[0] as f32);
// far from own ground: nothing happens
g.build_steel(wx + 5, wy, wx + 5, wy);
assert_ne!(g.map.t[idx(wx + 5, wy)].kind, Kind::Steel);
// heroes avoid it and dig 20 times slower than through earth
assert_eq!(g.map.hero_cost()(idx(wx, wy)), Some(300));
// selling: a reinforced earth wall again, half of the plating back
let gold = g.gold;
g.sell_rect(wx, wy, wx, wy);
assert_eq!(g.map.t[idx(wx, wy)].kind, Kind::Earth);
assert!(g.map.t[idx(wx, wy)].reinforced);
assert!(g.gold > gold);
}
#[test]
fn imps_reinforce_walls() {
let mut g = Game::new(3, 64, 64);
+28
View File
@@ -65,6 +65,8 @@ pub const D_DIAMOND: u32 = 123;
pub const D_RUBBLE: u32 = 124; // floor decals: pebbles and fragments
pub const D_CRACK: u32 = 125; // a long crack through several stones
pub const T_AERIE: u32 = 126; // 4 variants: straw, twigs and feathers
pub const T_STEEL: u32 = 130; // 4 variants: riveted iron plates
pub const I_STEEL: u32 = 134;
pub struct Gfx {
pub atlas: Texture2D,
@@ -744,6 +746,31 @@ fn gen_atlas() -> Image {
}
});
}
// steel walls: riveted plates with a little rust
for v in 0..4u32 {
cv.cell(T_STEEL + v, |x, y| {
let seam = x % 8 == 0 || y % 8 == 0;
let rivet = (x % 8 == 2 || x % 8 == 6) && (y % 8 == 2 || y % 8 == 6);
let rust = rnd(x / 2, y / 2, v + 240) > 0.86;
if seam {
Some(c(44, 48, 58))
} else if rivet {
Some(c(170, 176, 190))
} else if rust {
Some(shade((120, 70, 40), (rnd(x, y, v + 241) * 16.0) as i32))
} else {
Some(shade((96, 102, 116), (rnd(x, y, v + 242) * 10.0) as i32 - 5 + if y % 8 == 1 { 10 } else { 0 }))
}
});
}
cv.cell(I_STEEL, |x, y| {
if (2..=13).contains(&x) && (3..=12).contains(&y) {
let rivet = (x == 4 || x == 11) && (y == 5 || y == 10);
Some(if rivet { c(220, 224, 235) } else if x == 2 || y == 3 || x == 13 || y == 12 { c(60, 64, 76) } else { c(130, 136, 150) })
} else {
None
}
});
// floor decals
cv.cell(D_RUBBLE, |x, y| {
for k in 0..9 {
@@ -1229,6 +1256,7 @@ impl Gfx {
Kind::LavaBridge => T_STONEBRIDGE + (v & 1),
Kind::Shard => T_HEROFLOOR + v,
Kind::Aerie => T_AERIE + v,
Kind::Steel => T_STEEL + v,
}
}
+5 -3
View File
@@ -68,7 +68,7 @@ struct Input {
// Tile under the pointer for a tool: digging (and the hand) aim at walls too,
// building and everything else only at the floor below the pointer
fn tool_tile(cam: &Cam, map: &map::Map, tool: Tool, mx: f32, my: f32) -> Option<(i32, i32)> {
if matches!(tool, Tool::Dig | Tool::Hand) {
if matches!(tool, Tool::Dig | Tool::Hand | Tool::Steel) {
return cam.pick_tile(map, mx, my).map(|(x, z, _)| (x, z));
}
let p = cam.ground_point(mx, my);
@@ -81,7 +81,7 @@ fn tool_tile(cam: &Cam, map: &map::Map, tool: Tool, mx: f32, my: f32) -> Option<
}
fn area_tool(t: Tool) -> bool {
matches!(t, Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge)
matches!(t, Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge | Tool::Steel)
}
// Use the active tool on the world at (mx, my) (CSS pixels): a click, or a tap on touch screens
@@ -94,7 +94,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 | Tool::Bridge, Some(t)) => inp.ldown = Some(t),
(Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge | Tool::Steel, Some(t)) => inp.ldown = Some(t),
(Tool::Hand, _) => {
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
if slap {
@@ -127,6 +127,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),
Tool::Steel => app.game.build_steel(sx, sy, ex, ey),
_ => {}
}
}
@@ -900,6 +901,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
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::Steel => (Color::new(0.75, 0.8, 0.9, 1.0), None),
Tool::Tower(_) => (Color::new(1.0, 0.6, 0.25, 1.0), None),
};
let ov = Overlay {
+6 -3
View File
@@ -58,6 +58,7 @@ pub enum Kind {
Ruby, // ruby vein: rare, valuable
Diamond, // diamond vein: very rare, very valuable
Aerie, // room for flying creatures (their beds)
Steel, // steel wall: hardly anyone gets through
}
// options for the map generator (campaign missions)
@@ -86,7 +87,7 @@ pub struct River {
}
// order matters for the packed savegame codec: only append new kinds
const KINDS: [Kind; 24] = [
const KINDS: [Kind; 25] = [
Kind::Rock,
Kind::Earth,
Kind::Gold,
@@ -111,6 +112,7 @@ const KINDS: [Kind; 24] = [
Kind::Ruby,
Kind::Diamond,
Kind::Aerie,
Kind::Steel,
];
// treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond
@@ -159,10 +161,10 @@ impl Tile {
Tile { kind, owner: Owner::None, hp: 0.0, gold: 0, marked: false, reserved: 0, food: 0.0, bed: 0, var: 0, reinforced: false, res: 0 }
}
pub fn solid(&self) -> bool {
matches!(self.kind, Kind::Rock | Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond)
matches!(self.kind, Kind::Rock | Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond | Kind::Steel)
}
pub fn diggable(&self) -> bool {
matches!(self.kind, Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond)
matches!(self.kind, Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond | Kind::Steel)
}
// the treasure of a vein (None for everything else)
pub fn vein(&self) -> Option<u8> {
@@ -637,6 +639,7 @@ impl Map {
let tl = &self.t[i];
match tl.kind {
Kind::Earth if tl.reinforced => Some(70),
Kind::Steel => Some(300),
Kind::Earth => Some(7),
Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond => Some(12),
Kind::Rock | Kind::Core | Kind::Tower | Kind::Water | Kind::Lava => None,
+1
View File
@@ -1717,6 +1717,7 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Kind::LavaBridge => [130, 120, 115, 255],
Kind::Shard => [255, 60, 120, 255],
Kind::Aerie => [170, 140, 90, 255],
Kind::Steel => [150, 160, 180, 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..19, same order as ui::TOOLS
Tool(usize), // 0..20, 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..19).map(Action::Tool).collect();
let mut v: Vec<Action> = (0..20).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, U, Y][i.min(18)],
Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V, K, J, B, C, T, U, Y, O][i.min(19)],
Action::CamForward => W,
Action::CamBack => S,
Action::CamLeft => A,
+10 -2
View File
@@ -156,10 +156,11 @@ pub enum Tool {
Summon,
Tower(TowerKind),
Bridge,
Steel,
}
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
pub const TOOLS: [Tool; 19] = [
pub const TOOLS: [Tool; 20] = [
Tool::Dig,
Tool::Hand,
Tool::Room(0),
@@ -179,6 +180,7 @@ pub const TOOLS: [Tool; 19] = [
Tool::Tower(TowerKind::Magic),
Tool::Bridge,
Tool::Room(7),
Tool::Steel,
];
@@ -210,6 +212,7 @@ pub fn tool_info(t: Tool) -> ToolInfo {
Tool::Heal => ("heal", I_HEAL, None),
Tool::Summon => ("summon", I_SUMMON, None),
Tool::Bridge => ("bridge", I_BRIDGE, None),
Tool::Steel => ("steel", I_STEEL, None),
};
let name = match room {
Some(r) => tr(ROOM_NAMES[r]),
@@ -225,6 +228,7 @@ pub fn tool_cost(t: Tool, g: &Game) -> String {
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::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_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()]);
@@ -244,7 +248,7 @@ pub const CATS: [(&str, &[usize]); 6] = [
("cat.rooms", &[2, 3, 4, 5, 6, 12, 13, 18, 17]),
("cat.towers", &[14, 15, 16]),
("cat.spells", &[9, 10, 11]),
("cat.orders", &[8, 7]),
("cat.orders", &[8, 7, 19]),
];
#[derive(Clone, Copy, PartialEq, Debug)]
@@ -1061,6 +1065,10 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
};
match t.kind {
Kind::Rock => out.push((tr("tile.rock"), DIM)),
Kind::Steel => {
out.push((tr("tile.steel"), FRAME_HI));
out.push((tr("tile.steel_info"), DIM));
}
Kind::Earth if t.reinforced => {
out.push((tr("tile.reinforced"), FRAME_HI));
out.push((tr("tile.reinforced_info"), DIM));