diff --git a/lang/de.lang b/lang/de.lang index bb747ff..3d12086 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -272,3 +272,8 @@ newgame.width = Kartenbreite newgame.height = Kartenhöhe newgame.start = Starten newgame.info = {0} Portal(e), {1} bewachte Schatzhöhle(n) + +# ---- Wände, Türme, Folter +job.reinforce = Verstärkt eine Wand +tile.reinforced = Verstärkte Wand +tile.reinforced_info = Helden kommen kaum durch diff --git a/lang/en.lang b/lang/en.lang index 785dc33..682a876 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -273,3 +273,8 @@ newgame.width = Map width newgame.height = Map height newgame.start = Start newgame.info = {0} portal(s), {1} guarded treasure cave(s) + +# ---- walls, towers, torture +job.reinforce = Reinforcing a wall +tile.reinforced = Reinforced wall +tile.reinforced_info = heroes can hardly break through diff --git a/src/game.rs b/src/game.rs index f6eb5cb..19df1bc 100644 --- a/src/game.rs +++ b/src/game.rs @@ -42,13 +42,19 @@ pub enum Sfx { Defeat, Click, Error, + Reinforce, + Cannon, + Magic, + Capture, + Convert, } -pub const SFX_ALL: [Sfx; 29] = [ +pub const SFX_ALL: [Sfx; 34] = [ Sfx::Dig, Sfx::GoldDig, Sfx::DigDone, Sfx::Claim, Sfx::Deposit, Sfx::Build, Sfx::Sell, Sfx::MeleeHit, Sfx::Shoot, Sfx::Death, Sfx::HeroDeath, Sfx::Lightning, Sfx::Heal, Sfx::Summon, Sfx::PortalArrive, Sfx::WaveAlarm, Sfx::WaveStart, Sfx::CoreHit, Sfx::GateHit, Sfx::GateDefenders, Sfx::LevelUp, Sfx::Pickup, Sfx::Drop, Sfx::Payday, Sfx::FlagSet, Sfx::Victory, Sfx::Defeat, Sfx::Click, Sfx::Error, + Sfx::Reinforce, Sfx::Cannon, Sfx::Magic, Sfx::Capture, Sfx::Convert, ]; impl Sfx { @@ -128,6 +134,7 @@ pub enum Job { AttackCore, Leave, Rally, + Reinforce(usize), // wall tile } #[derive(Clone, Serialize, Deserialize)] @@ -840,7 +847,7 @@ impl Game { EK::Dwarf => 3.0, EK::Paladin => 2.0, _ => 1.0, - }; + } * 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]; @@ -884,6 +891,7 @@ impl Game { t.kind = Kind::Floor; t.owner = Owner::None; t.marked = false; + t.reinforced = false; t.reserved = 0; t.hp = 0.0; t.gold = 0; @@ -1054,6 +1062,22 @@ impl Game { found } + // an earth wall next to own floor that can be reinforced from tile i + fn reinforce_neighbor(&self, i: usize, me: u32) -> Option { + if self.map.t[i].owner != Owner::Player { + return None; + } + let (x, y) = xy(i); + for &(dx, dy) in &DIR4 { + if let Some(t) = self.map.get(x + dx, y + dy) { + if t.kind == Kind::Earth && !t.reinforced && !t.marked && (t.reserved == 0 || t.reserved == me) { + return Some(idx(x + dx, y + dy)); + } + } + } + None + } + 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) { @@ -1147,6 +1171,27 @@ impl Game { } } } + Job::Reinforce(w) => { + let t = self.map.t[w]; + if !adj4(cur, w) || t.kind != Kind::Earth || t.reinforced || t.marked { + self.unreserve(me); + e.job = Job::None; + return; + } + let (wx, wy) = xy(w); + e.face = sgn(wx as f32 + 0.5 - e.x); + e.working = true; + e.timer += dt; + if e.timer >= 2.5 { + self.unreserve(me); + e.job = Job::None; + let tl = &mut self.map.t[w]; + tl.reinforced = true; + tl.hp = 12.0; // the player's imps need twice as long to dig it out again + self.fx(8, wx as f32 + 0.5, wy as f32 + 0.5, 0.0, 0.0, 0.5, 0xff4040e0); + self.sfx(Sfx::Reinforce, wx as f32 + 0.5, wy as f32 + 0.5); + } + } Job::Claim(t) => { if cur != t || !self.claimable(t, me) { self.unreserve(me); @@ -1229,6 +1274,16 @@ impl Game { e.job = Job::Claim(end); return; } + if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.reinforce_neighbor(i, me).is_some(), true) { + let end = p.last().copied().unwrap_or(cur); + if let Some(w) = self.reinforce_neighbor(end, me) { + self.map.t[w].reserved = me; + e.path = p; + e.job = Job::Reinforce(w); + e.timer = 0.0; + return; + } + } e.think = 1.0; self.wander(e, 3); } @@ -1875,6 +1930,7 @@ pub fn job_name(e: &Ent) -> &'static str { } } Job::Leave => "job.leave", + Job::Reinforce(_) => "job.reinforce", Job::Rally => { if e.working { "job.gate_attack" @@ -1962,4 +2018,21 @@ mod tests { } } } + + #[test] + fn imps_reinforce_walls() { + let mut g = Game::new(3, 64, 64); + for _ in 0..(120 * 20) { + g.update(0.05); + } + let n = g.map.t.iter().filter(|t| t.reinforced).count(); + println!("reinforced walls after 2 minutes: {}", n); + assert!(n >= 10); + // heroes avoid them: the hero path cost through reinforced earth is 10x + let (cx, cy) = g.map.core; + let w = (cx - 4, cy); + if g.map.t[idx(w.0, w.1)].reinforced { + assert_eq!(g.map.hero_cost()(idx(w.0, w.1)), Some(70)); + } + } } diff --git a/src/gfx.rs b/src/gfx.rs index 67028ef..451b07f 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -6,7 +6,7 @@ use macroquad::prelude::*; pub const CELL: f32 = 16.0; const ATLAS_COLS: u32 = 16; -const ATLAS_ROWS: u32 = 5; +const ATLAS_ROWS: u32 = 6; // atlas indices (4 variants each) pub const T_ROCK: u32 = 0; @@ -40,6 +40,12 @@ pub const I_FLAG: u32 = 68; pub const I_BOLT: u32 = 69; pub const I_HEAL: u32 = 70; pub const I_SUMMON: u32 = 71; +pub const T_REINF: u32 = 72; // 4 variants: reinforced wall masonry +pub const T_TORTURE: u32 = 76; // 4 variants: torture chamber floor +pub const D_RACK: u32 = 80; +pub const I_ARROW: u32 = 81; +pub const I_CANNON: u32 = 82; +pub const I_MAGIC: u32 = 83; pub struct Gfx { pub atlas: Texture2D, @@ -365,6 +371,40 @@ fn gen_atlas() -> Image { _ => c(100, 60, 120), }) }); + // reinforced walls: masonry in dark stone with the player's red mortar + for v in 0..4u32 { + cv.cell(T_REINF + v, |x, y| { + let row = y / 4; + let off = if row % 2 == 1 { 4 } else { 0 }; + if y % 4 == 3 || (x + off) % 8 == 7 { + return Some(shade((70, 26, 30), (rnd(x, y, v + 90) * 8.0) as i32)); + } + let brick = hash((x + off) / 8, row, v + 91) % 4; + Some(shade((84, 76, 74), (rnd(x, y, v + 92) * 14.0) as i32 - 7 + brick as i32 * 5 - 6 + if y % 4 == 0 { 10 } else { 0 })) + }); + // torture chamber: dark stones with rusty iron grates + cv.cell(T_TORTURE + v, |x, y| { + let grate = (4..=11).contains(&x) && (4..=11).contains(&y) && (x % 3 == 1 || y % 3 == 1); + if grate { + return Some(shade((96, 44, 30), (rnd(x, y, v + 93) * 12.0) as i32)); + } + Some(stones(x, y, v + 94, (52, 40, 44), (26, 16, 18), 10)) + }); + } + // torture wheel + cv.cell(D_RACK, |x, y| { + let (dx, dy) = (x as f32 - 7.5, y as f32 - 7.0); + let d = (dx * dx + dy * dy).sqrt(); + let a = dy.atan2(dx); + if (5.5..7.0).contains(&d) || (d < 6.0 && (a * 4.0 / std::f32::consts::PI).fract().abs() < 0.12) { + Some(c(110, 76, 44)) + } else if y >= 13 && (x == 7 || x == 8 || (4..=11).contains(&x) && y == 15) { + Some(c(70, 48, 30)) + } else { + None + } + }); + // tool icons cv.cell(I_PICK, |x, y| { let handle = (x + y == 16 || x + y == 17) && x >= 3; @@ -423,6 +463,39 @@ fn gen_atlas() -> Image { None } }); + cv.cell(I_ARROW, |x, y| { + // a small wooden tower with an arrow + if (4..=11).contains(&x) && y >= 7 { + Some(if (x + y) % 4 == 0 { c(90, 60, 34) } else { c(130, 88, 50) }) + } else if y == 6 && x % 2 == 0 && (4..=11).contains(&x) { + Some(c(130, 88, 50)) + } else if y == 3 && (2..=13).contains(&x) || (x == 12 && (y == 2 || y == 4)) { + Some(c(220, 220, 220)) + } else { + None + } + }); + cv.cell(I_CANNON, |x, y| { + let barrel = y >= 4 && y <= 7 && x >= 5 && x <= 14 && (y - 4) as i32 <= 3; + let wheel = (x - 6) * (x - 6) + (y - 10) * (y - 10) <= 9; + if barrel { + Some(c(50, 50, 56)) + } else if wheel { + Some(c(120, 80, 40)) + } else { + None + } + }); + cv.cell(I_MAGIC, |x, y| { + let crystal = (x - 8).abs() + (y - 5).abs() <= 3; + if crystal { + Some(c(190, 110, 255)) + } else if (6..=9).contains(&x) && y >= 9 { + Some(c(110, 110, 124)) + } else { + None + } + }); cv.cell(I_SUMMON, |x, y| { let d = (((x - 8) * (x - 8) + (y - 8) * (y - 8)) as f32).sqrt(); let a = ((y - 8) as f32).atan2((x - 8) as f32); @@ -645,10 +718,11 @@ impl Gfx { Rect::new((i * SPR_CELL + 1) as f32, (1 + if white { SPR_CELL } else { 0 }) as f32, SPR as f32, SPR as f32) } - pub fn tile_tex(kind: Kind, claimed: bool, hero: bool, var: u8) -> u32 { + pub fn tile_tex(kind: Kind, claimed: bool, hero: bool, var: u8, reinforced: bool) -> u32 { let v = var as u32 & 3; match kind { Kind::Rock => T_ROCK + v, + Kind::Earth if reinforced => T_REINF + v, Kind::Earth => T_EARTH + v, Kind::Gold => T_GOLD + v, Kind::Floor => { @@ -668,6 +742,8 @@ impl Gfx { Kind::Core => T_COREFLOOR + v, Kind::Portal => T_PORTALFLOOR + v, Kind::Gate => T_GATEFLOOR + v, + Kind::Torture => T_TORTURE + v, + Kind::Tower => T_CLAIM + v, } } diff --git a/src/map.rs b/src/map.rs index 4ad6520..c7101b6 100644 --- a/src/map.rs +++ b/src/map.rs @@ -46,9 +46,12 @@ pub enum Kind { Core, Portal, Gate, + Torture, + Tower, // floor tile occupied by a defence tower (not walkable) } -const KINDS: [Kind; 12] = [ +// order matters for the packed savegame codec: only append new kinds +const KINDS: [Kind; 14] = [ Kind::Rock, Kind::Earth, Kind::Gold, @@ -61,6 +64,8 @@ const KINDS: [Kind; 12] = [ Kind::Core, Kind::Portal, Kind::Gate, + Kind::Torture, + Kind::Tower, ]; #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -93,11 +98,13 @@ pub struct Tile { pub bed: u32, // entity id + 1 sleeping here #[serde(rename = "v")] pub var: u8, // texture variant + #[serde(rename = "rf", default)] + pub reinforced: bool, // wall reinforced by imps } impl Tile { pub fn new(kind: Kind) -> Tile { - Tile { kind, owner: Owner::None, hp: 0.0, gold: 0, marked: false, reserved: 0, food: 0.0, bed: 0, var: 0 } + Tile { kind, owner: Owner::None, hp: 0.0, gold: 0, marked: false, reserved: 0, food: 0.0, bed: 0, var: 0, reinforced: false } } pub fn solid(&self) -> bool { matches!(self.kind, Kind::Rock | Kind::Earth | Kind::Gold) @@ -108,7 +115,7 @@ 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::Floor | Kind::Treasury | Kind::Lair | Kind::Farm | Kind::Training | Kind::Library | Kind::Portal | Kind::Gate | Kind::Torture ) } } @@ -429,9 +436,10 @@ impl Map { move |i| { let tl = &self.t[i]; match tl.kind { + Kind::Earth if tl.reinforced => Some(70), Kind::Earth => Some(7), Kind::Gold => Some(12), - Kind::Rock | Kind::Core => None, + Kind::Rock | Kind::Core | Kind::Tower => None, _ => Some(1), } } @@ -505,7 +513,7 @@ fn tile_record(t: &Tile) -> [u8; REC] { let mut r = [0u8; REC]; r[0] = KINDS.iter().position(|&k| k == t.kind).unwrap_or(0) as u8; r[1] = OWNERS.iter().position(|&o| o == t.owner).unwrap_or(0) as u8; - r[2] = t.marked as u8; + r[2] = t.marked as u8 | (t.reinforced as u8) << 1; r[3..7].copy_from_slice(&t.hp.to_le_bytes()); r[7..11].copy_from_slice(&t.gold.to_le_bytes()); r[11..15].copy_from_slice(&t.food.to_le_bytes()); @@ -567,7 +575,8 @@ fn tiles_de<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { let t = Tile { kind: *KINDS.get(r[0] as usize).unwrap_or(&Kind::Rock), owner: *OWNERS.get(r[1] as usize).unwrap_or(&Owner::None), - marked: r[2] != 0, + marked: r[2] & 1 != 0, + reinforced: r[2] & 2 != 0, hp: f32::from_le_bytes(f(3)), gold: i32::from_le_bytes(f(7)), food: f32::from_le_bytes(f(11)), diff --git a/src/render3d.rs b/src/render3d.rs index 6b499be..a70797e 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -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 = 80.0; +const ATLAS_H: f32 = 96.0; const DISP_AMP: f32 = 0.11; // wall surface irregularity // ------------------------------------------------------------------ camera @@ -482,7 +482,7 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: for z in cz * CHUNK..((cz + 1) * CHUNK).min(mh() as i32) { for x in cx * CHUNK..((cx + 1) * CHUNK).min(mw() as i32) { let t = map.t[idx(x, z)]; - let tex = Gfx::tile_tex(t.kind, t.owner == Owner::Player, t.owner == Owner::Hero, t.var); + 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 !t.solid() { @@ -694,7 +694,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32, fill_bucket: u8) -> u64 { Kind::Treasury => fill_bucket as u64, _ => 0, }; - (t.kind as u64) | (t.owner as u64) << 4 | (t.marked as u64) << 6 | extra << 8 + (t.kind as u64) | (t.owner as u64) << 5 | (t.marked as u64) << 7 | (t.reinforced as u64) << 8 | extra << 9 } None => 0xff, }; @@ -1268,6 +1268,7 @@ fn draw_overlay(map: &Map, ov: &Overlay, n: usize) { pub fn minimap_color(tile: &Tile) -> [u8; 4] { match tile.kind { Kind::Rock => [40, 40, 48, 255], + Kind::Earth if tile.reinforced => [120, 70, 70, 255], Kind::Earth => [86, 60, 40, 255], Kind::Gold => [220, 180, 50, 255], Kind::Floor => match tile.owner { @@ -1283,5 +1284,7 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] { Kind::Core => [255, 60, 60, 255], Kind::Portal => [190, 90, 240, 255], Kind::Gate => [230, 230, 255, 255], + Kind::Torture => [120, 40, 40, 255], + Kind::Tower => [255, 140, 60, 255], } } diff --git a/src/sfx.rs b/src/sfx.rs index 289c6ee..7895381 100644 --- a/src/sfx.rs +++ b/src/sfx.rs @@ -146,6 +146,15 @@ fn def(s: Sfx, r: f32) -> Vec { Sfx::Defeat => l(&[seg(Square(0.5), 220.0, 110.0, 0.6, 0.22, 2.0)], 38), Sfx::Click => l(&[seg(Square(0.5), 1000.0, 1000.0, 0.02, 0.12, 50.0)], 39), Sfx::Error => l(&[seg(Square(0.5), 110.0, 105.0, 0.16, 0.2, 4.0)], 40), + Sfx::Reinforce => l(&[seg(Tri, 320.0, 200.0, 0.07, 0.5, 20.0), seg(Tri, 260.0, 170.0, 0.09, 0.5, 20.0)], 41), + Sfx::Cannon => mix(vec![l(&[seg(Noise, 1500.0, 150.0, 0.5, 0.6, 6.0)], 42), l(&[seg(Tri, 75.0, 32.0, 0.45, 0.9, 5.0)], 43)]), + Sfx::Magic => mix(vec![l(&[seg(Sine, 880.0, 1760.0, 0.16, 0.35, 8.0)], 44), l(&[vib(seg(Square(0.125), 1320.0, 1320.0, 0.16, 0.08, 10.0), 0.05)], 45)]), + Sfx::Capture => l(&[ + seg(Square(0.25), 1400.0, 1400.0, 0.04, 0.16, 20.0), + seg(Square(0.25), 1100.0, 1100.0, 0.04, 0.16, 20.0), + seg(Square(0.25), 900.0, 900.0, 0.08, 0.16, 15.0), + ], 46), + Sfx::Convert => l(&arp(Saw, &[220.0, 262.0, 330.0, 440.0], 0.1, 0.22), 47), } } diff --git a/src/ui.rs b/src/ui.rs index 4affd5a..ebb40a1 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -617,6 +617,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::Earth if t.reinforced => { + out.push((tr("tile.reinforced"), FRAME_HI)); + out.push((tr("tile.reinforced_info"), DIM)); + } Kind::Earth => out.push((tr("tile.earth"), FRAME_HI)), Kind::Gold => { out.push((tr("tile.gold"), GOLD));