From 8853d84564d8b610829eea6febcddca7232d7fd6 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Tue, 3 Nov 2026 13:10:04 +0100 Subject: [PATCH] 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 --- lang/de.lang | 11 ++++++- lang/en.lang | 11 ++++++- src/campaign.rs | 4 +-- src/game.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++++++- src/gfx.rs | 28 +++++++++++++++++ src/main.rs | 8 +++-- src/map.rs | 9 ++++-- src/render3d.rs | 1 + src/settings.rs | 6 ++-- src/ui.rs | 12 ++++++-- 10 files changed, 156 insertions(+), 16 deletions(-) diff --git a/lang/de.lang b/lang/de.lang index 5694db0..6414449 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -145,7 +145,7 @@ help.k_slap = Shift+Links Hand: Rückhand für eigene, Faust für Helden au help.k_pan = Rechts ziehen Karte verschieben (auch WASD/Pfeile, Bildrand) help.k_rot = Mitte ziehen Kamera drehen und neigen (auch Q/E, R/F) help.k_zoom = Mausrad Zoomen -help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y Werkzeug +help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Werkzeug help.k_misc = P / Leertaste Pause M Ton an/aus +/- Lautstärke help.k_misc2 = G Kachel-Umrisse an/aus H Diese Hilfe help.s1 = 1. Grabe zum Portal und lass es von Grubenwichten erobern. @@ -529,3 +529,12 @@ unit.wyrm = Glutwyrm job.rest = Erholt sich im Horst job.torment = Bearbeitet einen Gefangenen menu.license = DungeonCraft - MIT-Lizenz - (c) 2026 Richard Zink + +# steel walls +tool.steel.label = Stahl +tool.steel.name = Stahlwände +tool.steel.desc = Beschlägt Erdwände an deinem Gebiet mit Stahl. Helden kommen kaum hindurch und nehmen lieber lange Umwege. Verkaufen macht wieder befestigte Wände daraus. +cost.steel = {0} Gold/Wand, {1} für befestigte Wände +hint.steel_where = Stahl lässt sich nur an Erdwände neben deinem Gebiet anbringen. +tile.steel = Stahlwand +tile.steel_info = Helden brauchen ewig, um hindurchzukommen. diff --git a/lang/en.lang b/lang/en.lang index e474f7d..39b5676 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -146,7 +146,7 @@ help.k_slap = Shift+Left Hand: backhand own units, punch heroes on your gro help.k_pan = Right drag Move the map (also WASD/arrows, screen edge) help.k_rot = Middle drag Rotate and tilt the camera (also Q/E, R/F) help.k_zoom = Mouse wheel Zoom -help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y Select tool +help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Select tool help.k_misc = P / Space Pause M Sound on/off +/- Volume help.k_misc2 = G Tile outlines on/off H This help help.s1 = 1. Dig to the portal and let your Pit Imps claim it. @@ -530,3 +530,12 @@ unit.wyrm = Ember Wyrm job.rest = Recovering in the aerie job.torment = Working on a prisoner menu.license = DungeonCraft - MIT License - (c) 2026 Richard Zink + +# steel walls +tool.steel.label = Steel +tool.steel.name = Steel Walls +tool.steel.desc = Plates earth walls next to your ground with steel. Heroes hardly get through and rather take long detours. Selling turns them back into reinforced walls. +cost.steel = {0} gold/wall, {1} for reinforced walls +hint.steel_where = Steel can only be put on earth walls next to your own ground. +tile.steel = Steel wall +tile.steel_info = Heroes need ages to get through. diff --git a/src/campaign.rs b/src/campaign.rs index 5bb0dec..0158803 100644 --- a/src/campaign.rs +++ b/src/campaign.rs @@ -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 diff --git a/src/game.rs b/src/game.rs index eeb40f7..437713b 100644 --- a/src/game.rs +++ b/src/game.rs @@ -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 { 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); diff --git a/src/gfx.rs b/src/gfx.rs index 5127d4d..c633901 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -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, } } diff --git a/src/main.rs b/src/main.rs index aa9db3b..c6bdb46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { diff --git a/src/map.rs b/src/map.rs index e785946..feddc21 100644 --- a/src/map.rs +++ b/src/map.rs @@ -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 { @@ -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, diff --git a/src/render3d.rs b/src/render3d.rs index 40c795e..5435ad9 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -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], } } diff --git a/src/settings.rs b/src/settings.rs index be56e69..993522d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -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 { - let mut v: Vec = (0..19).map(Action::Tool).collect(); + let mut v: Vec = (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, diff --git a/src/ui.rs b/src/ui.rs index 2b6cce2..6bc5ce0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -156,10 +156,11 @@ pub enum Tool { Summon, Tower(TowerKind), Bridge, + Steel, } // New tools are appended so that existing key bindings ("tool") 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, 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));