// Game logic: units, AI, economy, hero waves. use crate::map::*; pub const CORE_HP: f32 = 2000.0; pub const GATE_HP: f32 = 3000.0; pub const PAYDAY: f32 = 150.0; pub const FIRST_WAVE: f32 = 300.0; pub const WAVE_GAP: f32 = 180.0; pub const MAX_SQUAD: usize = 18; // Sound events. The game logic reports them, the sound module plays them. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Sfx { Dig, GoldDig, DigDone, Claim, Deposit, Build, Sell, MeleeHit, Shoot, Death, HeroDeath, Lightning, Heal, Summon, PortalArrive, WaveAlarm, WaveStart, CoreHit, GateHit, GateDefenders, LevelUp, Pickup, Drop, Payday, FlagSet, Victory, Defeat, Click, Error, } pub const SFX_ALL: [Sfx; 29] = [ Sfx::Dig, Sfx::GoldDig, Sfx::DigDone, Sfx::Claim, Sfx::Deposit, Sfx::Build, Sfx::Sell, Sfx::MeleeHit, Sfx::Shoot, Sfx::Death, Sfx::HeroDeath, Sfx::Lightning, Sfx::Heal, Sfx::Summon, Sfx::PortalArrive, Sfx::WaveAlarm, Sfx::WaveStart, Sfx::CoreHit, Sfx::GateHit, Sfx::GateDefenders, Sfx::LevelUp, Sfx::Pickup, Sfx::Drop, Sfx::Payday, Sfx::FlagSet, Sfx::Victory, Sfx::Defeat, Sfx::Click, Sfx::Error, ]; impl Sfx { pub fn index(self) -> usize { self as usize } } // A game message: language key + arguments (arguments starting with '@' are keys too) #[derive(Clone, Debug)] pub struct Msg { pub key: &'static str, pub args: Vec, } #[derive(Clone, Copy)] pub struct SfxEvent { pub sfx: Sfx, pub pos: Option<(f32, f32)>, // world position (for distance-based volume), None = UI } pub const MANA_MAX: f32 = 400.0; pub const CARRY_MAX: i32 = 300; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum EK { Worker, Kobold, Spider, Mage, Ogre, Knight, Archer, Priest, Dwarf, Paladin, } pub struct St { pub name: &'static str, pub hp: f32, pub atk: f32, pub range: f32, pub speed: f32, pub interval: f32, pub wage: i32, pub bounty: i32, } pub fn st(k: EK) -> St { let s = |name, hp, atk, range, speed, interval, wage, bounty| St { name, hp, atk, range, speed, interval, wage, bounty }; match k { EK::Worker => s("unit.worker", 45.0, 3.0, 1.0, 3.2, 1.0, 0, 0), EK::Kobold => s("unit.kobold", 75.0, 8.0, 1.0, 2.6, 0.9, 40, 0), EK::Spider => s("unit.spider", 95.0, 10.0, 1.0, 2.9, 1.0, 60, 0), EK::Mage => s("unit.mage", 60.0, 13.0, 4.5, 2.0, 1.4, 110, 0), EK::Ogre => s("unit.ogre", 230.0, 20.0, 1.0, 1.8, 1.4, 150, 0), EK::Knight => s("unit.knight", 140.0, 11.0, 1.0, 1.9, 1.1, 0, 120), EK::Archer => s("unit.archer", 75.0, 9.0, 4.5, 2.1, 1.3, 0, 100), EK::Priest => s("unit.priest", 80.0, 5.0, 3.5, 1.9, 1.5, 0, 120), EK::Dwarf => s("unit.dwarf", 110.0, 9.0, 1.0, 2.0, 1.0, 0, 100), EK::Paladin => s("unit.paladin", 700.0, 26.0, 1.0, 1.7, 1.2, 0, 1000), } } #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Job { None, Dig(usize), Claim(usize), Deliver, Wander, Sleep(usize), Eat(usize), Train(usize), Study(usize), Attack(u32), AttackCore, Leave, Rally, } #[derive(Clone)] pub struct Ent { pub id: u32, pub kind: EK, pub hero: bool, pub x: f32, pub y: f32, pub hp: f32, pub max_hp: f32, pub level: u32, pub xp: f32, pub job: Job, pub path: Vec, pub timer: f32, pub cd: f32, pub think: f32, pub repath: f32, pub carry: i32, pub hunger: f32, pub energy: f32, pub happy: f32, pub bed: Option, pub alive: bool, pub held: bool, pub anim: f32, pub flash: f32, pub face: f32, pub moving: bool, pub working: bool, pub guard: bool, // gate defender } impl Ent { fn dummy() -> Ent { Ent { id: 0, kind: EK::Worker, hero: false, x: 0.0, y: 0.0, hp: 0.0, max_hp: 1.0, level: 1, xp: 0.0, job: Job::None, path: Vec::new(), timer: 0.0, cd: 0.0, think: 0.0, repath: 0.0, carry: 0, hunger: 0.0, energy: 1.0, happy: 1.0, bed: None, alive: false, held: false, anim: 0.0, flash: 0.0, face: 1.0, moving: false, working: false, guard: false, } } pub fn tile(&self) -> usize { idx((self.x as i32).clamp(0, MW as i32 - 1), (self.y as i32).clamp(0, MH as i32 - 1)) } pub fn atk(&self) -> f32 { st(self.kind).atk * (1.0 + 0.12 * (self.level - 1) as f32) } pub fn creature(&self) -> bool { !self.hero && self.kind != EK::Worker } } #[derive(Clone, Copy)] pub struct Fx { pub kind: u8, // 0 projectile, 1 lightning, 2 hit, 3 smoke, 4 heal, 5 gold, 6 dust, 7 level up, 8 claim pub x: f32, pub y: f32, pub x2: f32, pub y2: f32, pub t: f32, pub max: f32, pub col: u32, } pub struct Game { pub map: Map, pub ents: Vec, pub next_id: u32, pub rng: Rng, pub gold: f32, pub mana: f32, pub core_hp: f32, pub time: f32, pub wave: u32, pub wave_timer: f32, pub warned: bool, pub portal_timer: f32, pub payday: f32, pub fx: Vec, pub msgs: Vec, pub state: u8, // 0 running, 1 victory, 2 defeat pub paused: bool, pub hand: Vec, pub counts: [usize; 5], // treasury, lair, farm, training hall, library pub gate_hp: f32, pub gate_hit_t: f32, pub gate_stage: u32, // defender thresholds triggered (0..3) pub flag: Option, pub events: Vec, pub last_wave_value: f32, pub torches: Vec, hint_t: [f32; 16], } // torch on a wall face: wall tile + direction (index into DIR4) towards the open side #[derive(Clone, Copy, PartialEq)] pub struct Torch { pub wall: usize, pub dir: usize, } impl Torch { // world position of the flame (x, z) on the ground plane pub fn pos(&self) -> (f32, f32) { let (x, y) = xy(self.wall); let (dx, dy) = DIR4[self.dir]; (x as f32 + 0.5 + dx as f32 * 0.58, y as f32 + 0.5 + dy as f32 * 0.58) } } 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 fn room_index(k: Kind) -> Option { ROOM_KINDS.iter().position(|&r| r == k) } fn adj4(a: usize, b: usize) -> bool { let (ax, ay) = xy(a); let (bx, by) = xy(b); (ax - bx).abs() + (ay - by).abs() == 1 } fn sgn(v: f32) -> f32 { if v < 0.0 { -1.0 } else { 1.0 } } impl Game { pub fn new(seed: u32) -> Game { let map = Map::generate(seed); let mut g = Game { map, ents: Vec::new(), next_id: 1, rng: Rng(seed.wrapping_mul(2654435761) | 1), gold: 1500.0, mana: 100.0, core_hp: CORE_HP, time: 0.0, wave: 0, wave_timer: FIRST_WAVE, warned: false, portal_timer: 10.0, payday: PAYDAY, fx: Vec::new(), msgs: Vec::new(), state: 0, paused: false, hand: Vec::new(), counts: [0; 5], gate_hp: GATE_HP, gate_hit_t: -100.0, gate_stage: 0, flag: None, events: Vec::new(), last_wave_value: 0.0, torches: Vec::new(), hint_t: [-1000.0; 16], }; let (cx, cy) = g.map.core; for k in 0..4 { g.spawn(EK::Worker, false, cx as f32 - 0.5, cy as f32 + k as f32 * 0.7 + 0.3, 1); } g.recount(); for i in 0..N { if g.map.t[i].owner == Owner::Player { g.place_torches(i); } } g.msg("msg.welcome1", vec![]); g.msg("msg.welcome2", vec![]); g.msg("msg.welcome3", vec![]); g.msg("msg.welcome4", vec![]); g } pub fn msg(&mut self, key: &'static str, args: Vec) { self.msgs.push(Msg { key, args }); if self.msgs.len() > 50 { self.msgs.remove(0); } } pub fn sfx(&mut self, sfx: Sfx, x: f32, y: f32) { if self.events.len() < 64 { self.events.push(SfxEvent { sfx, pos: Some((x, y)) }); } } pub fn sfx_ui(&mut self, sfx: Sfx) { if self.events.len() < 64 { self.events.push(SfxEvent { sfx, pos: None }); } } pub fn gate_center(&self) -> (f32, f32) { (self.map.gate.0 as f32 + 1.5, self.map.gate.1 as f32 + 1.5) } // Army value: sqrt(sum of HP * sum of damage/s) of all own creatures. pub fn army_value(&self) -> f32 { let (mut h, mut d) = (0.0, 0.0); for e in &self.ents { if e.alive && e.creature() { h += e.max_hp; d += e.atk() / st(e.kind).interval; } } (h * d).sqrt() } // imps hang torches on walls next to claimed floor (min. distance 3.5 tiles) pub fn place_torches(&mut self, i: usize) { let (x, y) = xy(i); for (d, &(dx, dy)) in DIR4.iter().enumerate() { let (wx, wy) = (x + dx, y + dy); if !inb(wx, wy) || !self.map.t[idx(wx, wy)].solid() { continue; } // the direction from the wall to the floor is the opposite one let t = Torch { wall: idx(wx, wy), dir: (d + 2) % 4 }; let (px, pz) = t.pos(); let near = self.torches.iter().any(|o| { let (ox, oz) = o.pos(); (ox - px).powi(2) + (oz - pz).powi(2) < 3.5 * 3.5 }); if !near { self.torches.push(t); } } } fn hint(&mut self, slot: usize, key: &'static str) { if self.time - self.hint_t[slot] > 30.0 { self.hint_t[slot] = self.time; self.msg(key, vec![]); } } pub fn recount(&mut self) { let mut c = [0usize; 5]; for t in &self.map.t { if t.owner == Owner::Player { if let Some(r) = room_index(t.kind) { c[r] += 1; } } } self.counts = c; } pub fn gold_cap(&self) -> f32 { 2000.0 + 500.0 * self.counts[0] as f32 } pub fn add_gold(&mut self, amount: f32) -> f32 { let cap = self.gold_cap(); let acc = amount.min((cap - self.gold).max(0.0)); self.gold += acc; acc } pub fn portal_owned(&self) -> bool { let (px, py) = self.map.portal; self.map.t[idx(px, py)].owner == Owner::Player } pub fn creature_count(&self) -> usize { self.ents.iter().filter(|e| e.alive && e.creature()).count() } pub fn worker_count(&self) -> usize { self.ents.iter().filter(|e| e.alive && !e.hero && e.kind == EK::Worker).count() } pub fn summon_cost(&self) -> f32 { 80.0 + 20.0 * (self.worker_count() as f32 - 4.0).max(0.0) } pub fn spawn(&mut self, kind: EK, hero: bool, x: f32, y: f32, level: u32) -> u32 { let s = st(kind); let id = self.next_id; self.next_id += 1; let mut e = Ent::dummy(); e.id = id; e.kind = kind; e.hero = hero; e.x = x; e.y = y; e.level = level; e.max_hp = s.hp * (1.0 + 0.15 * (level - 1) as f32); e.hp = e.max_hp; e.alive = true; e.think = self.rng.f(); e.energy = 0.7 + self.rng.f() * 0.3; e.hunger = self.rng.f() * 0.3; e.anim = self.rng.f() * 10.0; self.ents.push(e); id } pub fn fx(&mut self, kind: u8, x: f32, y: f32, x2: f32, y2: f32, max: f32, col: u32) { if self.fx.len() < 400 { self.fx.push(Fx { kind, x, y, x2, y2, t: 0.0, max, col }); } } pub fn find(&self, id: u32) -> Option { self.ents.iter().position(|e| e.id == id && e.alive && !e.held) } // ---------------------------------------------------------------- update pub fn update(&mut self, dt: f32) { let dt = dt.min(0.1); if self.state == 0 && !self.paused { self.step(dt); } for f in self.fx.iter_mut() { f.t += dt; } self.fx.retain(|f| f.t < f.max); } fn step(&mut self, dt: f32) { self.time += dt; self.recount(); self.mana = (self.mana + 1.0 * dt).min(MANA_MAX); self.core_hp = (self.core_hp + 0.5 * dt).min(CORE_HP); for t in self.map.t.iter_mut() { if t.kind == Kind::Farm { t.food = (t.food + dt / 18.0).min(3.0); } } if self.portal_owned() { self.portal_timer -= dt; if self.portal_timer <= 0.0 { self.portal_timer = 20.0 + self.rng.f() * 14.0; self.try_portal(); } } self.payday -= dt; if self.payday <= 0.0 { self.payday = PAYDAY; self.do_payday(); } self.wave_timer -= dt; if !self.warned && self.wave_timer < 30.0 { self.warned = true; self.msg("msg.wave_warn", vec![(self.wave + 1).to_string()]); self.sfx_ui(Sfx::WaveAlarm); } if self.wave_timer <= 0.0 { self.spawn_wave(); self.wave += 1; self.wave_timer = WAVE_GAP + self.rng.f() * 40.0; self.warned = false; } if self.time - self.gate_hit_t > 10.0 { self.gate_hp = (self.gate_hp + 8.0 * dt).min(GATE_HP); } for i in 0..self.ents.len() { if !self.ents[i].alive || self.ents[i].held { continue; } let mut e = std::mem::replace(&mut self.ents[i], Ent::dummy()); self.update_ent(&mut e, dt); self.ents[i] = e; } self.ents.retain(|e| e.alive); if self.core_hp <= 0.0 { self.core_hp = 0.0; self.state = 2; self.msg("msg.defeat", vec![]); self.sfx_ui(Sfx::Defeat); } else if self.gate_hp <= 0.0 { self.gate_hp = 0.0; self.state = 1; self.msg("msg.victory", vec![]); self.sfx_ui(Sfx::Victory); } } fn try_portal(&mut self) { let creatures = self.creature_count(); let lair = self.counts[1]; let farm = self.counts[2]; let cap = lair.min(30); if lair == 0 { self.hint(0, "hint.portal_no_lair"); return; } if farm == 0 { self.hint(1, "hint.no_farm"); return; } if creatures >= cap { self.hint(2, "hint.lair_full"); return; } let mut opts: Vec<(EK, u32)> = vec![(EK::Kobold, 4)]; if farm >= 6 { opts.push((EK::Spider, 3)); } if self.counts[4] >= 6 { opts.push((EK::Mage, 3)); } if self.counts[3] >= 9 { opts.push((EK::Ogre, 2)); } let total: u32 = opts.iter().map(|o| o.1).sum(); let mut r = self.rng.next() % total; let mut kind = EK::Kobold; for (k, w) in opts { if r < w { kind = k; break; } r -= w; } let (px, py) = self.map.portal; self.spawn(kind, false, px as f32 + 1.5, py as f32 + 1.5, 1); self.fx(3, px as f32 + 1.5, py as f32 + 1.5, 0.0, 0.0, 0.8, 0xffcc44ff); self.sfx(Sfx::PortalArrive, px as f32 + 1.5, py as f32 + 1.5); self.msg("msg.creature_arrived", vec![format!("@{}", st(kind).name)]); } fn do_payday(&mut self) { let mut paid = 0; let mut unpaid = 0; let mut total = 0.0; for i in 0..self.ents.len() { if !self.ents[i].alive || !self.ents[i].creature() { continue; } let e = &self.ents[i]; let w = st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32); if self.gold >= w { self.gold -= w; total += w; paid += 1; let e = &mut self.ents[i]; e.happy = (e.happy + 0.2).min(1.0); } else { unpaid += 1; self.ents[i].happy -= 0.45; } } if paid + unpaid > 0 { self.sfx_ui(Sfx::Payday); if unpaid > 0 { self.msg("msg.payday_partial", vec![paid.to_string(), (total as i32).to_string(), unpaid.to_string()]); } else { self.msg("msg.payday", vec![paid.to_string(), (total as i32).to_string()]); } } } // strength of a squad (same formula as the army value) fn squad_value(list: &[(EK, u32)]) -> f32 { let (mut h, mut d) = (0.0, 0.0); for &(k, l) in list { let s = st(k); h += s.hp * (1.0 + 0.15 * (l - 1) as f32); d += s.atk * (1.0 + 0.12 * (l - 1) as f32) / s.interval; } (h * d).sqrt() } // builds a hero squad with at least the target strength fn build_squad(&mut self, target: f32, base_level: u32, pool: &[(EK, u32)], boss: Option, min: usize) -> Vec<(EK, u32)> { let total: u32 = pool.iter().map(|p| p.1).sum(); let mut list = Vec::new(); for level in base_level.min(10)..=10 { list.clear(); if let Some(b) = boss { list.push((b, level)); } while list.len() < MAX_SQUAD && (list.len() < min || Self::squad_value(&list) < target) { let mut r = self.rng.next() % total; for &(k, w) in pool { if r < w { list.push((k, level)); break; } r -= w; } } if Self::squad_value(&list) >= target { break; } } list } fn spawn_squad(&mut self, list: &[(EK, u32)], guard: bool) { let (gx, gy) = self.map.gate; for &(k, l) in list { let x = gx as f32 + 0.5 + self.rng.f() * 2.0; let y = gy as f32 + 0.5 + self.rng.f() * 2.0; let id = self.spawn(k, true, x, y, l); if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) { e.guard = guard; } } self.fx(3, gx as f32 + 1.5, gy as f32 + 1.5, 0.0, 0.0, 1.0, 0xffffeecc); } // Endless waves: strength 60% -> 100% of the army value, with some randomness, at least 2 knights. fn spawn_wave(&mut self) { use EK::*; let w = self.wave + 1; let ratio = (0.6 + 0.05 * (w - 1) as f32).min(1.0) * (0.88 + self.rng.f() * 0.24); let min_value = Self::squad_value(&[(Knight, 1), (Knight, 1)]); let target = (self.army_value() * ratio).max(min_value); let mut pool = vec![(Knight, 4), (Archer, 3)]; if w >= 2 { pool.push((Dwarf, 2)); } if w >= 3 { pool.push((Priest, 2)); } let boss = if w % 5 == 0 { Some(Paladin) } else { None }; let base_level = 1 + (w - 1) / 3; let list = self.build_squad(target, base_level, &pool, boss, 2); self.last_wave_value = Self::squad_value(&list); self.spawn_squad(&list, false); self.sfx_ui(Sfx::WaveStart); if boss.is_some() { self.msg("msg.wave_boss", vec![w.to_string(), list.len().to_string()]); } else { self.msg("msg.wave", vec![w.to_string(), list.len().to_string()]); } } // defenders at 75 / 50 / 25 % gate HP fn check_gate_stage(&mut self) { use EK::*; let frac = self.gate_hp / GATE_HP; let want = if frac <= 0.25 { 3 } else if frac <= 0.5 { 2 } else if frac <= 0.75 { 1 } else { 0 }; while self.gate_stage < want { self.gate_stage += 1; let st_i = self.gate_stage; let ratio = [0.5, 0.65, 0.85][(st_i - 1) as usize] * (0.9 + self.rng.f() * 0.2); let min_value = Self::squad_value(&[(Knight, 2), (Knight, 2), (Archer, 2)]); let target = (self.army_value() * ratio).max(min_value); let pool = [(Knight, 4), (Archer, 3), (Priest, 2), (Dwarf, 1)]; let boss = if st_i == 3 { Some(Paladin) } else { None }; let base_level = 2 + self.wave / 3 + st_i; let list = self.build_squad(target, base_level, &pool, boss, 3); self.spawn_squad(&list, true); self.sfx_ui(Sfx::GateDefenders); if st_i == 3 { self.msg("msg.gate_boss", vec![(list.len() - 1).to_string()]); } else { self.msg("msg.gate_defenders", vec![list.len().to_string()]); } } } fn hit_gate(&mut self, e: &mut Ent) { let s = st(e.kind); e.cd = s.interval; let dmg = e.atk() * (0.85 + self.rng.f() * 0.3); self.gate_hp -= dmg; self.gate_hit_t = self.time; e.xp += dmg * 0.3; let (gx, gy) = self.gate_center(); if s.range > 1.5 { let col = if e.kind == EK::Mage { 0xffff44cc } else { 0xff70b0d0 }; self.fx(0, e.x, e.y - 0.3, gx, gy, 0.25, col); } else { let fx = (e.x + gx) * 0.5; let fy = (e.y + gy) * 0.5; self.fx(2, fx, fy, 0.0, 0.0, 0.25, 0xffffffff); } self.sfx(Sfx::GateHit, gx, gy); self.hint(10, "hint.gate_attack"); self.check_gate_stage(); } fn update_ent(&mut self, e: &mut Ent, dt: f32) { e.anim += dt; e.flash = (e.flash - dt).max(0.0); e.cd -= dt; e.think -= dt; e.repath -= dt; e.moving = false; e.working = false; let s = st(e.kind); while e.level < 10 && e.xp >= 100.0 * e.level as f32 { e.xp -= 100.0 * e.level as f32; e.level += 1; let old = e.max_hp; e.max_hp = s.hp * (1.0 + 0.15 * (e.level - 1) as f32); e.hp += e.max_hp - old; self.fx(7, e.x, e.y, 0.0, 0.0, 0.8, 0xff44ddff); if !e.hero { self.sfx(Sfx::LevelUp, e.x, e.y); } if !e.hero && e.level >= 3 { self.msg("msg.level_up", vec![format!("@{}", s.name), e.level.to_string()]); } } if e.hero { self.hero_ai(e, dt); } else if e.kind == EK::Worker { self.worker_ai(e, dt); } else { self.creature_ai(e, dt); } } // moves a unit along its path; true = still moving fn step_path(&mut self, e: &mut Ent, dt: f32) -> bool { if e.path.is_empty() { return false; } let next = e.path[0]; let tile = self.map.t[next]; if !tile.walkable() { if e.hero && tile.diggable() { // heroes dig their way through e.working = true; let (tx, ty) = xy(next); e.face = sgn(tx as f32 + 0.5 - e.x); e.timer += dt; if e.timer >= 0.5 { e.timer = 0.0; let pow = match e.kind { EK::Dwarf => 3.0, EK::Paladin => 2.0, _ => 1.0, }; self.fx(6, tx as f32 + 0.5, ty as f32 + 0.5, 0.0, 0.0, 0.4, 0xff3a5a7a); self.sfx(Sfx::Dig, tx as f32 + 0.5, ty as f32 + 0.5); let t = &mut self.map.t[next]; t.hp -= pow; if t.hp <= 0.0 { self.dig_out(next); } } return true; } e.path.clear(); return false; } let (tx, ty) = xy(next); let (cx, cy) = (tx as f32 + 0.5, ty as f32 + 0.5); 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; if !e.hero && e.creature() && e.hunger > 2.0 { sp *= 0.8; } if d <= sp { e.x = cx; e.y = cy; e.path.remove(0); } else { e.x += dx / d * sp; e.y += dy / d * sp; } if dx.abs() > 0.01 { e.face = sgn(dx); } e.moving = true; true } pub fn dig_out(&mut self, i: usize) { self.torches.retain(|t| t.wall != i); let t = &mut self.map.t[i]; t.kind = Kind::Floor; t.owner = Owner::None; t.marked = false; t.reserved = 0; t.hp = 0.0; t.gold = 0; let (x, y) = xy(i); self.fx(3, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.6, 0xff304a66); self.sfx(Sfx::DigDone, x as f32 + 0.5, y as f32 + 0.5); } fn unreserve(&mut self, me: u32) { for t in self.map.t.iter_mut() { if t.reserved == me { t.reserved = 0; } } } fn release_all(&mut self, id: u32) { let me = id + 1; for t in self.map.t.iter_mut() { if t.reserved == me { t.reserved = 0; } if t.bed == me { t.bed = 0; } } } fn los(&self, x0: f32, y0: f32, x1: f32, y1: f32) -> bool { let dx = x1 - x0; let dy = y1 - y0; let d = (dx * dx + dy * dy).sqrt(); let n = (d * 4.0) as i32 + 1; for k in 1..n { let f = k as f32 / n as f32; let x = (x0 + dx * f) as i32; let y = (y0 + dy * f) as i32; if let Some(t) = self.map.get(x, y) { if t.solid() || t.kind == Kind::Core { return false; } } } true } fn nearest_enemy(&self, e: &Ent, radius: f32) -> Option { let mut best = None; let mut bd = radius * radius; for o in &self.ents { if !o.alive || o.held || o.hero == e.hero { continue; } let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2); if d < bd { bd = d; best = Some(o.id); } } best } // fight a specific unit; false = target invalid/unreachable fn attack_job(&mut self, e: &mut Ent, id: u32, dt: f32) -> bool { let Some(j) = self.find(id) else { return false }; let (tx, ty) = (self.ents[j].x, self.ents[j].y); let d = ((tx - e.x).powi(2) + (ty - e.y).powi(2)).sqrt(); let s = st(e.kind); if d > 12.0 { return false; } if d <= s.range + 0.45 && (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; self.hit(e, j); } return true; } if e.path.is_empty() || e.repath <= 0.0 { e.repath = 0.6; let tt = idx(tx as i32, ty as i32); match self.map.path_to(e.tile(), tt) { Some(p) => e.path = p, None => return false, } } if !self.step_path(e, dt) && e.tile() == idx(tx as i32, ty as i32) { // same tile: approach directly let dx = tx - e.x; let dy = ty - e.y; let l = (dx * dx + dy * dy).sqrt().max(0.001); let sp = s.speed * dt; e.x += dx / l * sp.min(l); e.y += dy / l * sp.min(l); } true } fn hit(&mut self, e: &mut Ent, j: usize) { let dmg = e.atk() * (0.85 + self.rng.f() * 0.3); let (tx, ty) = (self.ents[j].x, self.ents[j].y); let s = st(e.kind); if s.range > 1.5 { let col = match e.kind { EK::Mage => 0xffff44cc, EK::Priest => 0xff88ffff, _ => 0xff70b0d0, }; self.fx(0, e.x, e.y - 0.3, tx, ty - 0.3, 0.25, col); self.sfx(Sfx::Shoot, e.x, e.y); } else { self.fx(2, tx, ty - 0.2, 0.0, 0.0, 0.25, 0xffffffff); self.sfx(Sfx::MeleeHit, tx, ty); } e.xp += dmg * 0.6; let my_id = e.id; let t = &mut self.ents[j]; t.hp -= dmg; t.flash = 0.15; if t.creature() && !matches!(t.job, Job::Attack(_)) { t.job = Job::Attack(my_id); t.path.clear(); } if t.hp <= 0.0 { self.kill(j); } } fn kill(&mut self, j: usize) { let e = &mut self.ents[j]; e.alive = false; let (id, kind, hero, x, y) = (e.id, e.kind, e.hero, e.x, e.y); self.release_all(id); self.fx(3, x, y, 0.0, 0.0, 0.7, 0xff505070); let s = st(kind); if hero { let b = s.bounty as f32; let got = self.add_gold(b); self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff); self.sfx(Sfx::HeroDeath, x, y); self.msg("msg.hero_slain", vec![format!("@{}", s.name), (got as i32).to_string()]); } else { self.sfx(Sfx::Death, x, y); self.msg("msg.creature_died", vec![format!("@{}", s.name)]); } } // ---------------------------------------------------------------- pit imps (workers) fn dig_neighbor(&self, i: usize, me: u32) -> Option { let (x, y) = xy(i); let mut found = None; for &(dx, dy) in &DIR4 { if let Some(t) = self.map.get(x + dx, y + dy) { if t.marked && t.diggable() { if t.reserved == me { return Some(idx(x + dx, y + dy)); } if t.reserved == 0 && found.is_none() { found = Some(idx(x + dx, y + dy)); } } } } found } fn claimable(&self, i: usize, me: u32) -> bool { let t = &self.map.t[i]; if t.owner != Owner::None || !matches!(t.kind, Kind::Floor | Kind::Portal) { return false; } if t.reserved != 0 && t.reserved != me { return false; } let (x, y) = xy(i); DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == Owner::Player)) } fn is_deposit(&self, i: usize) -> bool { let t = &self.map.t[i]; (t.kind == Kind::Treasury && t.owner == Owner::Player) || (self.counts[0] == 0 && self.map.adj_core(i)) } fn start_deliver(&mut self, e: &mut Ent) -> bool { let cur = e.tile(); if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.is_deposit(i), true) { e.path = p; e.job = Job::Deliver; return true; } false } fn wander(&mut self, e: &mut Ent, radius: i32) { let (x, y) = xy(e.tile()); for _ in 0..6 { let nx = x + self.rng.range(-radius, radius); let ny = y + self.rng.range(-radius, radius); if self.map.walk(nx, ny) && self.map.t[idx(nx, ny)].owner == Owner::Player { if let Some(p) = self.map.path_to(e.tile(), idx(nx, ny)) { if p.len() as i32 <= radius * 2 + 2 { e.path = p; break; } } } } e.job = Job::Wander; e.timer = 1.0 + self.rng.f() * 2.0; } fn worker_ai(&mut self, e: &mut Ent, dt: f32) { if self.step_path(e, dt) { return; } let me = e.id + 1; let cur = e.tile(); match e.job { Job::Dig(t) => { let tile = self.map.t[t]; if !tile.marked || !tile.diggable() || !adj4(cur, t) { self.unreserve(me); e.job = Job::None; return; } let (tx, ty) = xy(t); e.face = sgn(tx as f32 + 0.5 - e.x); e.working = true; e.timer += dt; if e.timer >= 0.45 { e.timer = 0.0; let fxx = (e.x + tx as f32 + 0.5) * 0.5; let fxy = (e.y + ty as f32 + 0.5) * 0.5; let tl = &mut self.map.t[t]; let done; if tl.kind == Kind::Gold { let take = tl.gold.min(25); tl.gold -= take; e.carry += take; done = tl.gold <= 0; self.fx(5, fxx, fxy, 0.0, 0.0, 0.5, 0xff30d0ff); self.sfx(Sfx::GoldDig, fxx, fxy); } else { tl.hp -= 1.0; done = tl.hp <= 0.0; self.fx(6, fxx, fxy, 0.0, 0.0, 0.4, 0xff3a5a7a); self.sfx(Sfx::Dig, fxx, fxy); } if done { self.dig_out(t); e.job = Job::None; } if e.carry >= CARRY_MAX { self.unreserve(me); e.job = Job::None; self.start_deliver(e); } } } Job::Claim(t) => { if cur != t || !self.claimable(t, me) { self.unreserve(me); e.job = Job::None; return; } e.working = true; e.timer += dt; if e.timer >= 1.0 { self.unreserve(me); e.job = Job::None; if self.map.t[t].kind == Kind::Portal { let (px, py) = self.map.portal; for y in 0..3 { for x in 0..3 { self.map.t[idx(px + x, py + y)].owner = Owner::Player; } } self.msg("msg.portal_claimed", vec![]); self.fx(7, px as f32 + 1.5, py as f32 + 1.5, 0.0, 0.0, 1.0, 0xffff66cc); } else { self.map.t[t].owner = Owner::Player; } self.place_torches(t); let (x, y) = xy(t); self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.5, 0xff4040e0); self.sfx(Sfx::Claim, x as f32 + 0.5, y as f32 + 0.5); } } Job::Deliver => { if self.is_deposit(cur) { let amount = e.carry as f32; let got = self.add_gold(amount); if got < amount { self.hint(3, "hint.treasury_full"); } if got > 0.0 { self.fx(5, e.x, e.y, 0.0, 0.0, 0.8, 0xff30d0ff); self.sfx(Sfx::Deposit, e.x, e.y); } e.carry = 0; e.job = Job::None; } else if !self.start_deliver(e) { e.job = Job::None; e.think = 1.0; } } Job::Wander => { e.timer -= dt; if e.timer <= 0.0 { e.job = Job::None; } } Job::None => { if e.think > 0.0 { return; } e.think = 0.3 + self.rng.f() * 0.2; e.timer = 0.0; if e.carry >= CARRY_MAX && self.start_deliver(e) { return; } if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.dig_neighbor(i, me).is_some(), true) { let end = p.last().copied().unwrap_or(cur); if let Some(t) = self.dig_neighbor(end, me) { self.map.t[t].reserved = me; e.path = p; e.job = Job::Dig(t); return; } } if e.carry > 0 && self.start_deliver(e) { return; } if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.claimable(i, me), true) { let end = p.last().copied().unwrap_or(cur); self.map.t[end].reserved = me; e.path = p; e.job = Job::Claim(end); return; } e.think = 1.0; self.wander(e, 3); } _ => e.job = Job::None, } } // ---------------------------------------------------------------- creatures fn find_room_tile(&self, cur: usize, kind: Kind, me: u32, need_food: bool) -> Option> { self.map.search( cur, self.map.walk_cost(), |i| { let t = &self.map.t[i]; t.kind == kind && t.owner == Owner::Player && (t.reserved == 0 || t.reserved == me) && (!need_food || t.food >= 1.0) }, true, ) } fn creature_ai(&mut self, e: &mut Ent, dt: f32) { let me = e.id + 1; e.hunger += dt / 75.0; if !matches!(e.job, Job::Sleep(_)) { e.energy -= dt / 110.0; } if e.hunger > 1.8 { e.happy -= dt * 0.004; } if e.think <= 0.0 { e.think = 0.4 + self.rng.f() * 0.3; if !matches!(e.job, Job::Attack(_) | Job::Leave) { let radius = if e.job == Job::Rally { 8.5 } else { 7.0 }; if let Some(id) = self.nearest_enemy(e, radius) { self.unreserve(me); e.job = Job::Attack(id); e.path.clear(); e.repath = 0.0; } } } if let Job::Attack(id) = e.job { if !self.attack_job(e, id, dt) { e.job = Job::None; e.path.clear(); } return; } if e.job == Job::Rally { self.rally(e, dt); return; } if self.step_path(e, dt) { return; } let cur = e.tile(); match e.job { Job::Sleep(_) => { e.energy += dt / 14.0; e.hp = (e.hp + e.max_hp * dt / 25.0).min(e.max_hp); if e.energy >= 1.0 { e.energy = 1.0; e.job = Job::None; } } Job::Eat(t) => { if cur != t { self.unreserve(me); e.job = Job::None; return; } e.working = true; e.timer += dt; if e.timer >= 2.0 { let tl = &mut self.map.t[t]; if tl.food >= 1.0 { tl.food -= 1.0; e.hunger = 0.0; e.hp = (e.hp + e.max_hp * 0.2).min(e.max_hp); } self.unreserve(me); e.job = Job::None; } } Job::Train(t) => { if cur != t || self.map.t[t].kind != Kind::Training || self.gold < 1.0 { self.unreserve(me); e.job = Job::None; return; } e.working = true; self.gold -= 1.5 * dt; e.xp += 7.0 * dt; e.timer -= dt; if e.timer <= 0.0 { self.unreserve(me); e.job = Job::None; } } Job::Study(t) => { if cur != t || self.map.t[t].kind != Kind::Library { self.unreserve(me); e.job = Job::None; return; } e.working = true; let rate = if e.kind == EK::Mage { 1.2 } else { 0.5 }; self.mana = (self.mana + rate * dt).min(MANA_MAX); e.xp += 3.0 * dt; e.timer -= dt; if e.timer <= 0.0 { self.unreserve(me); e.job = Job::None; } } Job::Wander => { e.timer -= dt; if e.timer <= 0.0 { e.job = Job::None; } } Job::Leave => { if self.map.t[cur].kind == Kind::Portal { e.alive = false; self.release_all(e.id); self.fx(3, e.x, e.y, 0.0, 0.0, 0.8, 0xffcc44ff); } else { self.start_leave(e); } } Job::None => self.creature_choose(e), _ => e.job = Job::None, } } fn start_leave(&mut self, e: &mut Ent) { let cur = e.tile(); e.job = Job::Leave; if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.map.t[i].kind == Kind::Portal, true) { e.path = p; } else { e.alive = false; self.release_all(e.id); self.fx(3, e.x, e.y, 0.0, 0.0, 0.8, 0xff505070); } } fn wants_rally(&self, e: &Ent) -> bool { self.flag.is_some() && e.hp > e.max_hp * 0.5 && e.energy > 0.15 && e.hunger < 1.8 && e.happy > 0.0 } // Attack flag: march to the flag; if the gate is nearby, attack it. fn rally(&mut self, e: &mut Ent, dt: f32) { let Some(f) = self.flag else { e.job = Job::None; e.path.clear(); return; }; if e.hp < e.max_hp * 0.3 || e.energy < 0.1 || e.hunger > 2.2 { // exhausted or badly hurt: retreat e.job = Job::None; e.path.clear(); return; } let (fx, fy) = xy(f); let (fx, fy) = (fx as f32 + 0.5, fy as f32 + 0.5); let (gx, gy) = self.gate_center(); let gate_near = self.gate_hp > 0.0 && (fx - gx).powi(2) + (fy - gy).powi(2) <= 8.0 * 8.0; let s = st(e.kind); if gate_near { let dg = ((e.x - gx).powi(2) + (e.y - gy).powi(2)).sqrt(); if dg <= s.range + 1.6 && (s.range < 1.5 || self.los(e.x, e.y, gx, gy)) { e.path.clear(); e.face = sgn(gx - e.x); e.working = true; if e.cd <= 0.0 { self.hit_gate(e); } return; } } let (target, tx, ty, near) = if gate_near { (idx(gx as i32, gy as i32), gx, gy, s.range + 1.2) } else { (f, fx, fy, 1.5) }; let d = ((e.x - tx).powi(2) + (e.y - ty).powi(2)).sqrt(); if d <= near && e.path.is_empty() { return; // wait at the flag } if e.path.is_empty() || e.repath <= 0.0 { e.repath = 1.5; match self.map.path_to(e.tile(), target) { Some(p) => e.path = p, None => { self.hint(11, "hint.flag_unreachable"); e.job = Job::Wander; e.timer = 3.0; return; } } } self.step_path(e, dt); } pub fn set_flag(&mut self, x: i32, y: i32) { if !inb(x, y) { return; } let i = idx(x, y); if self.flag == Some(i) { self.flag = None; self.msg("msg.flag_removed", vec![]); self.sfx_ui(Sfx::Click); for e in self.ents.iter_mut() { if e.alive && e.creature() && e.job == Job::Rally { e.job = Job::None; e.path.clear(); } } return; } if !self.map.t[i].walkable() { self.hint(12, "hint.flag_ground"); self.sfx_ui(Sfx::Error); return; } self.flag = Some(i); self.sfx(Sfx::FlagSet, x as f32 + 0.5, y as f32 + 0.5); self.msg("msg.flag_set", vec![]); let mut ids = Vec::new(); for e in self.ents.iter() { if e.alive && !e.held && e.creature() && matches!(e.job, Job::None | Job::Wander | Job::Train(_) | Job::Study(_) | Job::Rally) { ids.push(e.id); } } for id in ids { self.unreserve(id + 1); if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) { e.job = Job::None; e.path.clear(); } } } fn creature_choose(&mut self, e: &mut Ent) { let me = e.id + 1; let cur = e.tile(); let name = st(e.kind).name; if e.happy <= 0.0 { self.msg("msg.creature_leaves", vec![format!("@{}", name)]); self.start_leave(e); return; } if e.energy < 0.25 { if let Some(b) = e.bed { let t = &self.map.t[b]; if t.kind != Kind::Lair || t.bed != me { e.bed = None; } } if e.bed.is_none() { let found = self.map.search( cur, self.map.walk_cost(), |i| { let t = &self.map.t[i]; t.kind == Kind::Lair && t.bed == 0 }, true, ); if let Some(p) = found { let b = p.last().copied().unwrap_or(cur); self.map.t[b].bed = me; e.bed = Some(b); } } if let Some(b) = e.bed { if let Some(p) = self.map.path_to(cur, b) { e.path = p; e.job = Job::Sleep(b); return; } } e.happy -= 0.1; self.hint(4, "hint.no_beds"); e.job = Job::Sleep(cur); return; } if e.hunger > 1.0 && !(self.flag.is_some() && e.hunger < 1.5) { if let Some(p) = self.find_room_tile(cur, Kind::Farm, me, true) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; e.job = Job::Eat(t); e.timer = 0.0; return; } if e.hunger > 1.8 { self.hint(5, "hint.hungry"); } } if self.wants_rally(e) { e.job = Job::Rally; e.path.clear(); e.repath = 0.0; return; } if e.hp < e.max_hp * 0.5 && e.energy >= 0.25 && self.counts[1] > 0 { // hurt: rest in the lair e.energy = e.energy.min(0.2); self.creature_choose(e); return; } let r = self.rng.f(); if (e.kind == EK::Mage && r < 0.8) || r < 0.2 { if let Some(p) = self.find_room_tile(cur, Kind::Library, me, false) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; e.job = Job::Study(t); e.timer = 8.0 + self.rng.f() * 6.0; return; } } if r < 0.8 && self.gold >= 5.0 { if let Some(p) = self.find_room_tile(cur, Kind::Training, me, false) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; e.job = Job::Train(t); e.timer = 8.0 + self.rng.f() * 6.0; return; } } self.wander(e, 4); } // ---------------------------------------------------------------- heroes fn hero_ai(&mut self, e: &mut Ent, dt: f32) { if e.think <= 0.0 { e.think = 0.5; if e.kind == EK::Priest && e.cd <= 0.0 { let mut best: Option = None; let mut worst = 0.95; for (j, o) in self.ents.iter().enumerate() { if o.alive && o.hero { let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2); let r = o.hp / o.max_hp; if d < 16.0 && r < worst { worst = r; best = Some(j); } } } if e.hp / e.max_hp < worst { e.hp = (e.hp + 25.0).min(e.max_hp); self.fx(4, e.x, e.y, 0.0, 0.0, 0.8, 0xff60ff80); e.cd = 2.5; } else if let Some(j) = best { let o = &mut self.ents[j]; o.hp = (o.hp + 25.0).min(o.max_hp); let (ox, oy) = (o.x, o.y); self.fx(4, ox, oy, 0.0, 0.0, 0.8, 0xff60ff80); e.cd = 2.5; } } if !matches!(e.job, Job::Attack(_)) { let radius = if e.guard { 8.0 } else { 6.5 }; if let Some(id) = self.nearest_enemy(e, radius) { e.job = Job::Attack(id); e.path.clear(); e.repath = 0.0; } } } if let Job::Attack(id) = e.job { let (gx, gy) = self.gate_center(); let far = e.guard && (e.x - gx).powi(2) + (e.y - gy).powi(2) > 14.0 * 14.0; if !far && self.attack_job(e, id, dt) { return; } e.job = Job::AttackCore; e.path.clear(); e.think = 1.5; } if e.guard { // gate defenders stay at the gate let (gx, gy) = self.gate_center(); let d2 = (e.x - gx).powi(2) + (e.y - gy).powi(2); if d2 > 2.5 * 2.5 && e.path.is_empty() && e.repath <= 0.0 { e.repath = 1.0; let target = idx(gx as i32, gy as i32); if let Some(p) = self.map.path_to(e.tile(), target) { e.path = p; } } self.step_path(e, dt); return; } let cur = e.tile(); if e.path.is_empty() && self.map.adj_core(cur) { e.working = true; let (cx, cy) = self.map.core; e.face = sgn(cx as f32 + 1.5 - e.x); if e.cd <= 0.0 { e.cd = st(e.kind).interval; self.core_hp -= e.atk(); let fx = (e.x + cx as f32 + 1.5) * 0.5; let fy = (e.y + cy as f32 + 1.5) * 0.5; self.fx(2, fx, fy, 0.0, 0.0, 0.3, 0xff4040ff); self.sfx(Sfx::CoreHit, fx, fy); self.hint(6, "hint.core_attacked"); } return; } if e.path.is_empty() && e.repath <= 0.0 { e.repath = 1.0; if let Some(p) = self.map.hero_path_to_core(cur) { e.path = p; } } self.step_path(e, dt); } // ---------------------------------------------------------------- player actions pub fn mark_rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, on: bool) { for y in y0.min(y1)..=y0.max(y1) { for x in x0.min(x1)..=x0.max(x1) { if inb(x, y) { let t = &mut self.map.t[idx(x, y)]; if t.diggable() { t.marked = on; } } } } } pub fn build_rect(&mut self, room: usize, x0: i32, y0: i32, x1: i32, y1: i32) { let kind = ROOM_KINDS[room]; let cost = ROOM_COST[room] as f32; let mut built = 0; let mut broke = false; for y in y0.min(y1)..=y0.max(y1) { for x in x0.min(x1)..=x0.max(x1) { if !inb(x, y) { continue; } let i = idx(x, y); let t = self.map.t[i]; if t.kind == Kind::Floor && t.owner == Owner::Player { if self.gold < cost { broke = true; continue; } self.gold -= cost; let t = &mut self.map.t[i]; t.kind = kind; t.food = 0.0; t.bed = 0; built += 1; self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.5, 0xff40d0ff); if built == 1 { self.sfx(Sfx::Build, x as f32 + 0.5, y as f32 + 0.5); } } } } if broke { self.msg("msg.no_gold", vec![]); self.sfx_ui(Sfx::Error); } if built == 0 && !broke { self.hint(7, "hint.build_where"); } self.recount(); } pub fn sell_rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) { let mut refund = 0.0; for y in y0.min(y1)..=y0.max(y1) { for x in x0.min(x1)..=x0.max(x1) { if !inb(x, y) { continue; } let t = &mut self.map.t[idx(x, y)]; if t.owner == Owner::Player { if let Some(r) = room_index(t.kind) { t.kind = Kind::Floor; t.bed = 0; t.food = 0.0; refund += ROOM_COST[r] as f32 * 0.5; } } } } self.recount(); if refund > 0.0 { let got = self.add_gold(refund); self.sfx_ui(Sfx::Sell); self.msg("msg.sold", vec![(got as i32).to_string()]); } } pub fn pickup(&mut self, target: Option) -> bool { if self.hand.len() >= 8 { self.hint(8, "hint.hand_full"); return false; } if let Some(j) = target.filter(|&j| j < self.ents.len() && self.ents[j].alive && !self.ents[j].held && !self.ents[j].hero) { let id = self.ents[j].id; let me = id + 1; self.unreserve(me); let e = &mut self.ents[j]; e.held = true; e.job = Job::None; e.path.clear(); self.hand.push(id); self.sfx_ui(Sfx::Pickup); return true; } false } pub fn drop(&mut self, wx: f32, wy: f32) -> bool { let Some(&id) = self.hand.last() else { return false }; let (x, y) = (wx as i32, wy as i32); if !inb(x, y) { return false; } let t = self.map.t[idx(x, y)]; if !t.walkable() || t.owner != Owner::Player { self.hint(9, "hint.drop_where"); return false; } self.hand.pop(); if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) { e.held = false; e.x = wx; e.y = wy; e.job = Job::None; e.think = 0.0; e.path.clear(); } self.fx(3, wx, wy, 0.0, 0.0, 0.4, 0xff707090); self.sfx(Sfx::Drop, wx, wy); true } pub fn lightning(&mut self, wx: f32, wy: f32) { const COST: f32 = 50.0; if self.mana < COST { self.msg("msg.no_mana_bolt", vec![]); self.sfx_ui(Sfx::Error); return; } self.mana -= COST; self.sfx(Sfx::Lightning, wx, wy); self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa); let mut kills = Vec::new(); for (j, e) in self.ents.iter_mut().enumerate() { if e.alive && !e.held && e.hero && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 2.6 { e.hp -= 55.0; e.flash = 0.2; if e.hp <= 0.0 { kills.push(j); } } } for j in kills { self.kill(j); } } pub fn heal(&mut self, wx: f32, wy: f32) { const COST: f32 = 60.0; if self.mana < COST { self.msg("msg.no_mana_heal", vec![]); self.sfx_ui(Sfx::Error); return; } self.mana -= COST; self.sfx(Sfx::Heal, wx, wy); let mut fxs = Vec::new(); for e in self.ents.iter_mut() { if e.alive && !e.held && !e.hero && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 6.5 { e.hp = (e.hp + e.max_hp * 0.5).min(e.max_hp); fxs.push((e.x, e.y)); } } self.fx(7, wx, wy, 0.0, 0.0, 0.6, 0xff60ff80); for (x, y) in fxs { self.fx(4, x, y, 0.0, 0.0, 0.9, 0xff60ff80); } } pub fn summon(&mut self, wx: f32, wy: f32) { let cost = self.summon_cost(); let (x, y) = (wx as i32, wy as i32); if !self.map.walk(x, y) || self.map.t[idx(x, y)].owner != Owner::Player { self.hint(13, "hint.summon_where"); return; } if self.mana < cost { self.msg("msg.no_mana_cost", vec![(cost as i32).to_string()]); self.sfx_ui(Sfx::Error); return; } self.mana -= cost; self.sfx(Sfx::Summon, wx, wy); self.spawn(EK::Worker, false, wx, wy, 1); self.fx(7, wx, wy, 0.0, 0.0, 0.6, 0xffff66cc); } } // Language key describing what a unit is doing pub fn job_name(e: &Ent) -> &'static str { match e.job { Job::None => "job.none", Job::Dig(_) => "job.dig", Job::Claim(_) => "job.claim", Job::Deliver => "job.deliver", Job::Wander => "job.wander", Job::Sleep(_) => { if e.moving { "job.sleep_go" } else { "job.sleep" } } Job::Eat(_) => "job.eat", Job::Train(_) => "job.train", Job::Study(_) => "job.study", Job::Attack(_) => "job.fight", Job::AttackCore => { if e.working { "job.core_attack" } else { "job.core_march" } } Job::Leave => "job.leave", Job::Rally => { if e.working { "job.gate_attack" } else { "job.rally" } } } } #[cfg(test)] mod tests { use super::*; // Simulates games with a simple bot player and checks that nothing crashes. #[test] fn simulate_long_game() { for seed in [1u32, 7, 12345] { let mut g = Game::new(seed); let (cx, cy) = g.map.core; let (px, py) = g.map.portal; // dig a way to the portal (through earth/gold, around rock) let start = idx(cx - 1, cy + 1); let goal = idx(px + 1, py + 1); let path = g .map .search(start, |i| if g.map.t[i].kind == Kind::Rock { None } else { Some(1) }, |i| i == goal, false) .expect("Portal erreichbar"); for i in path { if g.map.t[i].diggable() { g.map.t[i].marked = true; } } g.mark_rect(cx - 6, cy - 6, cx + 9, cy + 9, true); let (ox, oy) = (cx - 3, cy - 3); g.build_rect(1, ox, oy, ox + 2, oy + 1); // lair 3x2 g.build_rect(2, ox + 5, oy, ox + 7, oy + 1); // farm 3x2 let dt = 0.05; let mut built = false; let mut portal_logged = false; for step in 0..(40 * 60 * 20) { g.update(dt); g.events.clear(); g.msgs.clear(); if step % 200 == 0 && !built && g.gold > 1900.0 { g.build_rect(0, ox, oy + 6, ox + 1, oy + 8); // treasury g.build_rect(3, ox + 6, oy + 6, ox + 8, oy + 8); // training hall g.build_rect(1, ox, oy + 2, ox + 2, oy + 2); // enlarge the lair built = true; } // the bot uses lightning against heroes at the core if step % 20 == 0 && g.mana >= 50.0 { let target = g.ents.iter().find(|e| e.alive && e.hero && (e.x - cx as f32).abs() < 4.0 && (e.y - cy as f32).abs() < 4.0).map(|e| (e.x, e.y)); if let Some((x, y)) = target { g.lightning(x, y); } } if step == 20 * 60 * 20 { let (gx, gy) = g.map.gate; g.set_flag(gx + 1, gy + 1); } if step % 2400 == 0 { println!( "seed {} t={:>5.0}s gold={:>5.0} creatures={} workers={} wave={} core={:.0} gate={:.0} heroes={} army={:.0}", seed, g.time, g.gold, g.creature_count(), g.worker_count(), g.wave, g.core_hp, g.gate_hp, g.ents.iter().filter(|e| e.alive && e.hero).count(), g.army_value() ); } if g.portal_owned() && step % 20 == 0 && !portal_logged { portal_logged = true; println!("seed {} portal claimed after {:.0}s", seed, g.time); } if g.state != 0 { println!("seed {} game over after {:.0}s, state={}", seed, g.time, g.state); break; } } } } }