diff --git a/lang/de.lang b/lang/de.lang index dba3099..9f6bc17 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 Werkzeug +help.k_tools = 1-9,0,X,V,K,J,B,C,T,U 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. @@ -383,3 +383,21 @@ help.t_long = Lange drücken: Infos zur Stelle; Hand: Einheiten dort absetzen help.t_double = Doppelt tippen mit der Hand: Rückhand für eigene, Faust für Helden auf eigenem Gebiet help.t_tools = Werkzeugleiste: Kategorien öffnen eine Reihe Werkzeuge; die Menütaste öffnet das Pausemenü help.start_touch = Tippen zum Starten + +# rivers and bridges +tool.bridge.label = Brücke +tool.bridge.name = Brücke bauen +tool.bridge.desc = Baut eine Brücke über Wasser oder Lava. Ziehe von deinem Gebiet aus über den Fluss; die Brücke wächst Feld für Feld von deinem Reich aus. +cost.bridge = {0} Gold/Feld über Wasser, {1} über Lava +hint.bridge_where = Brücken müssen an deinem Gebiet beginnen: Ziehe von deinem Reich aus über den Fluss. +hint.bridge_river = Brücken lassen sich nur über Wasser oder Lava bauen. +msg.hero_burned = Ein {0} ist in die Lava gefallen! +{1} Gold. +msg.creature_burned = Dein {0} ist in die Lava gefallen. +tile.water = Unterirdischer Fluss +tile.water_info = Niemand kann hindurchwaten. Baue eine Brücke. +tile.lava = Lava +tile.lava_info = Was hineinfällt, verbrennt. Steinbrücken führen hinüber. +tile.bridge = Holzbrücke +tile.lavabridge = Steinbrücke +tile.shard = Splitter deines Seelenkerns +tile.shard_info = Erobere diese Kammer, um den Splitter zurückzuholen. diff --git a/lang/en.lang b/lang/en.lang index 1bca49a..9321448 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 Select tool +help.k_tools = 1-9,0,X,V,K,J,B,C,T,U 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. @@ -384,3 +384,21 @@ help.t_long = Long press: information about the spot; hand: drop the units there help.t_double = Double tap with the hand: backhand own units, punch heroes on your ground help.t_tools = Toolbar: categories open a row of tools; the menu button opens the pause menu help.start_touch = Tap to start + +# rivers and bridges +tool.bridge.label = Bridge +tool.bridge.name = Build Bridge +tool.bridge.desc = Builds a bridge over water or lava. Drag from your own ground across the river; the bridge grows tile by tile from your territory. +cost.bridge = {0} gold/tile over water, {1} over lava +hint.bridge_where = Bridges must start at your own ground: drag from your territory across the river. +hint.bridge_river = Bridges can only be built over water or lava. +msg.hero_burned = A {0} fell into the lava! +{1} gold. +msg.creature_burned = Your {0} fell into the lava. +tile.water = Underground river +tile.water_info = Nobody can wade through it. Build a bridge. +tile.lava = Lava +tile.lava_info = Whatever falls in burns. Stone bridges cross it. +tile.bridge = Wooden bridge +tile.lavabridge = Stone bridge +tile.shard = Shard of your soul core +tile.shard_info = Claim this chamber to take the shard back. diff --git a/src/game.rs b/src/game.rs index 69d50ae..e312ff7 100644 --- a/src/game.rs +++ b/src/game.rs @@ -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 { 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 { @@ -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 diff --git a/src/gfx.rs b/src/gfx.rs index 3a4427f..8b53785 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 = 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, } } diff --git a/src/main.rs b/src/main.rs index ea430b5..08a3b8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { diff --git a/src/map.rs b/src/map.rs index 197e5ff..fa063dc 100644 --- a/src/map.rs +++ b/src/map.rs @@ -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 { + 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), } } diff --git a/src/render3d.rs b/src/render3d.rs index 16f46bc..1ecfe12 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 = 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 { (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], } } diff --git a/src/settings.rs b/src/settings.rs index 5abab7b..a987d57 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..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 { - let mut v: Vec = (0..17).map(Action::Tool).collect(); + let mut v: Vec = (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, diff --git a/src/ui.rs b/src/ui.rs index 464186d..d671d0e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -155,10 +155,11 @@ pub enum Tool { Heal, Summon, Tower(TowerKind), + Bridge, } // New tools are appended so that existing key bindings ("tool") 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, 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));