Drawbridges (down, up, automatic), frost spell and obsidian over cooled lava
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
76c23a06f0
commit
34b4640d85
32 files changed
+869
-31
No files matched your search
+234
-2
@@ -1,6 +1,8 @@
|
||||
// Bridges under strain: wooden bridges catch fire (fire traps, fire breath, fireballs, lightning),
|
||||
// the fire spreads along the planks and the bridge collapses into the river; knights wear bridges
|
||||
// down step by step, pit imps mend them again. Stone bridges over lava never burn.
|
||||
// Drawbridges are raised and lowered by hand or rise by themselves when enemies come near.
|
||||
// Frost (the frost glow trap, the frost spell) cools lava into walkable obsidian for a while.
|
||||
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
@@ -17,17 +19,26 @@ pub const BURN_DPS: f32 = 6.0;
|
||||
pub const TEAR_RATE: f32 = 0.12;
|
||||
// wear an imp mends per second
|
||||
pub const MEND_RATE: f32 = 0.25;
|
||||
// an automatic drawbridge rises when an enemy comes this close (tiles)
|
||||
pub const DRAWBRIDGE_WATCH: f32 = 6.0;
|
||||
// seconds the frost spell keeps lava cooled; the last seconds it steams
|
||||
pub const FREEZE_TIME: f32 = 40.0;
|
||||
pub const MELT_WARN: f32 = 5.0;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct BridgeWork {
|
||||
pub burning: Vec<(usize, f32)>, // tile, seconds it burns already
|
||||
pub wear: Vec<(usize, f32)>, // tile, damage 0..1
|
||||
#[serde(default)]
|
||||
pub cooled: Vec<(usize, f32)>, // obsidian tile, seconds until it melts
|
||||
#[serde(default)]
|
||||
pub watch_t: f32, // automatic drawbridges look around twice a second
|
||||
}
|
||||
|
||||
impl Game {
|
||||
// a wooden bridge (or wooden drawbridge) that can burn
|
||||
pub fn wooden_bridge(&self, i: usize) -> bool {
|
||||
self.map.t.get(i).map_or(false, |t| t.kind == Kind::Bridge)
|
||||
self.map.t.get(i).map_or(false, |t| t.kind == Kind::Bridge || (t.kind == Kind::Drawbridge && t.res & DB_LAVA == 0))
|
||||
}
|
||||
|
||||
pub fn burning(&self, i: usize) -> bool {
|
||||
@@ -65,8 +76,10 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
// the shared part of every step: fire, collapse
|
||||
// the shared part of every step: fire, collapse, drawbridges, melting obsidian
|
||||
pub(crate) fn update_bridges(&mut self, dt: f32) {
|
||||
self.update_drawbridges(dt);
|
||||
self.update_obsidian(dt);
|
||||
if self.bridgework.burning.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -135,6 +148,7 @@ impl Game {
|
||||
tl.reserved = 0;
|
||||
tl.res = 0;
|
||||
self.bridgework.wear.retain(|w| w.0 != i);
|
||||
self.bridgework.burning.retain(|w| w.0 != i);
|
||||
self.ground_lost(i);
|
||||
let (x, y) = xy(i);
|
||||
self.fx(6, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.6, 0xff3a5a7a);
|
||||
@@ -187,6 +201,181 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ drawbridges and frost
|
||||
impl Game {
|
||||
// the drawbridge tiles connected to tile i (same owner)
|
||||
pub fn drawbridge_group(&self, i: usize) -> Vec<usize> {
|
||||
let owner = self.map.t[i].owner;
|
||||
let mut group = vec![i];
|
||||
let mut k = 0;
|
||||
while k < group.len() {
|
||||
let (x, y) = xy(group[k]);
|
||||
for &(dx, dy) in DIR4.iter() {
|
||||
if !inb(x + dx, y + dy) {
|
||||
continue;
|
||||
}
|
||||
let n = idx(x + dx, y + dy);
|
||||
let t = &self.map.t[n];
|
||||
if t.kind == Kind::Drawbridge && t.owner == owner && !group.contains(&n) {
|
||||
group.push(n);
|
||||
}
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
group
|
||||
}
|
||||
|
||||
// the drawbridge tool: a click on an own drawbridge switches it (down -> up -> automatic -> down),
|
||||
// anything else builds drawbridges like bridges
|
||||
pub fn drawbridge_order(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
|
||||
if x0 == x1 && y0 == y1 && inb(x0, y0) {
|
||||
let i = idx(x0, y0);
|
||||
let t = self.map.t[i];
|
||||
if t.kind == Kind::Drawbridge && t.owner == self.me {
|
||||
let state = t.res & (DB_RAISED | DB_AUTO);
|
||||
let next = match state {
|
||||
0 => DB_RAISED,
|
||||
s if s & DB_AUTO != 0 => 0,
|
||||
_ => DB_AUTO,
|
||||
};
|
||||
self.set_drawbridge(i, next);
|
||||
let key = match next {
|
||||
DB_RAISED => "msg.drawbridge_up",
|
||||
DB_AUTO => "msg.drawbridge_auto",
|
||||
_ => "msg.drawbridge_down",
|
||||
};
|
||||
self.msg(key, vec![]);
|
||||
self.sfx_ui(Sfx::Click);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.build_bridges_of(x0, y0, x1, y1, true);
|
||||
}
|
||||
|
||||
// sets the state bits (DB_RAISED, DB_AUTO) of the whole drawbridge at tile i
|
||||
pub fn set_drawbridge(&mut self, i: usize, state: u8) {
|
||||
let group = self.drawbridge_group(i);
|
||||
let raise = state & DB_RAISED != 0 && !self.map.t[i].raised();
|
||||
let lower = state & DB_RAISED == 0 && self.map.t[i].raised();
|
||||
for &g in &group {
|
||||
let t = &mut self.map.t[g];
|
||||
t.res = (t.res & DB_LAVA) | state;
|
||||
}
|
||||
if raise {
|
||||
// whoever stands on it jumps to the bank (or falls)
|
||||
for &g in &group {
|
||||
self.ground_lost(g);
|
||||
}
|
||||
}
|
||||
if raise || lower {
|
||||
let (x, y) = xy(i);
|
||||
self.sfx(Sfx::Build, x as f32 + 0.5, y as f32 + 0.5);
|
||||
for &g in &group {
|
||||
let (gx, gy) = xy(g);
|
||||
self.fx(6, gx as f32 + 0.5, gy as f32 + 0.5, 0.0, 0.0, 0.3, 0xff506070);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// automatic drawbridges rise while an enemy of their master is near, and come down again
|
||||
fn update_drawbridges(&mut self, dt: f32) {
|
||||
self.bridgework.watch_t -= dt;
|
||||
if self.bridgework.watch_t > 0.0 {
|
||||
return;
|
||||
}
|
||||
self.bridgework.watch_t = 0.5;
|
||||
let mut seen: Vec<usize> = Vec::new();
|
||||
for i in 0..self.map.t.len() {
|
||||
let t = self.map.t[i];
|
||||
if t.kind != Kind::Drawbridge || t.res & DB_AUTO == 0 || seen.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
let group = self.drawbridge_group(i);
|
||||
seen.extend(group.iter().copied());
|
||||
let side = t.owner.side();
|
||||
let r2 = DRAWBRIDGE_WATCH * DRAWBRIDGE_WATCH;
|
||||
let enemy = self.ents.iter().any(|e| {
|
||||
e.alive
|
||||
&& !e.held
|
||||
&& e.captive == 0
|
||||
&& !e.chained
|
||||
&& e.side() != side
|
||||
&& !self.allied(side, e.side())
|
||||
&& group.iter().any(|&g| {
|
||||
let (gx, gy) = xy(g);
|
||||
(e.x - gx as f32 - 0.5).powi(2) + (e.y - gy as f32 - 0.5).powi(2) < r2
|
||||
})
|
||||
});
|
||||
if enemy != t.raised() {
|
||||
self.set_drawbridge(i, DB_AUTO | if enemy { DB_RAISED } else { 0 });
|
||||
if enemy && t.owner == self.me {
|
||||
self.msg("msg.drawbridge_rises", vec![]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// frost on tile i: lava turns to obsidian (or stays it) for at least `secs`
|
||||
pub fn cool_lava(&mut self, i: usize, secs: f32) {
|
||||
let t = &mut self.map.t[i];
|
||||
match t.kind {
|
||||
Kind::Lava => {
|
||||
t.kind = Kind::Obsidian;
|
||||
t.owner = Owner::None;
|
||||
self.bridgework.cooled.push((i, secs));
|
||||
let (x, y) = xy(i);
|
||||
for _ in 0..3 {
|
||||
self.parts.push(Part { kind: SMOKE, x: x as f32 + self.vrng.f(), y: y as f32 + self.vrng.f(), h: 0.1, vx: 0.0, vy: 0.0, vh: 0.5, t: 0.0, max: 1.6, col: abgr(200, 210, 220), size: 0.16 });
|
||||
}
|
||||
}
|
||||
Kind::Obsidian => {
|
||||
if let Some(c) = self.bridgework.cooled.iter_mut().find(|c| c.0 == i) {
|
||||
c.1 = c.1.max(secs);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// frost in a circle around (x, y)
|
||||
pub fn cool_lava_around(&mut self, x: f32, y: f32, r: f32, secs: f32) {
|
||||
let ri = r.ceil() as i32;
|
||||
let (cx, cy) = (x.floor() as i32, y.floor() as i32);
|
||||
for ty in cy - ri..=cy + ri {
|
||||
for tx in cx - ri..=cx + ri {
|
||||
if inb(tx, ty) && (tx as f32 + 0.5 - x).powi(2) + (ty as f32 + 0.5 - y).powi(2) <= r * r {
|
||||
self.cool_lava(idx(tx, ty), secs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// obsidian melts again: it steams before, and whoever still stands there is burnt
|
||||
fn update_obsidian(&mut self, dt: f32) {
|
||||
let mut k = 0;
|
||||
while k < self.bridgework.cooled.len() {
|
||||
let (i, left) = self.bridgework.cooled[k];
|
||||
if self.map.t[i].kind != Kind::Obsidian {
|
||||
self.bridgework.cooled.swap_remove(k);
|
||||
continue;
|
||||
}
|
||||
let left = left - dt;
|
||||
self.bridgework.cooled[k].1 = left;
|
||||
let (x, y) = xy(i);
|
||||
if left < MELT_WARN && self.vrng.f() < dt * 4.0 {
|
||||
self.parts.push(Part { kind: SMOKE, x: x as f32 + self.vrng.f(), y: y as f32 + self.vrng.f(), h: 0.05, vx: 0.0, vy: 0.0, vh: 0.4, t: 0.0, max: 1.4, col: abgr(150, 150, 160), size: 0.14 });
|
||||
}
|
||||
if left <= 0.0 {
|
||||
self.bridgework.cooled.swap_remove(k);
|
||||
self.map.t[i].kind = Kind::Lava;
|
||||
self.ground_lost(i);
|
||||
continue;
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -245,4 +434,47 @@ mod tests {
|
||||
while g.wear_bridge(tiles[1], 1.0) {}
|
||||
assert_eq!(g.map.t[tiles[1]].kind, Kind::Water);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawbridges_switch_and_rise_by_themselves() {
|
||||
let (mut g, tiles) = bridge_game();
|
||||
for &i in &tiles {
|
||||
g.map.t[i].kind = Kind::Drawbridge;
|
||||
g.map.t[i].res = 0;
|
||||
}
|
||||
let (x, y) = xy(tiles[2]);
|
||||
g.drawbridge_order(x, y, x, y);
|
||||
assert!(tiles.iter().all(|&i| g.map.t[i].raised()), "the whole bridge goes up");
|
||||
assert!(!g.map.walk(x, y));
|
||||
g.drawbridge_order(x, y, x, y);
|
||||
assert!(tiles.iter().all(|&i| g.map.t[i].res & DB_AUTO != 0 && !g.map.t[i].raised()), "automatic");
|
||||
// a knight comes near: it rises
|
||||
let id = g.spawn(EK::Knight, true, x as f32 + 0.5, y as f32 + 3.5, 1);
|
||||
for _ in 0..20 {
|
||||
g.update(0.05);
|
||||
}
|
||||
assert!(g.map.t[tiles[2]].raised(), "risen for the knight");
|
||||
g.ents.retain(|e| e.id != id);
|
||||
for _ in 0..20 {
|
||||
g.update(0.05);
|
||||
}
|
||||
assert!(!g.map.t[tiles[2]].raised(), "down again");
|
||||
g.drawbridge_order(x, y, x, y);
|
||||
assert_eq!(g.map.t[tiles[2]].res & (DB_RAISED | DB_AUTO), 0, "back to manual, down");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frost_cools_lava_for_a_while() {
|
||||
let mut g = Game::new(5, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
let i = idx(cx + 6, cy + 6);
|
||||
g.map.t[i].kind = Kind::Lava;
|
||||
g.cool_lava(i, 2.0);
|
||||
assert_eq!(g.map.t[i].kind, Kind::Obsidian);
|
||||
assert!(g.map.t[i].walkable());
|
||||
for _ in 0..(3 * 20) {
|
||||
g.update(0.05);
|
||||
}
|
||||
assert_eq!(g.map.t[i].kind, Kind::Lava, "melted again");
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -34,6 +34,7 @@ pub enum Act {
|
||||
Sell(Rect),
|
||||
Bridge(Rect),
|
||||
Steel(Rect),
|
||||
Drawbridge(Rect), // build drawbridges, or (one tile on an own drawbridge) switch it: down, up, automatic
|
||||
Tower {
|
||||
kind: TowerKind,
|
||||
x: i32,
|
||||
@@ -141,7 +142,7 @@ impl Game {
|
||||
Act::Dig(r) => (rect_center(r), MARK_DIG, 4.0),
|
||||
Act::Room { r, .. } => (rect_center(r), MARK_ROOM, 5.0),
|
||||
Act::Sell(r) => (rect_center(r), MARK_SELL, 4.0),
|
||||
Act::Bridge(r) => (rect_center(r), MARK_BRIDGE, 4.0),
|
||||
Act::Bridge(r) | Act::Drawbridge(r) => (rect_center(r), MARK_BRIDGE, 4.0),
|
||||
Act::Steel(r) => (rect_center(r), MARK_STEEL, 4.0),
|
||||
Act::Tower { x, y, .. } => (point(x as f32, y as f32), MARK_TOWER, 6.0),
|
||||
Act::Trap { x, y, .. } => (point(x as f32, y as f32), MARK_TRAP, 6.0),
|
||||
@@ -185,6 +186,7 @@ impl Game {
|
||||
Act::Room { room, r } => self.build_rect(room as usize, r.x0, r.y0, r.x1, r.y1),
|
||||
Act::Sell(r) => self.sell_rect(r.x0, r.y0, r.x1, r.y1),
|
||||
Act::Bridge(r) => self.build_bridges(r.x0, r.y0, r.x1, r.y1),
|
||||
Act::Drawbridge(r) => self.drawbridge_order(r.x0, r.y0, r.x1, r.y1),
|
||||
Act::Steel(r) => self.build_steel(r.x0, r.y0, r.x1, r.y1),
|
||||
Act::Tower { kind, x, y } => self.build_tower(kind, x, y),
|
||||
Act::Trap { kind, x, y } => self.place_trap(kind, x, y),
|
||||
|
||||
+22
-7
@@ -750,6 +750,8 @@ pub const ROOM_NAMES: [&str; N_ROOMS] = [
|
||||
|
||||
// bridge cost per tile: over water, over lava
|
||||
pub const BRIDGE_COST: [i32; 2] = [30, 75];
|
||||
// a drawbridge tile (over water or lava)
|
||||
pub const DRAWBRIDGE_COST: i32 = 150;
|
||||
// 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;
|
||||
@@ -2820,7 +2822,7 @@ impl Game {
|
||||
|
||||
// the ground of tile i turned into river (bridge removed): units on it fall in
|
||||
pub(crate) fn ground_lost(&mut self, i: usize) {
|
||||
let lava = self.map.t[i].kind == Kind::Lava;
|
||||
let lava = self.map.t[i].river() == Some(Kind::Lava);
|
||||
let (x, y) = xy(i);
|
||||
for j in 0..self.ents.len() {
|
||||
let e = &self.ents[j];
|
||||
@@ -2890,6 +2892,10 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn build_bridges(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
|
||||
self.build_bridges_of(x0, y0, x1, y1, false);
|
||||
}
|
||||
|
||||
pub fn build_bridges_of(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, draw: bool) {
|
||||
let (mut built, mut broke, mut any_river) = (0, false, false);
|
||||
loop {
|
||||
let mut add = Vec::new();
|
||||
@@ -2911,14 +2917,21 @@ impl Game {
|
||||
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;
|
||||
let cost = if draw { DRAWBRIDGE_COST } else { 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.kind = if draw {
|
||||
Kind::Drawbridge
|
||||
} else if lava {
|
||||
Kind::LavaBridge
|
||||
} else {
|
||||
Kind::Bridge
|
||||
};
|
||||
t.res = if draw && lava { DB_LAVA } else { 0 };
|
||||
t.owner = self.me;
|
||||
built += 1;
|
||||
progress = true;
|
||||
@@ -3060,7 +3073,7 @@ impl Game {
|
||||
continue;
|
||||
}
|
||||
if let Some(t) = self.map.get(cx + dx, cy + dy) {
|
||||
if t.bridge() && t.owner == self.me {
|
||||
if t.bridge() && !t.raised() && t.owner == self.me {
|
||||
return Some(idx(cx + dx, cy + dy));
|
||||
}
|
||||
}
|
||||
@@ -3073,7 +3086,7 @@ impl Game {
|
||||
// a knight walks next to the bridge tile and tears it down
|
||||
fn tear_bridge(&mut self, e: &mut Ent, i: usize, dt: f32) -> bool {
|
||||
let t = self.map.t[i];
|
||||
if !t.bridge() || t.owner != self.me {
|
||||
if !t.bridge() || t.raised() || t.owner != self.me {
|
||||
return false;
|
||||
}
|
||||
let (bx, by) = xy(i);
|
||||
@@ -4714,11 +4727,13 @@ impl Game {
|
||||
continue;
|
||||
}
|
||||
if t.owner == self.me && t.bridge() {
|
||||
let lava = t.kind == Kind::LavaBridge;
|
||||
let lava = t.river() == Some(Kind::Lava);
|
||||
let cost = if t.kind == Kind::Drawbridge { DRAWBRIDGE_COST } else { BRIDGE_COST[lava as usize] };
|
||||
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;
|
||||
t.res = 0;
|
||||
refund += cost as f32 * 0.5;
|
||||
self.ground_lost(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
+51
-3
@@ -80,6 +80,9 @@ pub const T_ARENA: u32 = 178;
|
||||
pub const T_ALTAR: u32 = 182;
|
||||
pub const I_ORDERS: u32 = 186; // attack, guard
|
||||
pub const T_RIVAL3: u32 = 189; // 4 variants: floor of the yellow master
|
||||
pub const I_DRAWBRIDGE: u32 = 193; // drawbridge tool: a raised flap on chains
|
||||
pub const I_FREEZE: u32 = 194; // frost spell: a snowflake
|
||||
pub const T_OBSIDIAN: u32 = 195; // 4 variants: cooled lava, dark glass with glowing seams
|
||||
|
||||
pub struct Gfx {
|
||||
pub atlas: Texture2D,
|
||||
@@ -803,6 +806,50 @@ fn gen_atlas() -> Image {
|
||||
None
|
||||
}
|
||||
});
|
||||
// drawbridge tool icon: a raised flap with its chains above a river
|
||||
cv.cell(I_DRAWBRIDGE, |x, y| {
|
||||
if (3..=5).contains(&x) && (2..=12).contains(&y) {
|
||||
Some(if y % 3 == 0 { c(70, 46, 26) } else { c(150, 106, 60) })
|
||||
} else if y == 12 && (5..=13).contains(&x) {
|
||||
Some(c(110, 76, 44))
|
||||
} else if (x - 5) * 2 == (y - 2) * 1 + 2 && (6..=12).contains(&x) {
|
||||
Some(c(170, 170, 180)) // chain
|
||||
} else if x == 13 && (3..=12).contains(&y) {
|
||||
Some(c(90, 90, 100)) // post
|
||||
} else if y >= 13 && (1..=14).contains(&x) {
|
||||
Some(c(60, 110, 170))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
// frost spell icon: a snowflake
|
||||
cv.cell(I_FREEZE, |x, y| {
|
||||
let (dx, dy) = (x - 8, y - 8);
|
||||
let arm = dx == 0 || dy == 0 || dx == dy || dx == -dy;
|
||||
let r = dx.abs().max(dy.abs());
|
||||
if arm && r <= 6 {
|
||||
Some(if r <= 1 { c(240, 250, 255) } else { c(150, 210, 255) })
|
||||
} else if r == 4 && (dx.abs() == 1 || dy.abs() == 1) && (dx == 0 || dy == 0 || dx.abs() == dy.abs() + 1 || dy.abs() == dx.abs() + 1) {
|
||||
Some(c(200, 235, 255))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
// obsidian: cooled lava, dark glass with a few glowing seams
|
||||
for v in 0..4u32 {
|
||||
cv.cell(T_OBSIDIAN + v, |x, y| {
|
||||
let r = hash(x, y, v + 400);
|
||||
if (x * 5 + y * 3 + v as i32 * 7) % 29 == 0 || ((x + 2 * y + v as i32) % 23 == 0 && r % 2 == 0) {
|
||||
Some(c(230, 90, 30))
|
||||
} else if r % 11 == 0 {
|
||||
Some(c(70, 60, 90))
|
||||
} else if (x + y) % 7 == 0 {
|
||||
Some(c(34, 26, 40))
|
||||
} else {
|
||||
Some(c(22, 16, 26))
|
||||
}
|
||||
});
|
||||
}
|
||||
// ---- more treasures: veins (earth with embedded pieces) and treasury piles
|
||||
let vein = |x: i32, y: i32, v: u32, seed: u32, hi: Color, mid: Color, lo: Color, size: f32| -> Color {
|
||||
let mut best = 99.0f32;
|
||||
@@ -1927,7 +1974,8 @@ impl Gfx {
|
||||
Kind::Tower => T_CLAIM + v,
|
||||
Kind::Water => T_WATER + v,
|
||||
Kind::Lava => T_LAVA + v,
|
||||
Kind::Bridge => T_BRIDGE + (v & 1),
|
||||
Kind::Bridge | Kind::Drawbridge => T_BRIDGE + (v & 1),
|
||||
Kind::Obsidian => T_OBSIDIAN + v,
|
||||
Kind::LavaBridge => T_STONEBRIDGE + (v & 1),
|
||||
Kind::Shard => T_HEROFLOOR + v,
|
||||
Kind::Aerie => T_AERIE + v,
|
||||
@@ -2137,8 +2185,8 @@ mod doc_images {
|
||||
for (k, t) in crate::traps::TRAP_KINDS.iter().enumerate() {
|
||||
save(&atlas, Gfx::cell(I_TRAPS + k as u32), 4, &format!("docs/img/icons/{}.png", t.id()));
|
||||
}
|
||||
for (k, s) in crate::spells::SPELLS.iter().enumerate() {
|
||||
save(&atlas, Gfx::cell(I_SPELLS + k as u32), 4, &format!("docs/img/icons/{}.png", s.id()));
|
||||
for s in crate::spells::SPELLS.iter() {
|
||||
save(&atlas, Gfx::cell(crate::spells::spell_icon(*s)), 4, &format!("docs/img/icons/{}.png", s.id()));
|
||||
}
|
||||
for (id, cell) in [("lightning", I_BOLT), ("heal", I_HEAL), ("summon", I_SUMMON), ("arrow", I_ARROW), ("cannon", I_CANNON), ("magic", I_MAGIC), ("rune", I_RUNE)] {
|
||||
save(&atlas, Gfx::cell(cell), 4, &format!("docs/img/icons/{}.png", id));
|
||||
|
||||
+4
-3
@@ -107,7 +107,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 | Tool::Steel)
|
||||
matches!(t, Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge | Tool::Steel | Tool::Drawbridge)
|
||||
}
|
||||
|
||||
// Use the active tool on the world at (mx, my) (CSS pixels): a click, or a tap on touch screens
|
||||
@@ -120,7 +120,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 | Tool::Steel, Some(t)) => inp.ldown = Some(t),
|
||||
(Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge | Tool::Steel | Tool::Drawbridge, Some(t)) => inp.ldown = Some(t),
|
||||
(Tool::Hand, _) => {
|
||||
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
|
||||
let Some(j) = j else { return };
|
||||
@@ -163,6 +163,7 @@ fn finish_rect(app: &mut App, tool: Tool, start: (i32, i32), mx: f32, my: f32) {
|
||||
Tool::Room(room) => app.game.queue(Act::Room { room: room as u8, r }),
|
||||
Tool::Sell => app.game.queue(Act::Sell(r)),
|
||||
Tool::Bridge => app.game.queue(Act::Bridge(r)),
|
||||
Tool::Drawbridge => app.game.queue(Act::Drawbridge(r)),
|
||||
Tool::Steel => app.game.queue(Act::Steel(r)),
|
||||
_ => {}
|
||||
}
|
||||
@@ -1946,7 +1947,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::Bridge | Tool::Drawbridge => (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),
|
||||
Tool::Trap(_) => (Color::new(1.0, 0.4, 0.3, 1.0), None),
|
||||
|
||||
+32
-5
@@ -68,8 +68,15 @@ pub enum Kind {
|
||||
GuardPost, // room: creatures hold the position
|
||||
Arena, // room: sparring
|
||||
Altar, // room: sacrifices
|
||||
Drawbridge, // bridge that can be raised (res: see DB_RAISED, DB_AUTO, DB_LAVA)
|
||||
Obsidian, // lava cooled by frost: walkable until it melts again
|
||||
}
|
||||
|
||||
// drawbridge state in Tile::res
|
||||
pub const DB_RAISED: u8 = 1; // raised: nobody can cross
|
||||
pub const DB_AUTO: u8 = 2; // rises by itself when enemies come near
|
||||
pub const DB_LAVA: u8 = 4; // spans lava (stone), else water (wood)
|
||||
|
||||
// options for the map generator (campaign missions)
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenOpts {
|
||||
@@ -96,7 +103,7 @@ pub struct River {
|
||||
}
|
||||
|
||||
// order matters for the packed savegame codec: only append new kinds
|
||||
const KINDS: [Kind; 34] = [
|
||||
const KINDS: [Kind; 36] = [
|
||||
Kind::Rock,
|
||||
Kind::Earth,
|
||||
Kind::Gold,
|
||||
@@ -131,6 +138,8 @@ const KINDS: [Kind; 34] = [
|
||||
Kind::GuardPost,
|
||||
Kind::Arena,
|
||||
Kind::Altar,
|
||||
Kind::Drawbridge,
|
||||
Kind::Obsidian,
|
||||
];
|
||||
|
||||
// treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond
|
||||
@@ -151,6 +160,18 @@ pub enum Owner {
|
||||
const OWNERS: [Owner; 6] = [Owner::None, Owner::Player, Owner::Hero, Owner::Rival1, Owner::Rival2, Owner::Rival3];
|
||||
|
||||
impl Owner {
|
||||
// the side of a ground owner (255: nobody)
|
||||
pub fn side(self) -> u8 {
|
||||
match self {
|
||||
Owner::Player => 0,
|
||||
Owner::Hero => 1,
|
||||
Owner::Rival1 => 2,
|
||||
Owner::Rival2 => 3,
|
||||
Owner::Rival3 => 4,
|
||||
Owner::None => 255,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn of_side(side: u8) -> Owner {
|
||||
match side {
|
||||
0 => Owner::Player,
|
||||
@@ -240,20 +261,26 @@ impl Tile {
|
||||
| Kind::GuardPost
|
||||
| Kind::Arena
|
||||
| Kind::Altar
|
||||
)
|
||||
| Kind::Obsidian
|
||||
) || (self.kind == Kind::Drawbridge && self.res & DB_RAISED == 0)
|
||||
}
|
||||
// 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)
|
||||
matches!(self.kind, Kind::Bridge | Kind::LavaBridge | Kind::Drawbridge)
|
||||
}
|
||||
// the river below a bridge or river tile
|
||||
pub fn raised(&self) -> bool {
|
||||
self.kind == Kind::Drawbridge && self.res & DB_RAISED != 0
|
||||
}
|
||||
// the river below a bridge or river tile (obsidian: the lava under the crust)
|
||||
pub fn river(&self) -> Option<Kind> {
|
||||
match self.kind {
|
||||
Kind::Water | Kind::Bridge => Some(Kind::Water),
|
||||
Kind::Lava | Kind::LavaBridge => Some(Kind::Lava),
|
||||
Kind::Lava | Kind::LavaBridge | Kind::Obsidian => Some(Kind::Lava),
|
||||
Kind::Drawbridge if self.res & DB_LAVA != 0 => Some(Kind::Lava),
|
||||
Kind::Drawbridge => Some(Kind::Water),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ pub const TABS: [&str; 4] = ["best.tab.creatures", "best.tab.humans", "best.tab.
|
||||
const CREATURES: [EK; 14] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Cogling, EK::Revenant, EK::Bat, EK::Gargoyle, EK::Owl, EK::Wyrm];
|
||||
const HUMANS: [EK; 8] = [EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Fairy, EK::BattleMage, EK::Giant, EK::Paladin];
|
||||
// buildings: indices into ui::TOOLS
|
||||
const BUILDINGS: [usize; 30] = [2, 3, 4, 5, 6, 12, 40, 13, 18, 21, 41, 42, 43, 44, 17, 19, 14, 15, 16, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31];
|
||||
const BUILDINGS: [usize; 31] = [2, 3, 4, 5, 6, 12, 40, 13, 18, 21, 41, 42, 43, 44, 17, 47, 19, 14, 15, 16, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Entry {
|
||||
@@ -266,6 +266,7 @@ impl Menu {
|
||||
crate::ui::Tool::Room(r) => crate::game::ROOM_NAMES[r].trim_start_matches("room.").to_string(),
|
||||
crate::ui::Tool::Tower(k) => crate::game::tower_name(k).trim_start_matches("tower.").to_string(),
|
||||
crate::ui::Tool::Bridge => "bridge".into(),
|
||||
crate::ui::Tool::Drawbridge => "drawbridge".into(),
|
||||
crate::ui::Tool::Trap(k) => k.id().into(),
|
||||
_ => "steel".into(),
|
||||
};
|
||||
|
||||
+54
-1
@@ -722,12 +722,54 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T
|
||||
[inward; 4],
|
||||
);
|
||||
}
|
||||
if t.kind == Kind::Obsidian {
|
||||
// a crust of cooled lava just above the glowing river
|
||||
let (oa, ob) = cell_uv(T_OBSIDIAN + (t.var as u32 & 3));
|
||||
let oc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5).max(0.45)), lg), 1.0);
|
||||
let y = -0.02;
|
||||
mb.quad([vec3(xf, y, zf), vec3(xf + 1.0, y, zf), vec3(xf + 1.0, y, zf + 1.0), vec3(xf, y, zf + 1.0)], [vec2(oa.x, oa.y), vec2(ob.x, oa.y), vec2(ob.x, ob.y), vec2(oa.x, ob.y)], [oc; 4], [Vec3::Y; 4]);
|
||||
continue;
|
||||
}
|
||||
if t.raised() {
|
||||
// a raised drawbridge: the flap stands upright at the bank it is hinged on
|
||||
let tex = if lava { T_STONEBRIDGE } else { T_BRIDGE };
|
||||
let (ba, bb) = cell_uv(tex);
|
||||
let fc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5).max(if lava { 0.6 } else { 0.0 }) * 0.85), lg), 1.0);
|
||||
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
|
||||
if !map.get(x + dx, z + dz).map_or(false, |o| o.walkable() && !o.bridge()) {
|
||||
continue;
|
||||
}
|
||||
// the hinge edge, from p to q
|
||||
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 off = inward * 0.12;
|
||||
let up = Vec3::Y * 1.0;
|
||||
// both faces of the flap, a little in front of the bank
|
||||
mb.quad([p + off, q + off, q + off + up, p + off + up], [vec2(ba.x, bb.y), vec2(bb.x, bb.y), vec2(bb.x, ba.y), vec2(ba.x, ba.y)], [fc; 4], [inward; 4]);
|
||||
mb.quad([q + off, p + off, p + off + up, q + off + up], [vec2(ba.x, bb.y), vec2(bb.x, bb.y), vec2(bb.x, ba.y), vec2(ba.x, ba.y)], [fc; 4], [-inward; 4]);
|
||||
ln.strip(p + off + up, q + off + up, inward, OUTLINE_W, OUTLINE_C);
|
||||
// the chains from the top of the flap down to the bank
|
||||
let chain = col(Vec3::splat(0.55), 1.0);
|
||||
for e in [p, q] {
|
||||
let a = e + off + up + (if e == p { (q - p) * 0.1 } else { (p - q) * 0.1 });
|
||||
let b = e - inward * 0.6 + Vec3::Y * 1.3;
|
||||
ln.strip(a, b, inward, OUTLINE_W * 0.7, chain);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
// a drawbridge over lava is made of stone
|
||||
let (ba, bb) = cell_uv(if t.kind == Kind::Drawbridge && lava { T_STONEBRIDGE + (t.var as u32 & 1) } else { tex });
|
||||
let dc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5).max(if lava { 0.6 } else { 0.0 })), lg), 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)]
|
||||
@@ -735,6 +777,14 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T
|
||||
[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]);
|
||||
if t.kind == Kind::Drawbridge {
|
||||
// iron bands across the planks mark a drawbridge
|
||||
let iron = col(Vec3::splat(0.32), 1.0);
|
||||
for f in [0.18, 0.82] {
|
||||
let (a, b) = if along_x { (vec3(xf + f, DECK_Y + 0.005, zf), vec3(xf + f, DECK_Y + 0.005, zf + 1.0)) } else { (vec3(xf, DECK_Y + 0.005, zf + f), vec3(xf + 1.0, DECK_Y + 0.005, zf + f)) };
|
||||
ln.strip(a, b, Vec3::Y, OUTLINE_W * 1.6, iron);
|
||||
}
|
||||
}
|
||||
// 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()) {
|
||||
@@ -994,6 +1044,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32) -> u64 {
|
||||
let extra = match t.kind {
|
||||
Kind::Farm => t.food as u64,
|
||||
Kind::Treasury => (t.gold / 50) as u64 | (t.res as u64) << 5,
|
||||
Kind::Drawbridge => t.res as u64,
|
||||
_ => 0,
|
||||
};
|
||||
let shown = map.shown(idx(x, z)) as u64;
|
||||
@@ -3030,6 +3081,8 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
|
||||
Kind::Lava => [250, 110, 30, 255],
|
||||
Kind::Bridge => [150, 110, 60, 255],
|
||||
Kind::LavaBridge => [130, 120, 115, 255],
|
||||
Kind::Drawbridge => [120, 80, 50, 255],
|
||||
Kind::Obsidian => [60, 40, 70, 255],
|
||||
Kind::Shard => [255, 60, 120, 255],
|
||||
Kind::Aerie => [170, 140, 90, 255],
|
||||
Kind::Steel => [150, 160, 180, 255],
|
||||
|
||||
@@ -34,7 +34,9 @@ pub const RESEARCH: &[(usize, f32)] = &[
|
||||
(25, 650.0), // stone roller
|
||||
(37, 700.0), // invisibility
|
||||
(19, 700.0), // steel walls
|
||||
(47, 700.0), // drawbridge
|
||||
(28, 800.0), // frost glow
|
||||
(48, 800.0), // frost spell
|
||||
(38, 850.0), // plague
|
||||
(27, 900.0), // rune mine
|
||||
(39, 950.0), // quake
|
||||
|
||||
@@ -162,4 +162,31 @@ fn export_scenes() {
|
||||
g.place_all_torches();
|
||||
run(&mut g, 4.0);
|
||||
save("lava-light", &mut g, cx as f32 + 5.0, cy as f32, 22.0, 0.0, false);
|
||||
|
||||
// bridges: a raised and a lowered drawbridge over water, a burning bridge, obsidian on lava
|
||||
let mut g = Game::new(17, 80, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
g.gold = 50000.0;
|
||||
open(&mut g, cx - 6, cy - 9, cx + 18, cy + 9);
|
||||
for y in cy - 9..=cy + 9 {
|
||||
for x in cx + 6..cx + 9 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = Kind::Water;
|
||||
t.owner = Owner::None;
|
||||
}
|
||||
for x in cx + 12..cx + 15 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = Kind::Lava;
|
||||
t.owner = Owner::None;
|
||||
}
|
||||
}
|
||||
g.drawbridge_order(cx + 5, cy - 5, cx + 9, cy - 5);
|
||||
g.drawbridge_order(cx + 5, cy - 1, cx + 9, cy - 1);
|
||||
g.drawbridge_order(cx + 6, cy - 1, cx + 6, cy - 1); // raised
|
||||
g.build_bridges(cx + 5, cy + 4, cx + 9, cy + 4);
|
||||
g.ignite_at(cx as f32 + 7.5, cy as f32 + 4.5);
|
||||
g.cool_lava_around(cx as f32 + 13.5, cy as f32 + 1.5, 2.5, 60.0);
|
||||
g.place_all_torches();
|
||||
run(&mut g, 2.5);
|
||||
save("bridges", &mut g, cx as f32 + 9.0, cy as f32, 18.0, 0.0, false);
|
||||
}
|
||||
+30
-1
@@ -16,9 +16,18 @@ pub enum Spell {
|
||||
Invisible,
|
||||
Plague,
|
||||
Quake,
|
||||
Freeze, // frost: cools lava into obsidian, chills enemies
|
||||
}
|
||||
|
||||
pub const SPELLS: [Spell; 8] = [Spell::Eye, Spell::CaveIn, Spell::Haste, Spell::Stoneskin, Spell::GoldRain, Spell::Invisible, Spell::Plague, Spell::Quake];
|
||||
pub const SPELLS: [Spell; 9] = [Spell::Eye, Spell::CaveIn, Spell::Haste, Spell::Stoneskin, Spell::GoldRain, Spell::Invisible, Spell::Plague, Spell::Quake, Spell::Freeze];
|
||||
|
||||
// the icon of a spell (the first eight share one row of the atlas)
|
||||
pub fn spell_icon(sp: Spell) -> u32 {
|
||||
match SPELLS.iter().position(|&t| t == sp).unwrap_or(0) {
|
||||
k if k < 8 => crate::gfx::I_SPELLS + k as u32,
|
||||
_ => crate::gfx::I_FREEZE,
|
||||
}
|
||||
}
|
||||
|
||||
pub const EYE_TIME: f32 = 20.0;
|
||||
pub const EYE_RADIUS: f32 = 8.0;
|
||||
@@ -34,6 +43,7 @@ impl Spell {
|
||||
Spell::Invisible => 50.0,
|
||||
Spell::Plague => 80.0,
|
||||
Spell::Quake => 90.0,
|
||||
Spell::Freeze => 80.0,
|
||||
}
|
||||
}
|
||||
// radius shown under the pointer
|
||||
@@ -43,6 +53,7 @@ impl Spell {
|
||||
Spell::CaveIn => 1.5,
|
||||
Spell::Plague => 2.0,
|
||||
Spell::Quake => 3.0,
|
||||
Spell::Freeze => 2.5,
|
||||
Spell::GoldRain => 0.0,
|
||||
_ => 1.6,
|
||||
}
|
||||
@@ -57,6 +68,7 @@ impl Spell {
|
||||
Spell::Invisible => "invisible",
|
||||
Spell::Plague => "plague",
|
||||
Spell::Quake => "quake",
|
||||
Spell::Freeze => "freeze",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,6 +244,23 @@ impl Game {
|
||||
}
|
||||
self.sfx(Sfx::Cannon, wx, wy);
|
||||
}
|
||||
Spell::Freeze => {
|
||||
// lava turns to obsidian for a while; enemies are chilled and slowed
|
||||
self.cool_lava_around(wx, wy, 2.5, crate::bridges::FREEZE_TIME);
|
||||
let hit: Vec<usize> = (0..self.ents.len()).filter(|&j| foe(&self.ents[j]) && near(&self.ents[j])).collect();
|
||||
for j in hit {
|
||||
let e = &mut self.ents[j];
|
||||
e.hp -= 20.0;
|
||||
e.slow = e.slow.max(4.0);
|
||||
e.flash = 0.15;
|
||||
if e.hp <= 0.0 {
|
||||
self.kill(j);
|
||||
}
|
||||
}
|
||||
self.part(RING, (wx, wy, 0.05), (0.0, 0.0, 0.0), 0.6, abgr(170, 230, 255), 2.5);
|
||||
self.sparkle(wx, wy, 24, abgr(200, 240, 255));
|
||||
self.sfx(Sfx::Magic, wx, wy);
|
||||
}
|
||||
}
|
||||
self.mana -= cost;
|
||||
}
|
||||
|
||||
+2
-1
@@ -485,7 +485,8 @@ impl Game {
|
||||
let near = |r: f32, ents: &Vec<Ent>| -> Vec<usize> { (0..ents.len()).filter(|&j| enemy(&ents[j]) && (ents[j].x - cx).powi(2) + (ents[j].y - cy).powi(2) < r * r).collect() };
|
||||
match t.kind {
|
||||
TrapKind::Frost => {
|
||||
// always on: cold around it slows and bites
|
||||
// always on: cold around it slows and bites, and keeps lava next to it cooled
|
||||
self.cool_lava_around(cx, cy, 2.5, 3.0);
|
||||
for j in near(2.5, &self.ents) {
|
||||
self.ents[j].slow = self.ents[j].slow.max(0.5);
|
||||
self.damage_unit(j, 2.0 * dt);
|
||||
|
||||
@@ -174,13 +174,14 @@ pub enum Tool {
|
||||
Tower(TowerKind),
|
||||
Bridge,
|
||||
Steel,
|
||||
Drawbridge,
|
||||
Trap(crate::traps::TrapKind),
|
||||
Spell(crate::spells::Spell),
|
||||
Order(u8), // 0 attack, 1 guard
|
||||
}
|
||||
|
||||
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
|
||||
pub const TOOLS: [Tool; 47] = [
|
||||
pub const TOOLS: [Tool; 49] = [
|
||||
Tool::Dig,
|
||||
Tool::Hand,
|
||||
Tool::Room(0),
|
||||
@@ -228,6 +229,8 @@ pub const TOOLS: [Tool; 47] = [
|
||||
Tool::Room(13),
|
||||
Tool::Order(0),
|
||||
Tool::Order(1),
|
||||
Tool::Drawbridge,
|
||||
Tool::Spell(crate::spells::Spell::Freeze),
|
||||
];
|
||||
|
||||
|
||||
@@ -259,7 +262,7 @@ pub fn tool_info(t: Tool) -> ToolInfo {
|
||||
Tool::Trap(k) => (k.id(), I_TRAPS + crate::traps::TRAP_KINDS.iter().position(|&t| t == k).unwrap_or(0) as u32, None),
|
||||
Tool::Order(0) => ("order_attack", I_ORDERS, None),
|
||||
Tool::Order(_) => ("order_guard", I_ORDERS + 1, None),
|
||||
Tool::Spell(sp) => (sp.id(), I_SPELLS + crate::spells::SPELLS.iter().position(|&t| t == sp).unwrap_or(0) as u32, None),
|
||||
Tool::Spell(sp) => (sp.id(), crate::spells::spell_icon(sp), None),
|
||||
Tool::Tower(TowerKind::Arrow) => ("arrow", I_ARROW, None),
|
||||
Tool::Tower(TowerKind::Cannon) => ("cannon", I_CANNON, None),
|
||||
Tool::Tower(TowerKind::Magic) => ("magic", I_MAGIC, None),
|
||||
@@ -270,6 +273,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::Drawbridge => ("drawbridge", I_DRAWBRIDGE, None),
|
||||
Tool::Steel => ("steel", I_STEEL, None),
|
||||
};
|
||||
let name = match room {
|
||||
@@ -285,6 +289,7 @@ pub fn tool_cost_static(t: Tool) -> String {
|
||||
Tool::Spell(sp) => trf("cost.mana", &[(sp.cost() as i32).to_string()]),
|
||||
Tool::Trap(k) => trf("cost.work", &[(k.work() as i32).to_string()]),
|
||||
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), BRIDGE_COST[1].to_string()]),
|
||||
Tool::Drawbridge => trf("cost.gold_tile", &[DRAWBRIDGE_COST.to_string()]),
|
||||
Tool::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_COST[1].to_string()]),
|
||||
Tool::Tower(TowerKind::Rune) => {
|
||||
let s = tower_stats(TowerKind::Rune);
|
||||
@@ -310,6 +315,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::Drawbridge => trf("cost.gold_tile", &[DRAWBRIDGE_COST.to_string()]),
|
||||
Tool::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_COST[1].to_string()]),
|
||||
Tool::Tower(TowerKind::Rune) => {
|
||||
let s = tower_stats(TowerKind::Rune);
|
||||
@@ -331,10 +337,10 @@ pub fn tool_cost(t: Tool, g: &Game) -> String {
|
||||
pub const CATS: [(&str, &[usize]); 7] = [
|
||||
("cat.dig", &[0]),
|
||||
("cat.hand", &[1]),
|
||||
("cat.rooms", &[2, 3, 4, 5, 6, 12, 40, 13, 18, 21, 41, 42, 43, 44, 17]),
|
||||
("cat.rooms", &[2, 3, 4, 5, 6, 12, 40, 13, 18, 21, 41, 42, 43, 44, 17, 47]),
|
||||
("cat.towers", &[14, 15, 16, 20]),
|
||||
("cat.traps", &[22, 23, 24, 25, 26, 27, 28, 29, 30, 31]),
|
||||
("cat.spells", &[9, 10, 11, 32, 33, 34, 35, 36, 37, 38, 39]),
|
||||
("cat.spells", &[9, 10, 11, 32, 33, 34, 35, 36, 37, 38, 39, 48]),
|
||||
("cat.orders", &[8, 45, 46, 7, 19]),
|
||||
];
|
||||
|
||||
@@ -1412,6 +1418,17 @@ impl Ui {
|
||||
}
|
||||
|
||||
// Info text for the unit or tile under the mouse (first line = title)
|
||||
// fire and wear of a bridge tile
|
||||
fn bridge_state(g: &Game, i: usize, out: &mut Vec<(String, Color)>) {
|
||||
if g.burning(i) {
|
||||
out.push((tr("tile.bridge_burning"), RED));
|
||||
}
|
||||
let w = g.wear_of(i);
|
||||
if w > 0.0 {
|
||||
out.push((trf("tile.bridge_worn", &[((w * 100.0) as i32).to_string()]), GOLD));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec<(String, Color)> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(j) = ent {
|
||||
@@ -1475,6 +1492,7 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
|
||||
return out;
|
||||
}
|
||||
let Some(t) = g.map.get(x, y) else { return out };
|
||||
let i = idx(x, y);
|
||||
let owner = match t.owner {
|
||||
Owner::Player => "owner.player",
|
||||
Owner::Hero => "owner.hero",
|
||||
@@ -1532,6 +1550,25 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
|
||||
Kind::Bridge | Kind::LavaBridge => {
|
||||
out.push((tr(if t.kind == Kind::Bridge { "tile.bridge" } else { "tile.lavabridge" }), FRAME_HI));
|
||||
out.push((tr(owner), DIM));
|
||||
bridge_state(g, i, &mut out);
|
||||
}
|
||||
Kind::Drawbridge => {
|
||||
out.push((tr("tile.drawbridge"), FRAME_HI));
|
||||
out.push((tr(owner), DIM));
|
||||
let state = if t.res & DB_AUTO != 0 {
|
||||
if t.raised() { "tile.drawbridge_auto_up" } else { "tile.drawbridge_auto" }
|
||||
} else if t.raised() {
|
||||
"tile.drawbridge_up"
|
||||
} else {
|
||||
"tile.drawbridge_down"
|
||||
};
|
||||
out.push((tr(state), BLUE));
|
||||
bridge_state(g, i, &mut out);
|
||||
}
|
||||
Kind::Obsidian => {
|
||||
out.push((tr("tile.obsidian"), FRAME_HI));
|
||||
let left = g.bridgework.cooled.iter().find(|c| c.0 == i).map_or(0.0, |c| c.1);
|
||||
out.push((trf("tile.obsidian_info", &[(left.ceil() as i32).to_string()]), if left < crate::bridges::MELT_WARN { RED } else { DIM }));
|
||||
}
|
||||
Kind::Shard => {
|
||||
out.push((tr("tile.shard"), RED));
|
||||
|
||||
Reference in new issue
Block a user