Defence towers: arrow (piercing), cannon (splash + knockback), magic (mana, slow, chain); 3 levels, repair by imps, heroes attack towers; torture/temple room tools prepared

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 07:47:50 +01:00
1 parent a1fe02ffa6
commit 903d6fb0f4
10 files changed
+708 -19

No files matched your search

+488 -8
View File
@@ -135,6 +135,8 @@ pub enum Job {
Leave,
Rally,
Reinforce(usize), // wall tile
Repair(u32), // tower id
AttackTower(u32), // tower id
}
#[derive(Clone, Serialize, Deserialize)]
@@ -166,6 +168,8 @@ pub struct Ent {
pub face: f32,
pub moving: bool,
pub working: bool,
#[serde(default)]
pub slow: f32, // seconds of slowdown left (magic tower)
pub guard: bool, // stays at its home (gate defenders, treasure guards)
#[serde(default)]
pub home: Option<(f32, f32)>, // guard post
@@ -201,6 +205,7 @@ impl Ent {
face: 1.0,
moving: false,
working: false,
slow: 0.0,
guard: false,
home: None,
}
@@ -250,7 +255,7 @@ pub struct Game {
pub state: u8, // 0 running, 1 victory, 2 defeat
pub paused: bool,
pub hand: Vec<u32>,
pub counts: [usize; 5], // treasury, lair, farm, training hall, library
pub counts: [usize; 7], // treasury, lair, farm, training hall, library, torture chamber, temple
pub gate_hp: f32,
pub gate_hit_t: f32,
pub gate_stage: u32, // defender thresholds triggered (0..3)
@@ -259,6 +264,8 @@ pub struct Game {
pub events: Vec<SfxEvent>,
pub last_wave_value: f32,
pub torches: Vec<Torch>,
#[serde(default)]
pub towers: Vec<Tower>,
#[serde(skip)]
pub hints: bool, // show hint messages (setting)
hint_t: [f32; 16],
@@ -280,9 +287,9 @@ impl Torch {
}
}
pub const ROOM_KINDS: [Kind; 5] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library];
pub const ROOM_COST: [i32; 5] = [60, 80, 100, 140, 160];
pub const ROOM_NAMES: [&str; 5] = ["room.treasury", "room.lair", "room.farm", "room.training", "room.library"];
pub const ROOM_KINDS: [Kind; 7] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Torture, Kind::Temple];
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"];
pub fn room_index(k: Kind) -> Option<usize> {
ROOM_KINDS.iter().position(|&r| r == k)
@@ -294,6 +301,15 @@ fn adj4(a: usize, b: usize) -> bool {
(ax - bx).abs() + (ay - by).abs() == 1
}
fn norm(x: f32, y: f32) -> (f32, f32) {
let l = (x * x + y * y).sqrt();
if l < 1e-6 {
(0.0, 0.0)
} else {
(x / l, y / l)
}
}
fn sgn(v: f32) -> f32 {
if v < 0.0 {
-1.0
@@ -324,7 +340,7 @@ impl Game {
state: 0,
paused: false,
hand: Vec::new(),
counts: [0; 5],
counts: [0; 7],
gate_hp: GATE_HP,
gate_hit_t: -100.0,
gate_stage: 0,
@@ -332,6 +348,7 @@ impl Game {
events: Vec::new(),
last_wave_value: 0.0,
torches: Vec::new(),
towers: Vec::new(),
hints: true,
hint_t: [-1000.0; 16],
};
@@ -401,6 +418,11 @@ impl Game {
d += e.atk() / st(e.kind).interval;
}
}
// towers count with half their combat value
for t in &self.towers {
h += 0.5 * t.max_hp();
d += 0.5 * t.dmg() / tower_stats(t.kind).interval;
}
(h * d).sqrt()
}
@@ -433,7 +455,7 @@ impl Game {
}
pub fn recount(&mut self) {
let mut c = [0usize; 5];
let mut c = [0usize; 7];
for t in &self.map.t {
if t.owner == Owner::Player {
if let Some(r) = room_index(t.kind) {
@@ -577,6 +599,7 @@ impl Game {
self.ents[i] = e;
}
self.ents.retain(|e| e.alive);
self.update_towers(dt);
if self.core_hp <= 0.0 {
self.core_hp = 0.0;
@@ -798,6 +821,7 @@ impl Game {
fn update_ent(&mut self, e: &mut Ent, dt: f32) {
e.anim += dt;
e.flash = (e.flash - dt).max(0.0);
e.slow = (e.slow - dt).max(0.0);
e.cd -= dt;
e.think -= dt;
e.repath -= dt;
@@ -866,7 +890,7 @@ impl Game {
let dx = cx - e.x;
let dy = cy - e.y;
let d = (dx * dx + dy * dy).sqrt();
let mut sp = st(e.kind).speed * dt;
let mut sp = st(e.kind).speed * dt * if e.slow > 0.0 { 0.55 } else { 1.0 };
if !e.hero && e.creature() && e.hunger > 2.0 {
sp *= 0.8;
}
@@ -1062,6 +1086,15 @@ impl Game {
found
}
// a damaged tower next to walkable tile i
fn damaged_tower_near(&self, i: usize) -> Option<u32> {
let (x, y) = xy(i);
self.towers.iter().find(|t| {
let (tx, ty) = xy(t.tile);
(tx - x).abs() <= 1 && (ty - y).abs() <= 1 && t.hp < t.max_hp() - 1.0
}).map(|t| t.id)
}
// an earth wall next to own floor that can be reinforced from tile i
fn reinforce_neighbor(&self, i: usize, me: u32) -> Option<usize> {
if self.map.t[i].owner != Owner::Player {
@@ -1171,6 +1204,27 @@ impl Game {
}
}
}
Job::Repair(id) => {
let Some(k) = self.towers.iter().position(|t| t.id == id) else {
e.job = Job::None;
return;
};
let (tx, ty) = self.towers[k].pos();
if (tx - e.x).abs() > 1.6 || (ty - e.y).abs() > 1.6 {
e.job = Job::None;
return;
}
e.face = sgn(tx - e.x);
e.working = true;
let t = &mut self.towers[k];
t.hp = (t.hp + 30.0 * dt).min(t.max_hp());
if t.hp >= t.max_hp() {
e.job = Job::None;
}
if (e.anim * 3.0).fract() < dt * 3.0 {
self.sfx(Sfx::Reinforce, tx, ty);
}
}
Job::Reinforce(w) => {
let t = self.map.t[w];
if !adj4(cur, w) || t.kind != Kind::Earth || t.reinforced || t.marked {
@@ -1274,6 +1328,14 @@ impl Game {
e.job = Job::Claim(end);
return;
}
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.damaged_tower_near(i).is_some(), true) {
let end = p.last().copied().unwrap_or(cur);
if let Some(id) = self.damaged_tower_near(end) {
e.path = p;
e.job = Job::Repair(id);
return;
}
}
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.reinforce_neighbor(i, me).is_some(), true) {
let end = p.last().copied().unwrap_or(cur);
if let Some(w) = self.reinforce_neighbor(end, me) {
@@ -1659,9 +1721,32 @@ impl Game {
e.job = Job::Attack(id);
e.path.clear();
e.repath = 0.0;
} else if !matches!(e.job, Job::AttackTower(_)) {
// no unit around: towers within reach are attacked
let near = self.towers.iter().filter(|t| {
let (tx, ty) = t.pos();
(tx - e.x).powi(2) + (ty - e.y).powi(2) < radius * radius
}).min_by(|a, b| {
let da = (a.pos().0 - e.x).powi(2) + (a.pos().1 - e.y).powi(2);
let db = (b.pos().0 - e.x).powi(2) + (b.pos().1 - e.y).powi(2);
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
}).map(|t| t.id);
if let Some(id) = near {
e.job = Job::AttackTower(id);
e.path.clear();
e.repath = 0.0;
}
}
}
}
if let Job::AttackTower(id) = e.job {
if self.attack_tower(e, id, dt) {
return;
}
e.job = Job::AttackCore;
e.path.clear();
e.think = 1.5;
}
if let Job::Attack(id) = e.job {
let (gx, gy) = e.home.unwrap_or_else(|| self.gate_center());
let far = e.guard && (e.x - gx).powi(2) + (e.y - gy).powi(2) > 14.0 * 14.0;
@@ -1773,7 +1858,14 @@ impl Game {
if !inb(x, y) {
continue;
}
let t = &mut self.map.t[idx(x, y)];
let i = idx(x, y);
if let Some(k) = self.towers.iter().position(|t| t.tile == i) {
refund += self.towers[k].invested() as f32 * 0.5;
self.towers.remove(k);
self.map.t[i].kind = Kind::Floor;
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;
@@ -1792,6 +1884,276 @@ impl Game {
}
}
pub fn tower_at(&self, i: usize) -> Option<&Tower> {
self.towers.iter().find(|t| t.tile == i)
}
// Build a tower on own floor, or upgrade an own tower of the same kind
pub fn build_tower(&mut self, kind: TowerKind, x: i32, y: i32) {
if !inb(x, y) {
return;
}
let i = idx(x, y);
let s = tower_stats(kind);
if let Some(k) = self.towers.iter().position(|t| t.tile == i) {
let t = &self.towers[k];
if t.kind != kind {
self.hint(14, "hint.tower_other");
return;
}
if t.level >= 3 {
self.hint(15, "hint.tower_max");
return;
}
let cost = s.cost[t.level as usize] as f32;
if self.gold < cost {
self.msg("msg.no_gold", vec![]);
self.sfx_ui(Sfx::Error);
return;
}
self.gold -= cost;
let t = &mut self.towers[k];
let old_max = t.max_hp();
t.level += 1;
t.hp += t.max_hp() - old_max;
let (px, py) = t.pos();
let lvl = t.level;
self.fx(7, px, py, 0.0, 0.0, 0.8, 0xff44ddff);
self.sfx(Sfx::LevelUp, px, py);
self.msg("msg.tower_upgraded", vec![format!("@{}", tower_name(kind)), lvl.to_string()]);
return;
}
let t = self.map.t[i];
if t.kind != Kind::Floor || t.owner != Owner::Player {
self.hint(16, "hint.tower_where");
self.sfx_ui(Sfx::Error);
return;
}
let cost = s.cost[0] as f32;
if self.gold < cost {
self.msg("msg.no_gold", vec![]);
self.sfx_ui(Sfx::Error);
return;
}
self.gold -= cost;
self.map.t[i].kind = Kind::Tower;
let id = self.next_id;
self.next_id += 1;
let tower = Tower { id, kind, tile: i, level: 1, hp: s.hp, cd: 1.0, aim: 0.0, flash: 0.0 };
let (px, py) = tower.pos();
self.towers.push(tower);
// units standing on the tile step aside
for e in self.ents.iter_mut() {
if e.alive && e.tile() == i {
for &(dx, dy) in &DIR8 {
if self.map.walk(x + dx, y + dy) {
e.x = (x + dx) as f32 + 0.5;
e.y = (y + dy) as f32 + 0.5;
e.path.clear();
break;
}
}
}
}
self.fx(8, px, py, 0.0, 0.0, 0.5, 0xff40d0ff);
self.sfx(Sfx::Build, px, py);
}
fn destroy_tower(&mut self, k: usize) {
let t = self.towers.remove(k);
self.map.t[t.tile].kind = Kind::Floor;
let (px, py) = t.pos();
self.fx(3, px, py, 0.0, 0.0, 1.0, 0xff505070);
self.sfx(Sfx::DigDone, px, py);
self.msg("msg.tower_destroyed", vec![format!("@{}", tower_name(t.kind))]);
}
// damage dealt to a unit by a tower (no attacker unit involved)
fn tower_damage(&mut self, j: usize, dmg: f32) {
let e = &mut self.ents[j];
e.hp -= dmg;
e.flash = 0.15;
if e.hp <= 0.0 && e.alive {
self.kill(j);
}
}
fn tower_target(&self, t: &Tower) -> Option<usize> {
let (px, py) = t.pos();
let r = t.range();
let minr = tower_stats(t.kind).min_range;
let mut best = None;
let mut bd = r * r;
for (j, e) in self.ents.iter().enumerate() {
if !e.alive || e.held || !e.hero {
continue;
}
let d = (e.x - px).powi(2) + (e.y - py).powi(2);
if d < bd && d >= minr * minr && self.los(px, py, e.x, e.y) {
bd = d;
best = Some(j);
}
}
best
}
fn update_towers(&mut self, dt: f32) {
for k in 0..self.towers.len() {
if k >= self.towers.len() {
break;
}
let t = &mut self.towers[k];
t.cd -= dt;
t.flash = (t.flash - dt).max(0.0);
if t.cd > 0.0 {
continue;
}
let t = self.towers[k].clone();
let Some(j) = self.tower_target(&t) else { continue };
let s = tower_stats(t.kind);
if s.mana > 0.0 && self.mana < s.mana {
self.hint(17, "hint.tower_mana");
self.towers[k].cd = 1.0;
continue;
}
self.mana -= s.mana;
self.towers[k].cd = s.interval;
self.towers[k].flash = 0.15;
let (px, py) = t.pos();
let (ex, ey) = (self.ents[j].x, self.ents[j].y);
self.towers[k].aim = (ey - py).atan2(ex - px);
let dmg = t.dmg();
match t.kind {
TowerKind::Arrow => {
// the arrow pierces up to two more heroes behind the target
let dir = norm(ex - px, ey - py);
let end = (px + dir.0 * t.range(), py + dir.1 * t.range());
self.fx(0, px, py - 0.6, end.0, end.1, 0.3, 0xffd0e0f0);
self.sfx(Sfx::Shoot, px, py);
let mut hits = vec![(j, dmg)];
for (o, e) in self.ents.iter().enumerate() {
if o == j || !e.alive || e.held || !e.hero || hits.len() >= 3 {
continue;
}
let rel = (e.x - px, e.y - py);
let along = rel.0 * dir.0 + rel.1 * dir.1;
let side = (rel.0 * dir.1 - rel.1 * dir.0).abs();
if along > 0.0 && along <= t.range() && side < 0.45 {
hits.push((o, dmg * 0.6));
}
}
for (o, d) in hits {
if self.ents[o].alive {
self.tower_damage(o, d);
}
}
}
TowerKind::Cannon => {
// splash damage and knockback around the impact
self.fx(0, px, py - 0.8, ex, ey, 0.35, 0xff303030);
self.fx(3, ex, ey, 0.0, 0.0, 0.6, 0xff4070a0);
self.fx(2, ex, ey - 0.2, 0.0, 0.0, 0.3, 0xff60c0ff);
self.sfx(Sfx::Cannon, px, py);
let mut hits = Vec::new();
for (o, e) in self.ents.iter().enumerate() {
if !e.alive || e.held || !e.hero {
continue;
}
let d = ((e.x - ex).powi(2) + (e.y - ey).powi(2)).sqrt();
if d < 1.6 {
hits.push((o, dmg * (1.0 - d / 2.0), d));
}
}
for (o, d, dist) in hits {
if !self.ents[o].alive {
continue;
}
// 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.tower_damage(o, d);
}
}
TowerKind::Magic => {
// sure hit, slows, jumps to a second hero
self.fx(0, px, py - 1.0, ex, ey, 0.2, 0xffff60d0);
self.sfx(Sfx::Magic, px, py);
self.ents[j].slow = 2.0;
let chain = self
.ents
.iter()
.enumerate()
.filter(|(o, e)| *o != j && e.alive && !e.held && e.hero && (e.x - ex).powi(2) + (e.y - ey).powi(2) < 9.0)
.min_by(|a, b| {
let da = (a.1.x - ex).powi(2) + (a.1.y - ey).powi(2);
let db = (b.1.x - ex).powi(2) + (b.1.y - ey).powi(2);
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(o, _)| o);
self.tower_damage(j, dmg);
if let Some(o) = chain {
let (ox, oy) = (self.ents[o].x, self.ents[o].y);
self.fx(0, ex, ey, ox, oy, 0.2, 0xffff60d0);
self.ents[o].slow = 2.0;
if self.ents[o].alive {
self.tower_damage(o, dmg * 0.6);
}
}
}
}
}
}
// a hero attacks a tower; false = tower gone or unreachable
fn attack_tower(&mut self, e: &mut Ent, id: u32, dt: f32) -> bool {
let Some(k) = self.towers.iter().position(|t| t.id == id) else { return false };
let (tx, ty) = self.towers[k].pos();
let s = st(e.kind);
let d = ((tx - e.x).powi(2) + (ty - e.y).powi(2)).sqrt();
if d <= s.range + 0.75 && (s.range < 1.5 || self.los(e.x, e.y, tx, ty)) {
e.path.clear();
e.face = sgn(tx - e.x);
e.working = true;
if e.cd <= 0.0 {
e.cd = s.interval;
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3);
if s.range > 1.5 {
self.fx(0, e.x, e.y - 0.3, tx, ty - 0.5, 0.25, 0xff70b0d0);
} else {
self.fx(2, tx, ty - 0.4, 0.0, 0.0, 0.25, 0xffffffff);
}
self.sfx(Sfx::MeleeHit, tx, ty);
let t = &mut self.towers[k];
t.hp -= dmg;
if t.hp <= 0.0 {
self.destroy_tower(k);
}
}
return true;
}
if e.path.is_empty() || e.repath <= 0.0 {
e.repath = 1.0;
let tile = self.towers[k].tile;
let (x, y) = xy(tile);
match self.map.search(e.tile(), self.map.walk_cost(), |i| {
let (ix, iy) = xy(i);
(ix - x).abs() <= 1 && (iy - y).abs() <= 1 && i != tile
}, true) {
Some(p) => e.path = p,
None => return false,
}
}
self.step_path(e, dt);
true
}
pub fn pickup(&mut self, target: Option<usize>) -> bool {
if self.hand.len() >= 8 {
self.hint(8, "hint.hand_full");
@@ -1903,6 +2265,73 @@ impl Game {
}
}
// ------------------------------------------------------------------ defence towers
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum TowerKind {
Arrow,
Cannon,
Magic,
}
pub struct TowerStats {
pub cost: [i32; 3], // build, upgrade to level 2, upgrade to level 3
pub hp: f32,
pub dmg: f32,
pub range: f32,
pub min_range: f32,
pub interval: f32,
pub mana: f32, // per shot
}
pub fn tower_stats(k: TowerKind) -> TowerStats {
match k {
TowerKind::Arrow => TowerStats { cost: [250, 200, 300], hp: 300.0, dmg: 10.0, range: 6.0, min_range: 0.0, interval: 0.8, mana: 0.0 },
TowerKind::Cannon => TowerStats { cost: [450, 350, 500], hp: 450.0, dmg: 32.0, range: 8.5, min_range: 2.0, interval: 2.6, mana: 0.0 },
TowerKind::Magic => TowerStats { cost: [350, 300, 450], hp: 300.0, dmg: 24.0, range: 7.0, min_range: 0.0, interval: 1.5, mana: 8.0 },
}
}
pub fn tower_name(k: TowerKind) -> &'static str {
match k {
TowerKind::Arrow => "tower.arrow",
TowerKind::Cannon => "tower.cannon",
TowerKind::Magic => "tower.magic",
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Tower {
pub id: u32,
pub kind: TowerKind,
pub tile: usize,
pub level: u32,
pub hp: f32,
pub cd: f32,
pub aim: f32, // direction of the last shot (radians, for drawing)
pub flash: f32,
}
impl Tower {
pub fn max_hp(&self) -> f32 {
tower_stats(self.kind).hp * [1.0, 1.4, 1.9][(self.level as usize - 1).min(2)]
}
pub fn dmg(&self) -> f32 {
tower_stats(self.kind).dmg * [1.0, 1.5, 2.2][(self.level as usize - 1).min(2)]
}
pub fn range(&self) -> f32 {
tower_stats(self.kind).range + 0.75 * (self.level - 1) as f32
}
pub fn pos(&self) -> (f32, f32) {
let (x, y) = xy(self.tile);
(x as f32 + 0.5, y as f32 + 0.5)
}
// gold spent on this tower so far
pub fn invested(&self) -> i32 {
tower_stats(self.kind).cost[..self.level as usize].iter().sum()
}
}
// Language key describing what a unit is doing
pub fn job_name(e: &Ent) -> &'static str {
match e.job {
@@ -1931,6 +2360,8 @@ pub fn job_name(e: &Ent) -> &'static str {
}
Job::Leave => "job.leave",
Job::Reinforce(_) => "job.reinforce",
Job::Repair(_) => "job.repair",
Job::AttackTower(_) => "job.attack_tower",
Job::Rally => {
if e.working {
"job.gate_attack"
@@ -2035,4 +2466,53 @@ mod tests {
assert_eq!(g.map.hero_cost()(idx(w.0, w.1)), Some(70));
}
}
#[test]
fn towers_shoot_and_get_attacked() {
for kind in [TowerKind::Arrow, TowerKind::Cannon, TowerKind::Magic] {
let mut g = Game::new(5, 64, 64);
g.gold = 2000.0;
g.mana = 400.0;
let (cx, cy) = g.map.core;
let (tx, ty) = (cx - 2, cy - 2);
g.build_tower(kind, tx, ty);
assert_eq!(g.towers.len(), 1);
g.build_tower(kind, tx, ty); // upgrade
assert_eq!(g.towers[0].level, 2);
// a hero inside the start area, 4 tiles away
let id = g.spawn(EK::Knight, true, (cx + 3) as f32 + 0.5, (cy - 2) as f32 + 0.5, 1);
let hp0 = g.ents.iter().find(|e| e.id == id).unwrap().hp;
let mut hurt = false;
let mut tower_hit = false;
for _ in 0..400 {
g.update(0.05);
if let Some(e) = g.ents.iter().find(|e| e.id == id) {
hurt |= e.hp < hp0;
} else {
hurt = true;
}
tower_hit |= g.towers.first().map_or(true, |t| t.hp < t.max_hp());
}
println!("{:?}: hero hurt {}, tower attacked {}", kind, hurt, tower_hit);
assert!(hurt, "{:?} did not damage the hero", kind);
}
}
#[test]
fn heroes_attack_towers() {
let mut g = Game::new(5, 64, 64);
g.gold = 2000.0;
for e in g.ents.iter_mut() {
e.alive = false; // no imps: the tower is the only target
}
let (cx, cy) = g.map.core;
g.build_tower(TowerKind::Cannon, cx - 2, cy - 2);
g.spawn(EK::Ogre, true, (cx - 2) as f32 + 0.5, (cy + 4) as f32 + 0.5, 5);
for _ in 0..600 {
g.update(0.05);
}
let t = g.towers.first();
println!("tower after attack: {:?}", t.map(|t| t.hp));
assert!(t.map_or(true, |t| t.hp < t.max_hp()));
}
}
+28
View File
@@ -46,6 +46,8 @@ pub const D_RACK: u32 = 80;
pub const I_ARROW: u32 = 81;
pub const I_CANNON: u32 = 82;
pub const I_MAGIC: u32 = 83;
pub const T_TEMPLE: u32 = 84; // 4 variants
pub const D_ALTAR: u32 = 88;
pub struct Gfx {
pub atlas: Texture2D,
@@ -391,6 +393,31 @@ fn gen_atlas() -> Image {
Some(stones(x, y, v + 94, (52, 40, 44), (26, 16, 18), 10))
});
}
// temple: dark marble with a glowing red-gold rune circle
for v in 0..4u32 {
cv.cell(T_TEMPLE + v, |x, y| {
let (dx, dy) = (x as f32 - 7.5, y as f32 - 7.5);
let d = (dx * dx + dy * dy).sqrt();
if (5.0..6.0).contains(&d) {
return Some(c(170, 70, 40));
}
if d < 1.5 {
return Some(c(220, 150, 60));
}
let vein = ((x as f32 * 0.6 + (y as f32 * 0.7).sin() * 2.0 + v as f32 * 3.0) as i32) % 7 == 0;
Some(if vein { c(70, 54, 66) } else { shade((44, 34, 46), (rnd(x, y, v + 95) * 10.0) as i32) })
});
}
// altar with a flame
cv.cell(D_ALTAR, |x, y| {
if y >= 9 && (3..=12).contains(&x) {
Some(if y == 9 { c(150, 140, 150) } else { c(100, 92, 104) })
} else if y < 9 && y >= 3 && (x - 7).abs() + (y - 8).abs() / 2 <= 3 {
Some(if (x + y) % 3 == 0 { c(255, 220, 80) } else { c(255, 120, 30) })
} else {
None
}
});
// torture wheel
cv.cell(D_RACK, |x, y| {
let (dx, dy) = (x as f32 - 7.5, y as f32 - 7.0);
@@ -743,6 +770,7 @@ impl Gfx {
Kind::Portal => T_PORTALFLOOR + v,
Kind::Gate => T_GATEFLOOR + v,
Kind::Torture => T_TORTURE + v,
Kind::Temple => T_TEMPLE + v,
Kind::Tower => T_CLAIM + v,
}
}
+2
View File
@@ -476,6 +476,7 @@ async fn main() {
(Tool::Lightning, _) => app.game.lightning(wx, wy),
(Tool::Heal, _) => app.game.heal(wx, wy),
(Tool::Summon, _) => app.game.summon(wx, wy),
(Tool::Tower(k), Some((x, z))) => app.game.build_tower(k, x, z),
_ => {}
}
}
@@ -568,6 +569,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::Tower(_) => (Color::new(1.0, 0.6, 0.25, 1.0), None),
};
let ov = Overlay {
hover: if over_ui { None } else { hover_tile.filter(|&(x, z)| inb(x, z)) },
+4 -2
View File
@@ -48,10 +48,11 @@ pub enum Kind {
Gate,
Torture,
Tower, // floor tile occupied by a defence tower (not walkable)
Temple,
}
// order matters for the packed savegame codec: only append new kinds
const KINDS: [Kind; 14] = [
const KINDS: [Kind; 15] = [
Kind::Rock,
Kind::Earth,
Kind::Gold,
@@ -66,6 +67,7 @@ const KINDS: [Kind; 14] = [
Kind::Gate,
Kind::Torture,
Kind::Tower,
Kind::Temple,
];
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
@@ -115,7 +117,7 @@ impl Tile {
pub fn walkable(&self) -> bool {
matches!(
self.kind,
Kind::Floor | Kind::Treasury | Kind::Lair | Kind::Farm | Kind::Training | Kind::Library | Kind::Portal | Kind::Gate | Kind::Torture
Kind::Floor | Kind::Treasury | Kind::Lair | Kind::Farm | Kind::Training | Kind::Library | Kind::Portal | Kind::Gate | Kind::Torture | Kind::Temple
)
}
}
+1 -1
View File
@@ -413,7 +413,7 @@ impl Menu {
let sh = screen_height();
let (mx, my) = mouse_position();
let cols = ((sw / CW()) as i32 - 2).min(66);
let rows = ((sh / CH()) as i32 - 2).min(28);
let rows = ((sh / CH()) as i32 - 2).min(34);
let x = ((sw - cols as f32 * CW()) * 0.5).max(0.0);
let y = ((sh - rows as f32 * CH()) * 0.5).max(0.0);
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &tr("menu.settings"), FRAME_HI);
+83
View File
@@ -531,6 +531,8 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas:
}
Kind::Training if (x + z) % 2 == 0 => mb.cross(xf + 0.5, zf + 0.5, 0.7, 0.75, D_DUMMY, dc),
Kind::Library if z % 2 == 0 => mb.cross(xf + 0.5, zf + 0.5, 0.9, 0.85, D_SHELF, dc),
Kind::Torture if (x + z) % 2 == 1 => mb.cross(xf + 0.5, zf + 0.5, 0.8, 0.8, D_RACK, dc),
Kind::Temple if x % 3 == 1 && z % 3 == 1 => mb.cross(xf + 0.5, zf + 0.5, 0.8, 0.8, D_ALTAR, dc),
Kind::Treasury if gold_fill > 0.02 => {
let s = 0.35 + 0.55 * gold_fill;
mb.cross(xf + 0.5, zf + 0.5, s, s * 0.8, D_COINS, dc);
@@ -801,6 +803,12 @@ 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);
}
for tw in &g.towers {
if tw.kind == TowerKind::Magic {
let (x, z) = tw.pos();
push(vec3(x, tower_height(tw) + 0.6, z), 4.5, vec3(0.75, 0.4, 1.0), 0.8 + tw.flash * 3.0, 10.0);
}
}
for f in &g.fx {
if f.kind == 1 {
push(vec3(f.x, 2.0, f.y), 8.0, vec3(1.0, 1.0, 0.85), 3.0 * (1.0 - f.t / f.max), 200.0);
@@ -874,6 +882,7 @@ impl R3d {
draw_cube(vec3(x, 0.55, z), vec3(0.07, 0.3, 0.07), None, Color::from_rgba(70, 45, 28, 255));
}
draw_landmarks_solid(g, t);
draw_towers_solid(g);
// ---- unlit glow / overlays
gl_use_material(&self.glow);
@@ -898,6 +907,7 @@ impl R3d {
}
draw_shadows(g);
draw_landmarks_glow(g, t);
draw_towers_glow(g, t);
if let Some(f) = g.flag {
draw_flag(f, t);
}
@@ -1007,6 +1017,16 @@ impl R3d {
draw_circle(p.x + w * 0.35, p.y, 3.0, Color::from_rgba(250, 210, 60, 255));
}
}
for tw in &g.towers {
if tw.hp < tw.max_hp() {
let (x, z) = tw.pos();
let p = Cam::project(m, vec3(x, tower_height(tw) + 0.9, z));
let w = 40.0;
let f = (tw.hp / tw.max_hp()).clamp(0.0, 1.0);
draw_rectangle(p.x - w * 0.5 - 1.0, p.y - 1.0, w + 2.0, 5.0, BLACK);
draw_rectangle(p.x - w * 0.5, p.y, w * f, 3.0, Color::from_rgba(255, 160, 60, 255));
}
}
if g.gate_hp > 0.0 && g.gate_hp < GATE_HP {
let (gx, gz) = g.gate_center();
let p = Cam::project(m, vec3(gx, 2.4, gz));
@@ -1017,6 +1037,68 @@ impl R3d {
}
}
// ------------------------------------------------------------------ towers
pub fn tower_height(t: &Tower) -> f32 {
match t.kind {
TowerKind::Arrow => 1.0 + 0.3 * (t.level - 1) as f32,
TowerKind::Cannon => 0.6 + 0.15 * (t.level - 1) as f32,
TowerKind::Magic => 1.2 + 0.3 * (t.level - 1) as f32,
}
}
fn shade_c(r: u8, g: u8, b: u8, f: f32) -> Color {
Color::new(r as f32 / 255.0 * f, g as f32 / 255.0 * f, b as f32 / 255.0 * f, 1.0)
}
fn draw_towers_solid(g: &Game) {
for tw in &g.towers {
let (x, z) = tw.pos();
let h = tower_height(tw);
let f = 0.75 + tw.flash * 1.5; // brightens briefly when firing
match tw.kind {
TowerKind::Arrow => {
draw_cube(vec3(x, h * 0.5, z), vec3(0.7, h, 0.7), None, shade_c(110, 75, 45, f));
draw_cube(vec3(x, h + 0.06, z), vec3(0.92, 0.12, 0.92), None, shade_c(130, 90, 55, f));
for (dx, dz) in [(-0.38, -0.38), (0.38, -0.38), (0.38, 0.38), (-0.38, 0.38)] {
draw_cube(vec3(x + dx, h + 0.2, z + dz), vec3(0.15, 0.18, 0.15), None, shade_c(130, 90, 55, f));
}
}
TowerKind::Cannon => {
draw_cube(vec3(x, h * 0.5, z), vec3(0.88, h, 0.88), None, shade_c(100, 100, 110, f));
draw_cube(vec3(x, h + 0.08, z), vec3(0.5, 0.16, 0.5), None, shade_c(70, 70, 78, f));
let (ax, az) = (tw.aim.cos(), tw.aim.sin());
for k in 0..4 {
let d = 0.1 + k as f32 * 0.14;
draw_cube(vec3(x + ax * d, h + 0.2, z + az * d), Vec3::splat(0.2 - k as f32 * 0.015), None, shade_c(40, 40, 46, f));
}
}
TowerKind::Magic => {
draw_cube(vec3(x, h * 0.5, z), vec3(0.42, h, 0.42), None, shade_c(84, 74, 104, f));
draw_cube(vec3(x, 0.1, z), vec3(0.75, 0.2, 0.75), None, shade_c(70, 62, 86, f));
draw_cube(vec3(x, h, z), vec3(0.6, 0.1, 0.6), None, shade_c(110, 96, 130, f));
}
}
// level pips
for k in 0..tw.level {
draw_cube(vec3(x - 0.2 + k as f32 * 0.2, 0.25, z + 0.46), Vec3::splat(0.09), None, Color::from_rgba(240, 200, 70, 255));
}
}
}
fn draw_towers_glow(g: &Game, t: f32) {
for (k, tw) in g.towers.iter().enumerate() {
if tw.kind != TowerKind::Magic {
continue;
}
let (x, z) = tw.pos();
let c = vec3(x, tower_height(tw) + 0.45 + (t * 2.0 + k as f32).sin() * 0.06, z);
let pulse = 1.0 + (t * 4.0 + k as f32).sin() * 0.1 + tw.flash * 2.0;
draw_sphere(c, 0.16 * pulse, None, Color::new(0.85, 0.5, 1.0, 0.95));
draw_sphere(c, 0.3 * pulse, None, Color::new(0.7, 0.3, 1.0, 0.25));
}
}
// ------------------------------------------------------------------ landmarks, fx, overlay
// Camera direction for camera-facing line quads (set once per frame)
@@ -1286,5 +1368,6 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Kind::Gate => [230, 230, 255, 255],
Kind::Torture => [120, 40, 40, 255],
Kind::Tower => [255, 140, 60, 255],
Kind::Temple => [200, 110, 50, 255],
}
}
+3 -3
View File
@@ -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..12, same order as ui::TOOLS
Tool(usize), // 0..17, 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..12).map(Action::Tool).collect();
let mut v: Vec<Action> = (0..17).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][i.min(11)],
Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V, K, J, B, C, T][i.min(16)],
Action::CamForward => W,
Action::CamBack => S,
Action::CamLeft => A,
+33 -3
View File
@@ -55,9 +55,11 @@ pub enum Tool {
Lightning,
Heal,
Summon,
Tower(TowerKind),
}
pub const TOOLS: [Tool; 12] = [
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
pub const TOOLS: [Tool; 17] = [
Tool::Dig,
Tool::Hand,
Tool::Room(0),
@@ -70,6 +72,11 @@ pub const TOOLS: [Tool; 12] = [
Tool::Lightning,
Tool::Heal,
Tool::Summon,
Tool::Room(5),
Tool::Room(6),
Tool::Tower(TowerKind::Arrow),
Tool::Tower(TowerKind::Cannon),
Tool::Tower(TowerKind::Magic),
];
@@ -88,7 +95,12 @@ pub fn tool_info(t: Tool) -> ToolInfo {
Tool::Room(1) => ("lair", T_LAIR, Some(1)),
Tool::Room(2) => ("farm", T_FARM, Some(2)),
Tool::Room(3) => ("training", T_TRAINING, Some(3)),
Tool::Room(_) => ("library", T_LIBRARY, Some(4)),
Tool::Room(4) => ("library", T_LIBRARY, Some(4)),
Tool::Room(5) => ("torture", T_TORTURE, Some(5)),
Tool::Room(_) => ("temple", T_TEMPLE, Some(6)),
Tool::Tower(TowerKind::Arrow) => ("arrow", I_ARROW, None),
Tool::Tower(TowerKind::Cannon) => ("cannon", I_CANNON, None),
Tool::Tower(TowerKind::Magic) => ("magic", I_MAGIC, None),
Tool::Sell => ("sell", I_SELL, None),
Tool::Flag => ("flag", I_FLAG, None),
Tool::Lightning => ("lightning", I_BOLT, None),
@@ -108,6 +120,14 @@ 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::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()]);
if s.mana > 0.0 {
c += &trf("cost.per_shot", &[(s.mana as i32).to_string()]);
}
c
}
_ => String::new(),
}
}
@@ -291,7 +311,7 @@ impl Ui {
gfx.window(0.0, tb_y, cols, tb_rows, S(), false, FRAME, BG);
self.rects.push(Rect::new(0.0, tb_y, sw, tb_rows as f32 * CH()));
self.tool_rects.clear();
let bw = 80.0 * K();
let bw = (80.0 * K()).min(((sw - CW() * 2.0 - 150.0) / TOOLS.len() as f32 - 4.0 * K()).max(48.0));
let mut hover_tool = None;
for (i, &t) in TOOLS.iter().enumerate() {
let bx = CW() + i as f32 * (bw + 4.0 * K());
@@ -638,6 +658,16 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
out.push((tr("tile.portal"), MANA));
out.push((tr(if t.owner == Owner::Player { "portal.claimed" } else { "portal.neutral" }), DIM));
}
Kind::Tower => {
if let Some(tw) = g.tower_at(idx(x, y)) {
out.push((trf("info.tower", &[tr(tower_name(tw.kind)), tw.level.to_string()]), FRAME_HI));
out.push((trf("info.hp", &[(tw.hp as i32).to_string(), (tw.max_hp() as i32).to_string()]), GREEN));
out.push((trf("info.tower_stats", &[format!("{:.0}", tw.dmg()), format!("{:.1}", tw.range())]), TXT));
if tw.level < 3 {
out.push((trf("info.tower_upgrade", &[tower_stats(tw.kind).cost[tw.level as usize].to_string()]), GOLD));
}
}
}
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));