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:
1 parent
af6be62805
commit
75dc445d3b
9 files changed
+536
-22
No files matched your search
+218
-8
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user