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:
1 parent
a1fe02ffa6
commit
903d6fb0f4
10 files changed
+708
-19
No files matched your search
+488
-8
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user