// Game logic: units, AI, economy, hero waves. use crate::commands::Command; use crate::map::*; use serde::{Deserialize, Serialize}; pub const CORE_HP: f32 = 2000.0; pub const GATE_HP: f32 = 3000.0; pub const PAYDAY: f32 = 150.0; pub const PAYDAY_WARN: f32 = 20.0; // the payday is announced this many seconds before pub const TREASURY_TILE: f32 = 500.0; // value one treasury tile holds pub const CORE_STORE: f32 = 2000.0; // value the soul core holds without any treasury pub const TRAIN_COST: f32 = 1.5; pub const HELPERS_MAX: usize = 4; // up to five imps at one task (one worker + four helpers) // gold per second of training (billed on payday) 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, Reinforce, Cannon, Magic, Capture, Convert, Slap, Punch, // Crookfinger's voice: squeaky syllables Voice1, Voice2, Voice3, Voice4, Voice5, Voice6, // weather, rogues and groves Thunder, Steal, Snare, Sermon, Rain, Wind, Spores, } pub const VOICE_SYLLABLES: [Sfx; 6] = [Sfx::Voice1, Sfx::Voice2, Sfx::Voice3, Sfx::Voice4, Sfx::Voice5, Sfx::Voice6]; pub const SFX_ALL: [Sfx; 49] = [ 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, Sfx::Reinforce, Sfx::Cannon, Sfx::Magic, Sfx::Capture, Sfx::Convert, Sfx::Slap, Sfx::Punch, Sfx::Voice1, Sfx::Voice2, Sfx::Voice3, Sfx::Voice4, Sfx::Voice5, Sfx::Voice6, Sfx::Thunder, Sfx::Steal, Sfx::Snare, Sfx::Sermon, Sfx::Rain, Sfx::Wind, Sfx::Spores, ]; 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 MAX_CREATURES: usize = 100; // creatures of one master (the beds decide below that) pub const SPRING_MANA: f32 = 0.6; // mana per second of each claimed mana spring pub const SPRING_MAX: f32 = 150.0; // more mana maximum per claimed spring pub const CARRY_MAX: i32 = 300; #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)] pub enum EK { Worker, Kobold, Spider, Mage, Ogre, Knight, Archer, Priest, Dwarf, Paladin, Jailer, // attracted by the torture chamber Priestess, // attracted by the temple Chainbreaker, // rare and very strong // flying creatures, attracted by the aerie Bat, // fast and weak Gargoyle, // aerie + torture chamber: stone, tough Owl, // aerie + library: arcane bolts from afar Wyrm, // big aerie + treasury: rare, very strong, fire breath Cogling, // forge: a sooty little tinkerer, builds traps and doors fastest Revenant, // rises from the graveyard: undead, needs no food and no bed // more heroes Giant, // slow, huge, smashes doors and traps Fairy, // flies, finds the dungeon's traps and heals heroes a little BattleMage, // blasts reinforced walls from afar, throws fireballs // siege engines (heroes bring them; a master can build his own, see siege.rs) Ram, // smashes doors, walls and towers, ignores creatures Catapult, // hurls stones at towers from afar // wild beasts of the caverns (beasts.rs): neutral until tamed CaveBear, // strong and shaggy, likes the mushroom garden Beetle, // a giant armoured beetle, likes the lair Trader, // a merchant visiting through the portal (traders.rs) // heroes of the later waves (rogues.rs) Thief, // fast, sneaks to a treasury, steals gold and runs off with it Monk, // fists and faith: spells do not touch him Ranger, // a bow from afar, lays snares in the dungeon's corridors Inquisitor, // fragile; now and then turns one of the weakest creatures to the heroes' side } 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 const RUNE_TOOL: usize = 20; // index of the rune forge tower in ui::TOOLS pub const SUMMON_TOOL: usize = 11; // index of summoning in ui::TOOLS 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), // heroes: the wage applies when they defected to the player EK::Knight => s("unit.knight", 140.0, 11.0, 1.0, 1.9, 1.1, 70, 120), EK::Archer => s("unit.archer", 75.0, 9.0, 4.5, 2.1, 1.3, 60, 100), EK::Priest => s("unit.priest", 80.0, 5.0, 3.5, 1.9, 1.5, 60, 120), EK::Dwarf => s("unit.dwarf", 110.0, 9.0, 1.0, 2.0, 1.0, 60, 100), EK::Paladin => s("unit.paladin", 700.0, 26.0, 1.0, 1.7, 1.2, 220, 1000), EK::Jailer => s("unit.jailer", 260.0, 16.0, 1.0, 1.6, 1.3, 140, 0), EK::Priestess => s("unit.priestess", 70.0, 14.0, 5.0, 2.0, 1.6, 120, 0), EK::Chainbreaker => s("unit.chainbreaker", 600.0, 36.0, 1.1, 2.3, 1.1, 400, 0), EK::Bat => s("unit.bat", 55.0, 6.0, 1.0, 3.6, 0.7, 30, 0), EK::Gargoyle => s("unit.gargoyle", 260.0, 15.0, 1.0, 2.2, 1.2, 120, 0), EK::Owl => s("unit.owl", 70.0, 12.0, 4.5, 2.8, 1.3, 100, 0), EK::Wyrm => s("unit.wyrm", 650.0, 30.0, 3.0, 2.5, 1.4, 380, 0), EK::Cogling => s("unit.cogling", 90.0, 9.0, 1.0, 2.4, 0.9, 70, 0), EK::Revenant => s("unit.revenant", 160.0, 14.0, 1.0, 1.9, 1.2, 30, 0), EK::Giant => s("unit.giant", 900.0, 34.0, 1.1, 1.3, 1.6, 200, 600), EK::Fairy => s("unit.fairy", 70.0, 5.0, 3.5, 3.2, 1.2, 60, 150), EK::BattleMage => s("unit.battlemage", 120.0, 18.0, 5.0, 1.8, 1.8, 120, 250), EK::Ram => s("unit.ram", 900.0, 45.0, 1.1, 1.0, 1.8, 0, 200), EK::Catapult => s("unit.catapult", 320.0, 40.0, 7.5, 0.9, 3.5, 0, 200), EK::CaveBear => s("unit.cavebear", 420.0, 24.0, 1.0, 1.9, 1.4, 110, 0), EK::Beetle => s("unit.beetle", 300.0, 13.0, 1.0, 1.7, 1.1, 70, 0), EK::Trader => s("unit.trader", 200.0, 1.0, 1.0, 1.5, 2.0, 0, 0), EK::Thief => s("unit.thief", 60.0, 6.0, 1.0, 3.1, 0.9, 50, 120), EK::Monk => s("unit.monk", 150.0, 12.0, 1.0, 2.3, 0.7, 80, 180), EK::Ranger => s("unit.ranger", 90.0, 11.0, 5.0, 2.2, 1.4, 80, 160), EK::Inquisitor => s("unit.inquisitor", 70.0, 7.0, 3.5, 1.8, 1.6, 90, 250), } } // flying creatures cross water and lava and sleep in the aerie pub fn flies(k: EK) -> bool { matches!(k, EK::Bat | EK::Gargoyle | EK::Owl | EK::Wyrm | EK::Fairy) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum Job { None, Dig(usize), Claim(usize), Deliver, Wander, Sleep(usize), Eat(usize), Train(usize), Study(usize), Attack(u32), AttackCore, Leave, Rally, Reinforce(usize), // wall tile Repair(u32), // tower id AttackTower(u32), // tower id Carry(u32), // imp carries an unconscious hero (id) to the torture chamber Pray(usize), // temple tile HealCore, // flame priestess heals the soul core TearBridge(usize), // knight tears down a bridge of the player Rest(usize), // flying creature recovers in the aerie (flees when badly hurt) Torment(u32), // the jailer works on a prisoner in the torture chamber Register, // new creature: reports at the portal MakeBed(usize), // new creature: sets up its bed FetchWage(usize), // payday: walks to the treasury (or the core) for the wage Help(usize), // imp with nothing else to do helps another imp at this tile Forge(usize), // creature works in the forge (traps and doors) Install(u32), // imp carries a finished trap or door (id) to its spot and installs it Brawl(u32), // two creatures that cannot stand each other scuffle (never to the death) Guard(usize), // holds a guard post Spar(usize), // sparring in the arena MendBridge(usize), // imp mends a worn bridge tile RaiseWall(usize), // imp raises a planned wall on own floor March(usize), // creature follows an attack marker (sword) at this tile } #[derive(Clone, Serialize, Deserialize)] 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, #[serde(default)] pub carry_res: u8, // treasure the imp carries (see map::RES_GOLD) 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, #[serde(default)] pub dir: f32, // facing direction on the map (radians, 0 = +x), for the eight sprite views pub moving: bool, pub working: bool, #[serde(default)] pub slow: f32, // seconds of slowdown left (magic tower) #[serde(default)] pub captive: u8, // heroes: 0 free, 1 unconscious, 2 carried, 3 in the torture chamber #[serde(default)] pub carrier: u32, // imp that has taken care of this captive #[serde(default)] pub torture_t: f32, #[serde(default)] pub skill_cd: f32, // special abilities (jailer pull, priestess heal) #[serde(default)] pub waypoint: Option<(i32, i32)>, // heroes: entry point into the player's realm #[serde(default)] pub boost: f32, // seconds of "slapped" speed-up left #[serde(default)] pub stun: f32, // seconds of stun left (punched) #[serde(default)] pub haste: f32, // spell: seconds of haste left #[serde(default)] pub shield: f32, // spell: seconds of stoneskin left (half damage) #[serde(default)] pub invis: f32, // spell: seconds of invisibility left (until it attacks) #[serde(default)] pub plague: f32, // spell: seconds of plague left (poison that spreads) #[serde(default)] pub name: u32, // seed of the creature's name (names.rs), 0: none (heroes, siege engines) #[serde(default)] pub kills: u32, // heroes this creature has slain (names.rs: the hall of the fallen) #[serde(default)] pub bane: u8, // what hurt it last: sprite index + 1 of a unit kind, or names::BANE_* #[serde(default)] pub ab_cd: f32, // cooldown of the learned ability #[serde(default)] pub mood_t: f32, // seconds until the next mood check (likes and hatreds) pub guard: bool, // stays at its home (gate defenders, treasure guards) #[serde(default)] pub home: Option<(f32, f32)>, // guard post #[serde(default)] pub patrol: Vec<(i32, i32)>, // fortress patrol: route of tiles, walked in a loop #[serde(default)] pub patrol_k: usize, // next point of the route #[serde(default = "yes")] pub settled: bool, // creatures: reported at the portal and set up their bed #[serde(default)] pub pay_due: f32, // wage of the last payday not fetched yet #[serde(default)] pub pay_t: f32, // seconds until the creature goes for its wage #[serde(default)] pub side: u8, // 0 player, 2..=4 the other masters, 6 dwarves (heroes: see side()) #[serde(default)] pub target: Option, // attackers: side whose core (or gate) they march on #[serde(default)] pub chained: bool, // a lone creature chained in a sealed cave: joins when the cave is claimed #[serde(default)] pub boss: u8, // a champion of the Order leading a wave (see bosses.rs), 0: none #[serde(default)] pub gear: u8, // equipment from the forge: 0 none, 1 iron, 2 steel, 3 rune (armory.rs) #[serde(skip, default = "zero_f32")] pub disp_x: f32, // smoothed screen position (not part of the saved or synced world state) #[serde(skip, default = "zero_f32")] pub disp_y: f32, } // units of the neutral dwarves count for nobody fn e_is_dwarf_side(side: u8) -> bool { side == crate::dwarves::DWARF_SIDE } fn yes() -> bool { true } fn zero_f32() -> f32 { 0.0 } 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, carry_res: 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, dir: std::f32::consts::FRAC_PI_2, moving: false, working: false, slow: 0.0, captive: 0, carrier: 0, torture_t: 0.0, skill_cd: 0.0, waypoint: None, boost: 0.0, stun: 0.0, haste: 0.0, shield: 0.0, invis: 0.0, plague: 0.0, name: 0, kills: 0, bane: 0, ab_cd: 0.0, mood_t: 0.0, guard: false, home: None, patrol: Vec::new(), patrol_k: 0, settled: true, pay_due: 0.0, pay_t: 0.0, side: 0, target: None, chained: false, boss: 0, gear: 0, disp_x: 0.0, disp_y: 0.0, } } 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) * crate::armory::gear_factor(self.gear) } // the player's creatures (not imps, heroes or the rivals' units) pub fn creature(&self) -> bool { !self.hero && self.side == me_side() && self.kind != EK::Worker && self.kind != EK::Trader && !self.chained } // 0 player, 1 humans, 2 and 3 rival masters pub fn side(&self) -> u8 { if self.hero { 1 } else { self.side } } // a unit of a rival master pub fn rival(&self) -> bool { !self.hero && (2..=4).contains(&self.side) } // a dwarf of a dwarf cave (neutral, never an opponent for victory) pub fn dwarf(&self) -> bool { !self.hero && self.side == crate::dwarves::DWARF_SIDE } // a hero that is still fighting (not a prisoner) pub fn active_hero(&self) -> bool { self.hero && self.captive == 0 } } // Goals of a campaign mission (all must be reached) #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] pub enum Goal { Shards, // take back all shards of the soul core Heart, // destroy the gate of the order KillAll, // defeat every knight (prisoners count as defeated) Loot(u32), // take treasure worth this much from knight vaults Towers, // raze all knight towers Survive(u32), // beat this many waves Rivals, // destroy the soul cores of all rival masters Springs(u32), // claim this many mana springs Freed(u32), // free this many chained creatures } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct MissionState { pub index: usize, // number of the mission in the campaign pub goals: Vec, pub tools: Vec, // allowed tools (ui::TOOLS order) pub waves: bool, pub wave_limit: u32, // 0 = endless pub gate_defenders: bool, pub heart: bool, // the map has a gate of the order pub time_limit: f32, // seconds, 0 = none pub shards: u32, // shards on the map } #[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 h: f32, // height of the start point (3D) pub h2: f32, // height of the end point (projectiles) pub size: f32, // 1 = normal; attacks of higher levels look bigger pub style: u8, // projectiles: arrow, cannonball, fireball, ... (see particles.rs) } thread_local! { // the master whose turn it is in the simulation (see Game::switch_to) static ME_SIDE: std::cell::Cell = std::cell::Cell::new(0); } pub fn me_side() -> u8 { ME_SIDE.with(|s| s.get()) } fn player_owner() -> Owner { Owner::Player } // The state of a dungeon master that is not being processed right now. The Game's own fields // (gold, mana, hand, ...) always belong to the master whose side is `me_side`; switching swaps // them with the stored ones. A single player game has no stored masters at all. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(default)] pub struct MasterState { pub side: u8, pub owner: Option, pub core: (i32, i32), pub core_hp: f32, pub gold: f32, pub wood: f32, pub mana: f32, pub payday: f32, pub payday_warned: bool, pub hand: Vec, pub flag: Option, pub reserve: f32, pub incoming: Vec<(u8, f32, Option)>, pub training_bill: f32, pub counts: [usize; N_ROOMS], pub room_level: Vec, pub gear_stock: Vec, pub gear_work: f32, pub guard_marks: Vec, pub attack_marks: Vec, pub springs: usize, pub known: Vec, pub researchable: Vec, pub research: f32, pub forge_work: f32, pub rune_free: u32, pub rune_final: u32, pub rune_next: u32, pub portal_timer: f32, pub punch_ready: f32, #[serde(default)] pub talents: crate::talents::Talents, } #[derive(Serialize, Deserialize)] 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, #[serde(skip)] pub fx: Vec, #[serde(skip)] pub msgs: Vec, pub state: u8, // 0 running, 1 victory, 2 defeat pub paused: bool, pub hand: Vec, #[serde(skip)] pub counts: [usize; N_ROOMS], // room tiles per room kind (ROOM_KINDS order, recounted) #[serde(default)] pub gear_stock: Vec, // finished equipment waiting for a creature, per tier (armory.rs) #[serde(default)] pub gear_work: f32, // forge work on the next piece #[serde(default)] pub room_level: Vec, // level of every room kind (ROOM_KINDS order; 1 when missing), researched pub gate_hp: f32, pub gate_hit_t: f32, pub gate_stage: u32, // defender thresholds triggered (0..3) pub flag: Option, #[serde(skip)] pub events: Vec, pub last_wave_value: f32, pub torches: Vec, #[serde(default)] pub towers: Vec, #[serde(default)] pub punch_ready: f32, // game time when the hand can punch again #[serde(default)] pub entry_history: Vec<(i32, i32)>, // entry points of recent waves #[serde(default)] pub planned_entries: Vec<(i32, i32)>, // entry points of the coming/current wave (minimap) #[serde(default)] pub reserve: f32, // value stored in the soul core (the rest lies in the treasury tiles) #[serde(skip)] pub incoming: Vec<(u8, f32, Option)>, // treasures added since the last treasury update (kind, value, delivered at) #[serde(default)] pub training_bill: f32, // training costs, paid on the next payday #[serde(default)] pub payday_warned: bool, #[serde(default)] pub loot: f32, // value taken from knight vaults #[serde(default)] pub shards_taken: u32, #[serde(default)] pub towers_razed: u32, // knight towers destroyed #[serde(skip)] pub contested: Vec, #[serde(default)] pub mission: Option, // campaign mission (None: free game) #[serde(default)] pub script: Option, // block scripts of an editor mission #[serde(default)] pub objective: Option, // the objective a mission script set #[serde(default)] pub rivals: Vec, // AI dungeon masters #[serde(default = "yes")] pub gate_on: bool, // free game: the humans' gate exists #[serde(skip)] pub springs: usize, // claimed mana springs (recounted) #[serde(skip)] pub parts: Vec, // visual particles (never saved) #[serde(skip)] pub stuck: Vec, // arrows stuck in walls and floors, scorch marks #[serde(skip, default = "crate::particles::vrng")] pub vrng: Rng, // random numbers for the particles only #[serde(default)] pub traps: Vec, // traps and doors (ordered, in the forge, carried, armed) #[serde(default)] pub rollers: Vec, #[serde(default)] pub clouds: Vec, #[serde(default)] pub known: Vec, // tools known (ui::TOOLS order; empty = old savegame: everything) #[serde(default)] pub researchable: Vec, // tools that can be researched in the library #[serde(default)] pub research: f32, // research points towards the current item #[serde(default)] pub reveals: Vec<(f32, f32, f32)>, // seer's eye: x, y, seconds left #[serde(default)] pub graves: Vec, #[serde(default)] pub altar_last: Option, // the last sacrifice that waits for its pair #[serde(skip)] pub fog_t: f32, // fog of war: seconds until the next update #[serde(skip)] pub forge_work: f32, // work done in the forge this frame #[serde(skip)] pub demo: Option, // the endless bridge battle behind the title menu #[serde(default)] pub dwarf_caves: Vec, #[serde(default)] pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges #[serde(default)] pub weather: crate::weather::Weather, // above ground (weather.rs) #[serde(default)] pub memorials: Vec, // stones for fallen creatures (names.rs) #[serde(default)] pub hall: Vec, // the local master's fallen of this game (names.rs) #[serde(default)] pub trading: crate::traders::Trading, // the visiting trader (traders.rs) #[serde(default)] pub happenings: Option, // random events (free games and versus), see events.rs #[serde(default)] pub snares: Vec, // tiles with a ranger's snare (rogues.rs) #[serde(default)] pub talents: crate::talents::Talents, // of the master being processed (talents.rs) #[serde(default)] pub guard_marks: Vec, // guard markers (shields) of the master being processed (orders.rs) #[serde(default)] pub attack_marks: Vec, // attack markers (swords) #[serde(default)] pub marks_t: f32, #[serde(default)] pub stats: crate::stats::GameStats, // statistics of the master at this computer (see stats.rs) #[serde(default)] pub wall_plans: Vec<(usize, u8)>, // walls to raise: tile, master side (see walls.rs) #[serde(default)] pub wood: f32, // the second resource (see wood.rs) #[serde(default)] pub woodwork: crate::wood::WoodWork, // felled mushrooms growing back #[serde(default)] pub dwarf_t: f32, #[serde(default)] pub rune_free: u32, // rune forge towers that can be built for free (from chests) #[serde(default)] pub rune_final: u32, // the same, but given back for a sold tower: no second refund #[serde(default)] pub rune_next: u32, // sold rune forge towers: back as a chest in the next mission #[serde(default)] pub freed: u32, // chained creatures freed // tiles next to fighting heroes: knight ground there cannot be claimed #[serde(skip, default = "player_owner")] pub me: Owner, // owner of the ground of the master being processed #[serde(skip)] pub me_side: u8, // that master's side (0 = the first player) #[serde(skip)] pub local_side: u8, // the master who plays at this computer #[serde(default)] pub masters: Vec, // the other masters (multiplayer, AI masters), saved with the game #[serde(skip)] pub pending: Vec, // orders of this master, applied at the next step boundary #[serde(skip)] pub cmd_seq: u32, // number of the next command queued here #[serde(skip)] pub sim_steps: u64, // fixed simulation steps done so far (the lockstep clock) #[serde(skip)] step_acc: f32, // real time not yet paid for with a step #[serde(skip)] pub netplay: Option, // a match: turns come in, the world steps per turn #[serde(skip)] pub ai_sides: Vec, // match sides whose human master left: AI keeps them playing #[serde(skip)] pub ai_difficulty: u8, // difficulty used when a left side becomes an AI rival #[serde(skip)] pub coop: bool, // co-op match: the playable master sides are on one side against the heroes #[serde(skip)] pub teams: Vec<(u8, u8)>, // (side, team) of a versus match; a side without an entry is its own team #[serde(skip)] pub out_sides: Vec, // masters of a match whose soul core fell (their creatures fight on) #[serde(skip)] pub marks: Vec, // short-lived markers for commands given by the masters #[serde(skip)] pub hints: bool, // show hint messages (setting) #[serde(skip)] pub debug_markers: bool, // show debug-only command markers #[serde(skip, default = "hint_times")] hint_t: [f32; 48], // last time each hint slot was shown } pub const MARK_DIG: u8 = 0; pub const MARK_ROOM: u8 = 1; pub const MARK_SELL: u8 = 2; pub const MARK_BRIDGE: u8 = 3; pub const MARK_STEEL: u8 = 4; pub const MARK_TOWER: u8 = 5; pub const MARK_TRAP: u8 = 6; pub const MARK_SPELL: u8 = 7; pub const MARK_LIGHTNING: u8 = 8; pub const MARK_HEAL: u8 = 9; pub const MARK_SUMMON: u8 = 10; pub const MARK_FLAG: u8 = 11; pub const MARK_PICKUP: u8 = 12; pub const MARK_DROP: u8 = 13; pub const MARK_SLAP: u8 = 14; pub const MARK_ATTACK: u8 = 15; pub const MARK_GUARD: u8 = 16; pub const MARK_CANCEL: u8 = 17; #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct Mark { pub x: f32, pub y: f32, pub kind: u8, pub side: u8, pub until: u64, #[serde(default)] pub fixed: bool, // guard/attack/gather order flags are shown without the debug setting } fn hint_times() -> [f32; 48] { [-1000.0; 48] } // every simulation step has the same length: on every client of a match the same commands are // applied and then the same number of steps runs, so the worlds stay identical pub const STEP: f32 = 1.0 / 50.0; // 50 steps a second // one frame never runs away into the future (a lagging tab catches up at most this much) const MAX_STEPS_PER_FRAME: usize = 12; // torch on a wall face: wall tile + direction (index into DIR4) towards the open side; // or a torch on a little stand in the middle of a floor tile (stand: wall = that tile) #[derive(Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Torch { pub wall: usize, pub dir: usize, #[serde(default)] pub stand: bool, } // rooms where imps may set up a torch stand (no beds, hoards, mushrooms or crates in the way) pub fn stand_ground(k: Kind) -> bool { matches!(k, Kind::Floor | Kind::Training | Kind::Library | Kind::Torture | Kind::Temple | Kind::Prison | Kind::Graveyard | Kind::GuardPost | Kind::Arena | Kind::Altar) } // a torch stand goes where no torch shines closer than this (tiles) pub const STAND_GAP: f32 = 4.5; 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); if self.stand { return (x as f32 + 0.5, y as f32 + 0.5); } 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) } // height of the bottom of the flame pub fn flame_h(&self) -> f32 { if self.stand { 0.62 } else { 0.72 } } } pub const N_ROOMS: usize = 14; pub const ROOM_FORGE: usize = 8; pub const ROOM_KINDS: [Kind; N_ROOMS] = [ Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Torture, Kind::Temple, Kind::Aerie, Kind::Forge, Kind::Prison, Kind::Graveyard, Kind::GuardPost, Kind::Arena, Kind::Altar, ]; pub const ROOM_COST: [i32; N_ROOMS] = [60, 80, 100, 140, 160, 200, 180, 120, 150, 160, 140, 90, 180, 250]; pub const ROOM_NAMES: [&str; N_ROOMS] = [ "room.treasury", "room.lair", "room.farm", "room.training", "room.library", "room.torture", "room.temple", "room.aerie", "room.forge", "room.prison", "room.graveyard", "room.guardpost", "room.arena", "room.altar", ]; // bridge cost per tile: over water, over lava pub const BRIDGE_COST: [i32; 2] = [10, 75]; // a drawbridge tile: over water (and wood, see wood.rs), over lava pub const DRAWBRIDGE_COST: [i32; 2] = [120, 150]; // steel wall cost per tile: from earth, from a reinforced wall pub const STEEL_COST: [i32; 2] = [80, 40]; pub const STEEL_HP: f32 = 40.0; pub fn room_index(k: Kind) -> Option { ROOM_KINDS.iter().position(|&r| r == k) } impl Game { // the researched level of a room kind of the master being processed (1, 2 or 3) pub fn room_level_of(&self, k: Kind) -> u8 { room_index(k).and_then(|r| self.room_level.get(r).copied()).unwrap_or(1).max(1) } // how much better a room works at its level: +25% per level above the first pub fn room_bonus(&self, k: Kind) -> f32 { 1.0 + 0.25 * (self.room_level_of(k) - 1) as f32 } } pub(crate) 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 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) } } pub(crate) fn sgn(v: f32) -> f32 { if v < 0.0 { -1.0 } else { 1.0 } } impl Game { // free game with the humans' gate only (tests; the menu uses new_free) #[cfg_attr(not(test), allow(dead_code))] pub fn new(seed: u32, w: usize, h: usize) -> Game { let mut g = Game::from_map(Map::generate(seed, w, h), seed); // heroes take longer to reach the core on big maps: longer grace period let (cx, cy) = g.map.core; let (gx, gy) = g.map.gate; let dist = ((gx - cx).abs() + (gy - cy).abs()) as f32; g.wave_timer = FIRST_WAVE + ((dist - 45.0) * 1.5).clamp(0.0, 300.0); g.guard_treasures(); g.place_dwarf_caves(seed); g.add_wilds(seed, true); g.init_research(None); g.init_fog(); g.msg("msg.welcome1", vec![]); g.msg("msg.welcome2", vec![]); g.msg("msg.welcome3", vec![]); g.msg("msg.welcome4", vec![]); g } // treasure caves are guarded by heroes pub fn guard_treasures(&mut self) { for k in 0..self.map.treasures.len() { let (tx, ty) = self.map.treasures[k]; let squad = [EK::Knight, EK::Archer, EK::Dwarf, if k % 2 == 0 { EK::Priest } else { EK::Knight }]; for (j, &kind) in squad.iter().enumerate() { let x = tx as f32 + 0.5 + (j as f32 - 1.5) * 0.6; let id = self.spawn(kind, true, x, ty as f32 + 0.5, 2); if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) { e.guard = true; e.home = Some((tx as f32 + 0.5, ty as f32 + 0.5)); } } } } // game state around a generated map: treasury ring around the core, four imps, torches pub fn from_map(map: Map, seed: u32) -> Game { 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; N_ROOMS], room_level: Vec::new(), gear_stock: Vec::new(), gear_work: 0.0, 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(), towers: Vec::new(), punch_ready: 0.0, entry_history: Vec::new(), planned_entries: Vec::new(), hints: true, debug_markers: false, me: Owner::Player, me_side: 0, local_side: 0, masters: Vec::new(), pending: Vec::new(), cmd_seq: 0, sim_steps: 0, step_acc: 0.0, netplay: None, ai_sides: Vec::new(), ai_difficulty: 1, coop: false, teams: Vec::new(), out_sides: Vec::new(), marks: Vec::new(), hint_t: hint_times(), reserve: 0.0, incoming: Vec::new(), training_bill: 0.0, payday_warned: false, loot: 0.0, shards_taken: 0, towers_razed: 0, contested: Vec::new(), mission: None, script: None, objective: None, rivals: Vec::new(), gate_on: true, springs: 0, freed: 0, demo: None, traps: Vec::new(), rollers: Vec::new(), clouds: Vec::new(), forge_work: 0.0, fog_t: 0.0, graves: Vec::new(), altar_last: None, reveals: Vec::new(), known: Vec::new(), researchable: Vec::new(), research: 0.0, parts: Vec::new(), stuck: Vec::new(), vrng: crate::particles::vrng(), dwarf_caves: Vec::new(), bridgework: Default::default(), weather: Default::default(), memorials: Vec::new(), hall: Vec::new(), trading: Default::default(), happenings: None, snares: Vec::new(), talents: Default::default(), guard_marks: Vec::new(), attack_marks: Vec::new(), marks_t: 0.0, stats: Default::default(), wall_plans: Vec::new(), wood: crate::wood::WOOD_START, woodwork: Default::default(), dwarf_t: 30.0, rune_free: 0, rune_final: 0, rune_next: 0, }; let (cx, cy) = g.map.core; // the soul core starts surrounded by a one tile wide ring of treasury for y in cy - 1..=cy + 3 { for x in cx - 1..=cx + 3 { let t = &mut g.map.t[idx(x, y)]; if t.kind == Kind::Floor { t.kind = Kind::Treasury; t.owner = Owner::Player; } } } 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(); g.place_all_torches(); g } pub fn place_all_torches(&mut self) { // torches on walls that are gone (rivers, fortresses) are removed let map = &self.map; self.torches.retain(|t| if t.stand { stand_ground(map.t[t.wall].kind) } else { map.t[t.wall].solid() }); for i in 0..n() { if self.map.t[i].owner == self.me { self.place_torches(i); } } } // ---------------------------------------------------------------- campaign missions // the rune forge tower only shows up in the build menu once there is one to build or upgrade pub fn tool_visible(&self, tool: usize) -> bool { tool != RUNE_TOOL || self.rune_available() > 0 || self.towers.iter().any(|t| !t.hero && t.kind == TowerKind::Rune) } pub fn tool_allowed(&self, tool: usize) -> bool { if !self.known.is_empty() { return self.known(tool); } self.mission.as_ref().map_or(true, |m| m.tools.get(tool).copied().unwrap_or(true)) } pub fn goal_done(&self, goal: Goal) -> bool { match goal { Goal::Shards => self.shards_taken >= self.mission.as_ref().map_or(0, |m| m.shards), Goal::Heart => self.gate_hp <= 0.0, Goal::KillAll => !self.ents.iter().any(|e| e.alive && e.active_hero()), Goal::Loot(n) => self.loot >= n as f32, Goal::Towers => !self.towers.iter().any(|t| t.hero), Goal::Survive(n) => self.wave >= n && !self.ents.iter().any(|e| e.alive && e.active_hero() && !e.guard && e.patrol.is_empty()), Goal::Rivals => self.rivals.iter().all(|r| !r.alive), Goal::Springs(n) => self.springs >= n as usize, Goal::Freed(n) => self.freed >= n, } } pub fn waves_on(&self) -> bool { self.mission.as_ref().map_or(self.gate_on, |m| m.waves && (m.wave_limit == 0 || self.wave < m.wave_limit)) } pub fn has_gate(&self) -> bool { self.mission.as_ref().map_or(self.gate_on, |m| m.heart) } // messages, hints and sounds are meant for the master who plays at this computer: while // another master is being simulated (his economy, his orders) nothing of that reaches the screen fn speaking(&self) -> bool { self.me_side == self.local_side } pub fn msg(&mut self, key: &'static str, args: Vec) { if !self.speaking() { return; // a foreign master ordered something: only he hears about it } 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.speaking() { return; } if self.events.len() < 64 { self.events.push(SfxEvent { sfx, pos: Some((x, y)) }); } } pub fn sfx_ui(&mut self, sfx: Sfx) { if !self.speaking() { return; } 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; } } // towers count with half their combat value for t in self.towers.iter().filter(|t| !t.hero) { h += 0.5 * t.max_hp(); d += 0.5 * t.dmg() / tower_stats(t.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, stand: false }; 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); } } // a big room the wall torches do not light: a torch on a stand in the middle of it, // on the master's own ground only (never neutral or foreign ground) and not next to a wall let tile = self.map.t[i]; let open_around = (-1..=1).all(|dy| (-1..=1).all(|dx| self.map.get(x + dx, y + dy).map_or(false, |t| !t.solid()))); if tile.owner == self.me && stand_ground(tile.kind) && open_around { let (px, pz) = (x as f32 + 0.5, y as f32 + 0.5); let near = self.torches.iter().any(|o| { let (ox, oz) = o.pos(); (ox - px).powi(2) + (oz - pz).powi(2) < STAND_GAP * STAND_GAP }); if !near { self.torches.push(Torch { wall: i, dir: 0, stand: true }); } } } // torch stands go when their ground is lost or becomes a room they do not fit in fn tidy_torch_stands(&mut self) { let map = &self.map; self.torches.retain(|t| !t.stand || (stand_ground(map.t[t.wall].kind) && !matches!(map.t[t.wall].owner, Owner::None | Owner::Hero))); } pub(crate) fn hint(&mut self, slot: usize, key: &'static str) { if !self.speaking() { return; // another master ordered something: he keeps his hints for himself } if self.hints && 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; N_ROOMS]; self.springs = self.map.t.iter().filter(|t| t.kind == Kind::ManaSpring && t.owner == self.me).count(); for t in &self.map.t { if t.owner == self.me { if let Some(r) = room_index(t.kind) { c[r] += 1; } } } self.counts = c; } pub fn mana_max(&self) -> f32 { MANA_MAX + SPRING_MAX * self.springs as f32 } pub fn gold_cap(&self) -> f32 { CORE_STORE + TREASURY_TILE * self.counts[0] as f32 * self.room_bonus(Kind::Treasury) } pub fn add_gold(&mut self, amount: f32) -> f32 { self.add_res(amount, RES_GOLD) } // adds treasure of one kind (as far as there is room); returns the value accepted pub fn add_res(&mut self, amount: f32, res: u8) -> f32 { self.add_res_at(amount, res, None) } // treasure delivered at a tile: it fills the treasury tiles nearest to that place pub fn add_res_at(&mut self, amount: f32, res: u8, at: Option) -> f32 { let cap = self.gold_cap(); let acc = amount.min((cap - self.gold).max(0.0)); self.gold += acc; if acc > 0.0 { self.incoming.push((res, acc, at)); self.count(crate::stats::GOLD, acc); } acc } // what the next payday costs: wages of all creatures plus the training bill pub fn next_payday_cost(&self) -> f32 { let wages: f32 = self.ents.iter().filter(|e| e.alive && e.creature()).map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum::() * self.talents.wages(); wages + self.training_bill } // The gold counter is the truth; the treasury tiles show where it lies. Added treasure fills // tiles of the same kind first, then empty ones, the rest goes into the core. Spending takes // from random treasury tiles first, then from the core. pub fn sync_treasury(&mut self) { let mut tiles = Vec::new(); let mut stored = 0.0; for i in 0..self.map.t.len() { let t = &mut self.map.t[i]; if t.kind == Kind::Treasury && t.owner == self.me { tiles.push(i); stored += t.gold as f32; } else if t.gold != 0 && t.vein().is_none() && t.kind != Kind::Treasury { t.gold = 0; // a sold treasury tile loses its content t.res = 0; } } let diff = self.gold - (self.reserve + stored); let incoming = std::mem::take(&mut self.incoming); if diff < -0.5 { let mut need = -diff; while need > 0.5 { let full: Vec = tiles.iter().copied().filter(|&i| self.map.t[i].gold > 0).collect(); if full.is_empty() { break; } let i = full[(self.rng.next() as usize) % full.len()]; let t = &mut self.map.t[i]; let take = (t.gold as f32).min(need.ceil()); t.gold -= take as i32; need -= take; } self.reserve = (self.reserve - need.max(0.0)).max(0.0); } else if diff > 0.5 { let mut left = diff; let mut parts: Vec<(u8, f32, Option)> = incoming; let typed: f32 = parts.iter().map(|p| p.1).sum(); if typed < left { parts.push((RES_GOLD, left - typed, None)); } for (res, amount, at) in parts { let mut a = amount.min(left); left -= a; // delivered treasure goes to the treasury tiles nearest to the place of delivery let mut order = tiles.clone(); if let Some(at) = at { let (ax, ay) = xy(at); order.sort_by_key(|&i| { let (x, y) = xy(i); (x - ax).pow(2) + (y - ay).pow(2) }); } // without a place: tiles holding this treasure first, then empty tiles; // with a place: strictly the nearest tiles that can take it let passes: &[u8] = if at.is_some() { &[2] } else { &[0, 1] }; for &pass in passes { for &i in &order { if a <= 0.5 { break; } let t = &mut self.map.t[i]; let fits = match pass { 0 => t.gold > 0 && t.res == res, 1 => t.gold <= 0, _ => (t.gold > 0 && t.res == res && (t.gold as f32) < TREASURY_TILE) || t.gold <= 0, }; if !fits { continue; } let room = TREASURY_TILE - t.gold as f32; let put = a.min(room).floor(); if put >= 1.0 { t.gold += put as i32; t.res = res; a -= put; } } } self.reserve += a.max(0.0); } } } pub fn portals_owned(&self) -> usize { (0..self.map.portals.len()).filter(|&k| self.map.portal_owner(k) == self.me).count() } // gate defenders alive (not the treasure guards) pub fn gate_defenders_alive(&self) -> bool { let (gx, gy) = self.gate_center(); self.ents.iter().any(|e| e.alive && e.guard && e.captive == 0 && e.home.map_or(false, |(x, y)| (x - gx).abs() < 1.0 && (y - gy).abs() < 1.0)) } #[allow(dead_code)] pub fn portal_owned(&self) -> bool { let Some(&(px, py)) = self.map.portals.first() else { return false }; self.map.t[idx(px, py)].owner == self.me } 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 && e.side == self.me_side && !e.chained).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.side = self.me_side; e.x = x; e.y = y; e.disp_x = x; e.disp_y = y; e.level = level; if !hero && !matches!(kind, EK::Ram | EK::Catapult | EK::Trader) { e.name = crate::names::seed_for(id); } 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) { let h = if kind == 0 { 0.45 } else { 0.4 }; self.fx_particles(kind, x, y, h, col); if self.fx.len() < 400 { self.fx.push(Fx { kind, x, y, x2, y2, t: 0.0, max, col, h, h2: 0.45, size: 1.0, style: 0 }); } } // projectile from (x, y) at height h to (x2, y2) at height h2 #[allow(clippy::too_many_arguments)] pub fn shot(&mut self, x: f32, y: f32, h: f32, x2: f32, y2: f32, h2: f32, max: f32, col: u32) { if self.fx.len() < 400 { self.fx.push(Fx { kind: 0, x, y, x2, y2, t: 0.0, max, col, h, h2, size: 1.0, style: 0 }); } } // the last effect gets bigger (attacks of experienced units) fn grow_last_fx(&mut self, size: f32) { if let Some(f) = self.fx.last_mut() { f.size = size; } } // effect at a given height pub fn fx_at(&mut self, kind: u8, x: f32, y: f32, h: f32, max: f32, col: u32) { self.fx_particles(kind, x, y, h, col); if self.fx.len() < 400 { self.fx.push(Fx { kind, x, y, x2: 0.0, y2: 0.0, t: 0.0, max, col, h, h2: h, size: 1.0, style: 0 }); } } pub fn find(&self, id: u32) -> Option { self.ents.iter().position(|e| e.id == id && e.alive && !e.held) } // ---------------------------------------------------------------- update // smoothed positions for the screen only: the simulation stays fixed-step and deterministic pub fn smooth_display(&mut self, dt: f32) { if dt <= 0.0 { return; } let k = (1.0 - (-22.0 * dt).exp()).clamp(0.0, 1.0); for e in self.ents.iter_mut() { if e.disp_x == 0.0 && e.disp_y == 0.0 { e.disp_x = e.x; e.disp_y = e.y; continue; } let dx = e.x - e.disp_x; let dy = e.y - e.disp_y; if dx * dx + dy * dy > 4.0 { e.disp_x = e.x; e.disp_y = e.y; } else { e.disp_x += dx * k; e.disp_y += dy * k; } } } pub fn update(&mut self, dt: f32) { let mut steps = 0usize; let cmds = if self.netplay.is_some() { // a match runs turn by turn (see netplay.rs): without the next turn the world stands still, // and local orders are not applied here - they travel to the server first Vec::new() } else { self.step_acc = (self.step_acc + dt).min(STEP * MAX_STEPS_PER_FRAME as f32); while self.step_acc >= STEP { self.step_acc -= STEP; steps += 1; } std::mem::take(&mut self.pending) }; self.apply_turn(&cmds, steps); if !self.marks.is_empty() { self.marks.retain(|m| self.sim_steps < m.until); } // what only the camera cares about runs in real time: it is not part of the shared state self.update_particles(dt); for f in self.fx.iter_mut() { f.t += dt; } self.fx.retain(|f| f.t < f.max); } // one fixed step: the world of every master, then the shared bookkeeping pub(crate) fn step_fixed(&mut self) { self.sim_steps += 1; if self.state == 0 && !self.paused { self.step(STEP); } for side in self.master_sides() { self.switch_to(side); self.sync_treasury(); } // mission scripts: always from the first player's side, the same on every computer if self.script.is_some() { self.switch_to(0); self.run_scripts(); } let local = self.local_side; self.switch_to(local); self.sample_stats(STEP); } // ---------------------------------------------------------------- masters pub fn master_sides(&self) -> Vec { let mut v: Vec = std::iter::once(self.me_side).chain(self.masters.iter().map(|m| m.side)).collect(); v.sort_unstable(); v } pub fn is_master(&self, side: u8) -> bool { side == self.me_side || self.masters.iter().any(|m| m.side == side) } pub(crate) fn is_hostile(&self, e: &Ent) -> bool { e.alive && !e.held && e.captive == 0 && !e.chained && e.side() != self.me_side && !self.allied(self.me_side, e.side()) } pub fn is_rival_side(&self, side: u8) -> bool { self.rivals.iter().any(|r| r.alive && r.side == side) } fn take_active(&mut self) -> MasterState { MasterState { side: self.me_side, owner: Some(self.me), core: self.map.core, core_hp: self.core_hp, gold: self.gold, wood: self.wood, mana: self.mana, payday: self.payday, payday_warned: self.payday_warned, hand: std::mem::take(&mut self.hand), flag: self.flag, reserve: self.reserve, incoming: std::mem::take(&mut self.incoming), training_bill: self.training_bill, counts: self.counts, room_level: self.room_level.clone(), gear_stock: self.gear_stock.clone(), gear_work: self.gear_work, guard_marks: std::mem::take(&mut self.guard_marks), attack_marks: std::mem::take(&mut self.attack_marks), springs: self.springs, known: std::mem::take(&mut self.known), researchable: std::mem::take(&mut self.researchable), research: self.research, forge_work: self.forge_work, rune_free: self.rune_free, rune_final: self.rune_final, rune_next: self.rune_next, portal_timer: self.portal_timer, punch_ready: self.punch_ready, talents: self.talents.clone(), } } fn put_active(&mut self, m: MasterState) { self.me_side = m.side; self.me = m.owner.unwrap_or(Owner::of_side(m.side)); ME_SIDE.with(|s| s.set(m.side)); self.map.core = m.core; self.core_hp = m.core_hp; self.gold = m.gold; self.wood = m.wood; self.mana = m.mana; self.payday = m.payday; self.payday_warned = m.payday_warned; self.hand = m.hand; self.flag = m.flag; self.reserve = m.reserve; self.incoming = m.incoming; self.training_bill = m.training_bill; self.counts = m.counts; self.room_level = m.room_level; self.gear_stock = m.gear_stock; self.gear_work = m.gear_work; self.guard_marks = m.guard_marks; self.attack_marks = m.attack_marks; self.springs = m.springs; self.known = m.known; self.researchable = m.researchable; self.research = m.research; self.forge_work = m.forge_work; self.rune_free = m.rune_free; self.rune_final = m.rune_final; self.rune_next = m.rune_next; self.portal_timer = m.portal_timer; self.punch_ready = m.punch_ready; self.talents = m.talents; } // make `side` the master being processed (no-op for sides that are no masters) pub fn switch_to(&mut self, side: u8) { if side == self.me_side { return; } let Some(k) = self.masters.iter().position(|m| m.side == side) else { return }; let target = self.masters.swap_remove(k); let cur = self.take_active(); self.masters.push(cur); self.put_active(target); } // a new master on `side` with his core at (cx, cy) (top-left of the 3x3 core); single player // games have no stored masters #[allow(dead_code)] // used by the tests; the *Multiplayer* menu (step D) will call it pub fn add_master(&mut self, side: u8, core: (i32, i32)) { let mut m = self.take_active(); let me = std::mem::replace(&mut m, MasterState::default()); // the copy keeps the economy settings; the new one starts fresh let fresh = MasterState { side, owner: Some(Owner::of_side(side)), core, core_hp: CORE_HP, gold: 1500.0, mana: 100.0, payday: PAYDAY, known: me.known.clone(), researchable: me.researchable.clone(), portal_timer: 10.0, ..MasterState::default() }; self.put_active(me); self.masters.push(fresh); } // A match: `sides` (out of 0, 2, 3, 4 - side 1 is the heroes') play against each other and // `local` is the master who plays at this computer. Side 0 keeps the core of the generated map // (that is where the hero gate path leads); the other masters get their cores at fixed places // worked out from the map size alone, so every client of a match puts them in the same spots. #[allow(dead_code)] // used by the tests; the *Multiplayer* menu (step D) will call it pub fn start_masters(&mut self, sides: &[u8], local: u8) -> bool { let sides: Vec = sides.iter().copied().filter(|s| Owner::MASTER_SIDES.contains(s)).collect(); if sides.is_empty() || !sides.contains(&0) || !sides.contains(&local) { return false; } if sides.len() < 2 && !self.coop { return false; } let (w, h) = (mw() as i32, mh() as i32); if w < 72 || h < 72 { return false; // the master areas need room } let cores: Vec<(i32, i32)> = sides.iter().map(|&s| match s { 0 => self.map.core, 2 => (w - 30, 12), 3 => (w - 30, h - 16), _ => (14, h - 16) }).collect(); for (k, side) in sides.iter().enumerate() { if *side == self.me_side { continue; } self.carve_master_area(*side, cores[k]); // every master gets a portal of his own (side 0 has the map's portal) self.add_master_portal(*side, cores[k]); self.add_master(*side, cores[k]); self.switch_to(*side); let (cx, cy) = cores[k]; // a new master needs the same starting crew side 0 already has for j in 0..4 { self.spawn(EK::Worker, false, cx as f32 - 0.5, cy as f32 + j as f32 * 0.7 + 0.3, 1); } } self.local_side = local; self.switch_to(local); true } // a master's own portal right beside his starting area (if there is room) pub(crate) fn add_master_portal(&mut self, side: u8, core: (i32, i32)) { let owner = Owner::of_side(side); let (px, py) = (core.0 + 6, core.1); if px + 3 >= mw() as i32 - 2 || py < 2 || py + 3 >= mh() as i32 - 2 { return; } for y in py..py + 3 { for x in px..px + 3 { let t = &mut self.map.t[idx(x, y)]; if !matches!(t.kind, Kind::Core | Kind::Gate) { t.kind = Kind::Portal; t.owner = owner; t.gold = 0; t.marked = false; t.reinforced = false; } } } if !self.map.portals.contains(&(px, py)) { self.map.portals.push((px, py)); } } // the starting area of a master: a 9x9 room of his own floor and his 3x3 soul core pub(crate) fn carve_master_area(&mut self, side: u8, core: (i32, i32)) { let owner = Owner::of_side(side); for y in core.1 - 3..core.1 + 6 { for x in core.0 - 3..core.0 + 6 { if x < 2 || y < 2 || x >= mw() as i32 - 2 || y >= mh() as i32 - 2 { continue; } let t = &mut self.map.t[idx(x, y)]; if matches!(t.kind, Kind::Portal | Kind::Gate | Kind::Core) { continue; } t.kind = Kind::Floor; t.owner = owner; t.gold = 0; t.marked = false; t.reinforced = false; } } for y in 0..3 { for x in 0..3 { let t = &mut self.map.t[idx(core.0 + x, core.1 + y)]; t.kind = Kind::Core; t.owner = owner; } } } pub(crate) fn update_units(&mut self, dt: f32) { for i in 0..self.ents.len() { if !self.ents[i].alive || self.ents[i].held { continue; } let side = self.ents[i].side; if !self.ents[i].hero && side != self.me_side && !self.masters.is_empty() { self.switch_to(side); } let mut e = std::mem::replace(&mut self.ents[i], Ent::dummy()); let (x0, y0) = (e.x, e.y); self.update_ent(&mut e, dt); self.turn(&mut e, x0, y0); self.ents[i] = e; } // units that wasted away (plague) for j in 0..self.ents.len() { if self.ents[j].alive && !self.ents[j].held && self.ents[j].hp <= 0.0 { self.kill(j); } } self.ents.retain(|e| e.alive); } fn step(&mut self, dt: f32) { if self.demo.is_some() { self.demo_step(dt); return; } self.time += dt; // mushroom gardens grow faster at a higher level (of the master who owns them) let farm = Kind::Farm; let lv: Vec<(Owner, f32)> = self.master_sides().into_iter().map(|s| (Owner::of_side(s), { let r = room_index(farm).unwrap_or(0); let l = if s == self.me_side { self.room_level.get(r).copied() } else { self.masters.iter().find(|m| m.side == s).and_then(|m| m.room_level.get(r).copied()) }; 1.0 + 0.25 * (l.unwrap_or(1).max(1) - 1) as f32 })).collect(); for t in self.map.t.iter_mut() { if t.kind == Kind::Farm { let f = lv.iter().find(|x| x.0 == t.owner).map_or(1.0, |x| x.1); t.food = (t.food + dt * f / 18.0).min(3.0); } } for side in self.master_sides() { // a master whose heart fell has no household any more: no mana, no new creatures, no payday if self.is_out(side) { continue; } self.switch_to(side); self.economy(dt); } self.after_economy(dt); } // a master's household: mana, core, portals, payday fn economy(&mut self, dt: f32) { self.recount(); self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * self.talents.mana_rate() * dt).min(self.mana_max()); self.core_hp = (self.core_hp + 0.5 * dt).min(CORE_HP); let owned = self.portals_owned(); if owned > 0 { self.portal_timer -= dt; if self.portal_timer <= 0.0 { self.portal_timer = (20.0 + self.rng.f() * 14.0) / (owned as f32).sqrt() * self.lair_factor(); self.try_portal(); } } self.payday -= dt; if !self.payday_warned && self.payday <= PAYDAY_WARN { self.payday_warned = true; let cost = self.next_payday_cost(); if cost >= 1.0 { self.msg("msg.payday_soon", vec![(self.payday.ceil() as i32).to_string(), (cost.ceil() as i32).to_string()]); self.sfx_ui(Sfx::FlagSet); if cost > self.gold { self.hint(22, "hint.payday_short"); } } } if self.payday <= 0.0 { self.payday = PAYDAY; self.payday_warned = false; self.do_payday(); } // every two seconds: corners between reinforced walls are closed if (self.time / 2.0).floor() != ((self.time - dt) / 2.0).floor() { self.reinforce_corners(); self.tidy_torch_stands(); self.tidy_memorials(); } self.update_forge(dt); self.update_research(); self.update_marks(dt); } fn after_economy(&mut self, dt: f32) { // the shared world (waves, units, towers, rivals ...) runs from the first master's side: the // same on every computer of a match, whichever master plays at this one. Single-player // games only have that one side. let local = self.local_side; let first = self.master_sides().first().copied().unwrap_or(local); self.switch_to(first); if self.waves_on() { self.wave_tick(dt); } if self.time - self.gate_hit_t > 10.0 && self.has_gate() { self.gate_hp = (self.gate_hp + 8.0 * dt).min(GATE_HP); } self.update_contested(); self.update_units(dt); self.switch_to(first); // update_units worked master by master self.update_snares(); self.update_towers(dt); self.update_reveals(dt); self.update_graves(dt); if !self.rivals.is_empty() { self.update_rivals(dt); } self.update_dwarves(dt); self.update_weather(dt); self.update_bridges(dt); self.update_wood(dt); self.update_events(dt); self.update_traders(dt); // fallen hearts of a match are shared state: every client marks them in the same step self.update_fallen_masters(); // the end of the game and the fog are about the master at this computer self.switch_to(local); if self.netplay.is_some() { self.check_match_end(); } else { self.check_end(); } self.update_fog(dt); } fn check_end(&mut self) { if self.core_hp <= 0.0 { self.core_hp = 0.0; self.state = 2; self.msg("msg.defeat", vec![]); self.sfx_ui(Sfx::Defeat); return; } if let Some(m) = &self.mission { if m.time_limit > 0.0 && self.time > m.time_limit { self.state = 2; self.msg("msg.time_up", vec![]); self.sfx_ui(Sfx::Defeat); return; } let goals = m.goals.clone(); if !goals.is_empty() && goals.iter().all(|&g| self.goal_done(g)) { self.state = 1; self.msg("msg.mission_won", vec![]); self.sfx_ui(Sfx::Victory); } return; } // free game: every opponent (rival masters and the humans' gate) must fall let rivals_out = self.rivals.iter().all(|r| !r.alive); let gate_out = !self.gate_on || self.gate_hp <= 0.0; if rivals_out && gate_out { self.gate_hp = self.gate_hp.max(0.0); self.state = 1; self.msg("msg.victory", vec![]); self.sfx_ui(Sfx::Victory); } } fn wave_tick(&mut self, dt: f32) { self.wave_timer -= dt; if !self.warned && self.wave_timer < 30.0 { self.warned = true; let goal = self.wave_goal(); if goal != self.me_side { // the wave will march on another master: no entry markers in this dungeon self.planned_entries.clear(); self.announce_wave("msg.wave_warn", "msg.wave_warn_on_master", vec![(self.wave + 1).to_string()], goal, Sfx::WaveAlarm); return; } self.plan_entries(); if self.planned_entries.is_empty() { self.msg("msg.wave_warn", vec![(self.wave + 1).to_string()]); } else { let dirs: Vec = self.planned_entries.iter().map(|&p| format!("@{}", self.direction_key(p))).collect(); if dirs.len() >= 2 { self.msg("msg.wave_warn_two", vec![(self.wave + 1).to_string(), dirs[0].clone(), dirs[1].clone()]); } else { self.msg("msg.wave_warn_dir", vec![(self.wave + 1).to_string(), dirs[0].clone()]); } } self.sfx_ui(Sfx::WaveAlarm); } // markers disappear when no hero is on the way to an entry point any more if !self.planned_entries.is_empty() && self.wave_timer > 30.0 && !self.ents.iter().any(|e| e.alive && e.waypoint.is_some()) { self.planned_entries.clear(); } 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; } } // the imp working on tile t (dig, claim or reinforce) fn task_worker(&self, t: usize) -> Option { self.ents.iter().position(|o| o.alive && !o.hero && o.kind == EK::Worker && matches!(o.job, Job::Dig(x) | Job::Claim(x) | Job::Reinforce(x) if x == t)) } // imps helping at tile t (standing next to it) fn helpers(&self, t: usize) -> usize { let (tx, ty) = xy(t); self.ents .iter() .filter(|o| o.alive && o.kind == EK::Worker && o.job == Job::Help(t) && o.path.is_empty() && (o.x - tx as f32 - 0.5).abs() < 1.6 && (o.y - ty as f32 - 0.5).abs() < 1.6) .count() } // working speed at a tile: +25% per helper, at most 200% (five imps) pub(crate) fn help_speed(&self, t: usize) -> f32 { 1.0 + 0.25 * self.helpers(t).min(HELPERS_MAX) as f32 } // an idle imp walks to the nearest task with room for another helper fn start_help(&mut self, e: &mut Ent) -> bool { let tasks: Vec = self .ents .iter() .filter(|o| o.alive && !o.hero && o.kind == EK::Worker) .filter_map(|o| match o.job { Job::Dig(t) | Job::Claim(t) | Job::Reinforce(t) => Some(t), _ => None, }) .filter(|&t| self.ents.iter().filter(|o| o.alive && o.job == Job::Help(t)).count() < HELPERS_MAX) .collect(); if tasks.is_empty() { return false; } let cur = e.tile(); let near = |i: usize| tasks.iter().copied().find(|&t| i != t && adj4(i, t) || (i == t && self.map.t[t].walkable())); let Some(p) = self.map.search(cur, self.map.creature_cost(flies(e.kind)), |i| near(i).is_some(), true) else { return false }; let end = p.last().copied().unwrap_or(cur); let Some(t) = near(end) else { return false }; e.path = p; e.job = Job::Help(t); e.think = 1.5; true } // free beds in the lairs: much room lets creatures arrive faster, little room slower pub fn free_beds(&self) -> usize { let (lair, aerie) = self.free_lair_aerie(); lair + aerie } // free places in the lairs (walking creatures) and in the aerie (flying creatures) pub fn free_lair_aerie(&self) -> (usize, usize) { let flyers = self.ents.iter().filter(|e| e.alive && e.creature() && flies(e.kind)).count(); let walkers = self.creature_count() - flyers; // a bed per lair tile (aerie: per aerie tile), never more than MAX_CREATURES in all let room = MAX_CREATURES.saturating_sub(walkers + flyers); (self.counts[1].saturating_sub(walkers).min(room), self.counts[7].saturating_sub(flyers).min(room)) } // how many creatures this master can house: his beds, at most MAX_CREATURES pub fn creature_cap(&self) -> usize { (self.counts[1] + self.counts[7]).min(MAX_CREATURES) } pub fn lair_factor(&self) -> f32 { (8.0 / (self.free_beds() as f32 + 2.0)).clamp(0.35, 3.0) } #[cfg(test)] pub fn try_portal_for_test(&mut self) { let (px, py) = self.map.portals[0]; for y in 0..3 { for x in 0..3 { self.map.t[idx(px + x, py + y)].owner = self.me; } } self.try_portal(); } fn try_portal(&mut self) { let lair = self.counts[1]; let aerie = self.counts[7]; let farm = self.counts[2]; if lair == 0 && aerie == 0 { self.hint(0, "hint.portal_no_lair"); return; } if farm == 0 { self.hint(1, "hint.no_farm"); return; } if self.creature_count() >= MAX_CREATURES { self.hint(35, "hint.creature_max"); return; } let (free_lair, free_aerie) = self.free_lair_aerie(); if free_lair == 0 && free_aerie == 0 { self.hint(2, "hint.lair_full"); return; } let mut opts: Vec<(EK, u32)> = Vec::new(); // flying creatures need room in the aerie if free_aerie > 0 { if aerie >= 4 { opts.push((EK::Bat, 4)); } if aerie >= 6 && self.counts[5] >= 4 { opts.push((EK::Gargoyle, 2)); } if aerie >= 6 && self.counts[4] >= 6 { opts.push((EK::Owl, 2)); } let has_wyrm = self.ents.iter().any(|e| e.alive && e.kind == EK::Wyrm && !e.hero); if aerie >= 16 && self.counts[0] >= 16 && !has_wyrm { opts.push((EK::Wyrm, 1)); } } if free_lair == 0 { if opts.is_empty() { self.hint(2, "hint.lair_full"); return; } } else { self.portal_walkers(&mut opts); } if opts.is_empty() { return; } self.portal_arrival(opts); } // walking creatures that the rooms attract fn portal_walkers(&mut self, opts: &mut Vec<(EK, u32)>) { let farm = self.counts[2]; opts.push((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)); } if self.counts[5] >= 6 { opts.push((EK::Jailer, 2)); } if self.counts[6] >= 6 { opts.push((EK::Priestess, 2)); } if self.counts[ROOM_FORGE] >= 6 { opts.push((EK::Cogling, 3)); } // the Chainbreaker only visits big dungeons with experienced creatures and a rich treasury let room_tiles: usize = self.counts.iter().sum(); let veteran = self.ents.iter().any(|e| e.alive && e.creature() && e.level >= 5); let has_one = self.ents.iter().any(|e| e.alive && e.kind == EK::Chainbreaker && !e.hero); if room_tiles >= 40 && veteran && self.gold >= 4000.0 && !has_one { opts.push((EK::Chainbreaker, 1)); } } fn portal_arrival(&mut self, opts: Vec<(EK, u32)>) { 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 owned: Vec = (0..self.map.portals.len()).filter(|&k| self.map.portal_owner(k) == self.me).collect(); let pk = owned[(self.rng.next() as usize) % owned.len()]; let (px, py) = self.map.portals[pk]; let id = self.spawn(kind, false, px as f32 + 1.5, py as f32 + 1.5, 1); // new creatures report at the portal and set up their bed first if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) { e.settled = false; e.job = Job::Register; e.timer = 2.0; } 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)]); } // Payday: the training bill is paid at once; every creature fetches its wage from a // treasury itself, at a random moment within about a minute fn do_payday(&mut self) { let bill = self.training_bill.min(self.gold).max(0.0); self.gold -= bill; self.training_bill = 0.0; let mut n = 0; let mut total = bill; let merchant = self.talents.wages(); for i in 0..self.ents.len() { if !self.ents[i].alive || !self.ents[i].creature() { continue; } let r = self.rng.f(); let e = &mut self.ents[i]; let w = st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32) * merchant; e.pay_due += w; e.pay_t = r * 55.0; n += 1; total += w; } if n > 0 || bill > 0.0 { self.sfx_ui(Sfx::Payday); self.msg("msg.payday_walk", vec![n.to_string(), (total as i32).to_string()]); } } // a creature goes for its wage: the nearest treasury tile holding treasure, else the core fn start_fetch_wage(&mut self, e: &mut Ent) { let cur = e.tile(); let goal = |i: usize| { let t = &self.map.t[i]; (t.kind == Kind::Treasury && t.owner == self.me && t.gold > 0) || (self.counts[0] == 0 && self.map.adj_core(i)) }; let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), goal, true) } else { self.map.search(cur, self.map.walk_cost(), goal, true) }; match found { Some(p) => { let t = p.last().copied().unwrap_or(cur); e.path = p; e.job = Job::FetchWage(t); } // no way to a treasury: the wage is paid out anyway None => self.take_wage(e, None), } } // takes the wage out of the treasury tile (the rest from elsewhere); unpaid = angry fn take_wage(&mut self, e: &mut Ent, at: Option) { let mut need = e.pay_due; if let Some(t) = at { let tile = &mut self.map.t[t]; if tile.kind == Kind::Treasury { let take = (tile.gold as f32).min(need).floor(); tile.gold -= take as i32; self.gold -= take; need -= take; } } let rest = need.min(self.gold.max(0.0)); self.gold -= rest; need -= rest; e.pay_due = 0.0; if need > 0.5 { e.happy -= 0.45; self.msg("msg.wage_unpaid", vec![format!("@{}", st(e.kind).name)]); self.sfx(Sfx::Error, e.x, e.y); } else { e.happy = (e.happy + 0.2).min(1.0); self.fx(5, e.x, e.y, 0.0, 0.0, 0.8, 0xff30d0ff); self.sfx(Sfx::Deposit, e.x, e.y); } } // a new creature: report at the portal, go to the lair (aerie), set up the bed fn settle(&mut self, e: &mut Ent, dt: f32) { let me = e.id + 1; match e.job { Job::Register => { e.working = true; e.timer -= dt; if e.timer <= 0.0 { let cur = e.tile(); let bed_kind = if flies(e.kind) { Kind::Aerie } else { Kind::Lair }; let free = |i: usize| self.map.t[i].kind == bed_kind && self.map.t[i].bed == 0 && self.map.t[i].owner == self.me; let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), free, true) } else { self.map.search(cur, self.map.walk_cost(), free, true) }; match found { Some(p) => { let b = p.last().copied().unwrap_or(cur); self.map.t[b].bed = me; e.bed = Some(b); e.path = p; e.job = Job::MakeBed(b); e.timer = 4.0; } None => { e.settled = true; e.job = Job::None; } } } } Job::MakeBed(b) => { if self.step_path(e, dt) { return; } if self.map.t[b].bed != me { e.settled = true; e.job = Job::None; return; } e.working = true; e.timer -= dt; if e.timer <= 0.0 { e.settled = true; e.job = Job::None; let (bx, by) = xy(b); self.fx(8, bx as f32 + 0.5, by as f32 + 0.5, 0.0, 0.0, 0.6, 0xff40d0ff); self.sfx(Sfx::Build, bx as f32 + 0.5, by as f32 + 0.5); } } _ => { e.job = Job::Register; e.timer = 2.0; } } } // strength of a squad (same formula as the army value) pub(crate) 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); let home = (gx as f32 + 1.5, gy as f32 + 1.5); if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) { e.guard = guard; if guard { e.home = Some(home); } } } 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. pub(crate) 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)]); // the humans march on the strongest master; the wave is measured against him let goal = self.wave_goal(); let target = (self.side_army(goal) * 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)); pool.push((Fairy, 1)); } if w >= 4 { pool.push((BattleMage, 1)); } if w >= 6 { pool.push((Giant, 1)); } // the rogues of the later waves (in the campaign only from the later missions on) for (k, from_wave, from_mission) in crate::rogues::ROGUES { if w >= from_wave && self.mission.as_ref().map_or(true, |m| m.index >= from_mission) { pool.push((k, 1)); } } // siege engines for the dungeon's doors, walls and towers if w >= 4 && self.traps.iter().any(|t| t.kind.door()) { pool.push((Ram, 1)); } if w >= 6 && self.towers.iter().any(|t| !t.hero) { pool.push((Catapult, 1)); } 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); let before = self.ents.len(); self.spawn_squad(&list, false); // the paladin of every fifth wave is one of the Order's champions if boss.is_some() { if let Some(j) = (before..self.ents.len()).find(|&j| self.ents[j].kind == Paladin) { self.make_boss(j, 1 + ((w / 5 - 1) % 4) as u8); } } if goal != self.me_side { for e in self.ents[before..].iter_mut() { e.target = Some(goal); e.waypoint = None; } self.planned_entries.clear(); let args = vec![w.to_string(), list.len().to_string()]; if self.is_master(goal) { self.announce_wave("msg.wave", "msg.wave_on_master", args, goal, Sfx::WaveStart); } else { self.sfx_ui(Sfx::WaveStart); self.msg(if goal == 2 { "msg.wave_on_rival1" } else { "msg.wave_on_rival2" }, args); } return; } if self.planned_entries.is_empty() { self.plan_entries(); } let entries = self.planned_entries.clone(); if !entries.is_empty() { let split = list.len() >= 6 && entries.len() >= 2; // the paladin (if any) is spawned first and joins group A, which gets the extra hero for (k, e) in self.ents[before..].iter_mut().enumerate() { e.waypoint = Some(if split && k % 2 == 1 { entries[1] } else { entries[0] }); } if !split { self.planned_entries.truncate(1); } self.entry_history.extend(self.planned_entries.iter().copied()); let n = self.entry_history.len(); if n > 4 { self.entry_history.drain(0..n - 4); } } let key = if boss.is_some() { "msg.wave_boss" } else { "msg.wave" }; let goal = self.me_side; self.announce_wave(key, "msg.wave_on_master", vec![w.to_string(), list.len().to_string()], goal, Sfx::WaveStart); } // The side a hero wave marches on: the strongest master still standing (creatures, towers and // gold), players and rival masters alike; ties go to the lower side. pub fn wave_goal(&self) -> u8 { let mut best = (self.me_side, f32::MIN); let mut sides: Vec = self.master_sides().into_iter().filter(|&s| !self.is_out(s) && self.core_hp_of(s) > 0.0).collect(); sides.extend(self.rivals.iter().filter(|r| r.alive).map(|r| r.side)); sides.sort_unstable(); for s in sides { let gold = if self.is_master(s) { self.gold_of(s) } else { 0.0 }; let score = self.side_army(s) + 0.05 * gold; if score > best.1 { best = (s, score); } } best.0 } // the fighting strength of one side, like `army_value`: its creatures and (half) its towers pub fn side_army(&self, side: u8) -> f32 { let owner = Owner::of_side(side); let (mut h, mut d) = (0.0, 0.0); for e in &self.ents { if e.alive && !e.hero && !e.chained && e.kind != EK::Worker && e.side() == side { h += e.max_hp; d += e.atk() / st(e.kind).interval; } } for t in self.towers.iter().filter(|t| !t.hero && self.map.t[t.tile].owner == owner) { h += 0.5 * t.max_hp(); d += 0.5 * t.dmg() / tower_stats(t.kind).interval; } (h * d).sqrt() } // tells about a wave: the attacked master hears `key`; every other player of a match hears // `other` (with the attacked master's colour as last argument). A wave on a rival master is // told to the player like a normal warning. fn announce_wave(&mut self, key: &'static str, other: &'static str, args: Vec, goal: u8, sfx: Sfx) { if !self.is_master(goal) || self.netplay.is_none() || self.local_side == goal { let me = self.me_side; self.switch_to(goal); self.msg(key, args); self.sfx_ui(sfx); self.switch_to(me); return; } let mut a = args; a.push(format!("@net.side{goal}")); self.tell_local(other, a, Some(sfx)); } // 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()]); } } } pub(crate) 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, 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"); if self.mission.as_ref().map_or(true, |m| m.gate_defenders) { 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.slow = (e.slow - dt).max(0.0); e.skill_cd -= dt; e.boost = (e.boost - dt).max(0.0); if e.stun > 0.0 { e.stun -= dt; e.moving = false; return; } self.tick_effects(e, dt); // slapped units work and move faster, hasted ones even more let dt = if e.boost > 0.0 { dt * 1.5 } else { dt } * if e.haste > 0.0 { 1.6 } else { 1.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) * crate::armory::gear_factor(e.gear); 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![crate::names::arg(e), e.level.to_string()]); } } if e.chained { // waits in chains until the player's imps claim its cave e.moving = false; let (x, y) = xy(e.tile()); let reached = std::iter::once((0, 0)).chain(DIR8.iter().copied()).any(|(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |t| t.owner == self.me)); if reached { e.chained = false; e.settled = false; e.job = Job::Register; e.timer = 1.0; e.happy = 1.0; self.freed += 1; self.fx(7, e.x, e.y, 0.0, 0.0, 1.2, 0xff44ddff); self.sfx(Sfx::Convert, e.x, e.y); self.msg("msg.creature_freed", vec![format!("@{}", st(e.kind).name)]); } } else if self.demo.is_some() { self.demo_ai(e, dt); } else if e.hero && e.captive != 0 { self.captive_update(e, dt); } else if !self.is_master(e.side) && (self.is_rival_side(e.side) || e.rival()) { self.rival_ai(e, dt); } else if e.kind == EK::Trader { self.trader_ai(e, dt); } else if e.dwarf() { self.dwarf_ai(e, dt); } else 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); } } // the direction a unit faces: where it walks, otherwise what it works on or fights fn turn(&self, e: &mut Ent, x0: f32, y0: f32) { let (dx, dy) = (e.x - x0, e.y - y0); if dx * dx + dy * dy > 1e-8 { e.dir = dy.atan2(dx); return; } if !e.working { return; } let tile = |t: usize| { let (x, y) = xy(t); Some((x as f32 + 0.5, y as f32 + 0.5)) }; let target = match e.job { Job::Dig(t) | Job::Reinforce(t) | Job::Help(t) | Job::TearBridge(t) | Job::MendBridge(t) | Job::RaiseWall(t) => tile(t), Job::Attack(id) | Job::Torment(id) | Job::Carry(id) | Job::Brawl(id) => self.find(id).map(|j| (self.ents[j].x, self.ents[j].y)), Job::Repair(id) | Job::AttackTower(id) => self.towers.iter().find(|t| t.id == id).map(|t| t.pos()), _ => None, }; if let Some((tx, ty)) = target { let (dx, dy) = (tx - e.x, ty - e.y); if dx * dx + dy * dy > 0.01 { e.dir = dy.atan2(dx); } } } // moves a unit along its path; true = still moving pub(crate) 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.kind == Kind::Door { if e.side() != 0 { // enemies smash doors on their way if self.bash_door(e, next, dt) { return true; } } else if tile.res & 1 == 1 && e.tile() != next { // locked: our own creatures have to find another way e.path.clear(); return false; } } if !tile.walkable() && !(flies(e.kind) && tile.liquid()) { if (e.hero || (!self.is_master(e.side) && (self.is_rival_side(e.side) || e.rival()))) && 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 | EK::Giant => 2.0, EK::Ram => 4.0, _ => 1.0, } * if e.kind == EK::BattleMage && (tile.reinforced || tile.kind == Kind::Steel) { // the battle mage blasts reinforced walls (steel resists better) if tile.kind == Kind::Steel { 0.4 } else { 1.6 } } else if tile.kind == Kind::Steel { 0.05 } else if tile.reinforced { 0.2 } else { 1.0 }; if e.kind == EK::BattleMage { let (sx, sy) = (e.x, e.y); self.shot(sx, sy, 0.6, tx as f32 + 0.5, ty as f32 + 0.5, 0.5, 0.3, 0xff3080ff); self.style_last_fx(crate::particles::ST_FIRE); } 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.slow > 0.0 { 0.55 } else { 1.0 }; 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]; // a grove's mushroom leaves its moss behind let grove = t.kind == Kind::Mushroom && t.res & MUSH_GROVE != 0; t.kind = if grove { Kind::Grove } else { Kind::Floor }; if grove { t.res = 0; } t.owner = Owner::None; t.marked = false; t.reinforced = false; t.reserved = 0; t.hp = 0.0; t.gold = 0; let (x, y) = xy(i); self.count(crate::stats::DUG, 1.0); 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); } pub(crate) fn unreserve(&mut self, me: u32) { for t in self.map.t.iter_mut() { if t.reserved == me { t.reserved = 0; } } } pub(crate) 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; } } } pub(crate) 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 } pub(crate) fn nearest_enemy(&self, e: &Ent, radius: f32) -> Option { let mut best = None; let mut bd = radius * radius; for o in &self.ents { // every other side is an enemy; co-op partners and team mates fight the heroes, not each other let coop_ally = self.allied(e.side(), o.side()); if !o.alive || o.held || o.side() == e.side() || coop_ally || o.captive != 0 || o.chained || o.invis > 0.0 { 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 pub(crate) 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 e.kind == EK::Jailer && e.skill_cd <= 0.0 && d > 1.4 && d < 4.5 && self.los(e.x, e.y, tx, ty) { // jailer: pulls the enemy in with a chain let (dx, dy) = norm(tx - e.x, ty - e.y); let (nx, ny) = (e.x + dx * 0.9, e.y + dy * 0.9); if self.map.walk(nx as i32, ny as i32) { self.fx(0, tx, ty, e.x, e.y, 0.3, 0xff8090a0); self.sfx(Sfx::Capture, e.x, e.y); let t = &mut self.ents[j]; t.x = nx; t.y = ny; t.path.clear(); e.skill_cd = 6.0; return true; } } 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 && self.use_ability(e, j) { return 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); let p = if flies(e.kind) { self.map.fly_path_to(e.tile(), tt) } else { self.map.path_to(e.tile(), tt) }; match p { 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 } #[cfg(test)] pub(crate) fn hit_for_test(&mut self, e: &mut Ent, j: usize) { self.hit(e, j); } fn hit(&mut self, e: &mut Ent, j: usize) { // the Warlord talent of the attacker's master let boost = if !e.hero && Owner::MASTER_SIDES.contains(&e.side) { self.talents_of(e.side).damage() } else { 1.0 }; let dmg = e.atk() * (0.85 + self.rng.f() * 0.3) * boost; let (tx, ty) = (self.ents[j].x, self.ents[j].y); let s = st(e.kind); // the higher the level, the bigger the attack looks (up to almost twice at Lv 10) let lv = 1.0 + 0.1 * (e.level.max(1) - 1) as f32; if s.range > 1.5 { let col = match e.kind { EK::Mage => 0xffff44cc, EK::Priest => 0xff88ffff, EK::Priestess => 0xff3080ff, EK::Owl => 0xffffa060, EK::Wyrm => 0xff2080ff, EK::Inquisitor => 0xff40d0ff, _ => 0xff70b0d0, }; // flyers shoot from the air let h = if flies(e.kind) { 0.8 } else { 0.45 }; self.shot(e.x, e.y, h, tx, ty, 0.45, 0.25, col); self.grow_last_fx(lv); use crate::particles::*; self.style_last_fx(match e.kind { EK::Archer | EK::Ranger => ST_ARROW, EK::Inquisitor => ST_HOLY, EK::Mage => ST_SHADOW, EK::Priest => ST_HOLY, EK::Priestess | EK::Wyrm | EK::BattleMage => ST_FIRE, EK::Owl => ST_ARCANE, _ => ST_PLAIN, }); self.sfx(Sfx::Shoot, e.x, e.y); } else { self.fx(2, tx, ty, 0.0, 0.0, 0.25, if e.level >= 9 { 0xff60e0ff } else { 0xffffffff }); self.grow_last_fx(lv); self.sfx(Sfx::MeleeHit, tx, ty); } e.xp += dmg * 0.6; if e.kind == EK::Chainbreaker { // twin axes: a second enemy next to the Chainbreaker takes half damage let me = e.side(); let second = self .ents .iter() .enumerate() .find(|(o, x)| *o != j && x.alive && !x.held && x.side() != me && x.captive == 0 && (x.x - e.x).powi(2) + (x.y - e.y).powi(2) < 2.6) .map(|(o, _)| o); if let Some(o) = second { let (ox, oy) = (self.ents[o].x, self.ents[o].y); self.fx(2, ox, oy, 0.0, 0.0, 0.25, 0xff40a0ff); self.grow_last_fx(lv); self.ents[o].hp -= dmg * 0.5; self.ents[o].flash = 0.15; if self.ents[o].hp <= 0.0 { self.kill(o); } } } if e.kind == EK::Priestess || e.kind == EK::Wyrm || e.kind == EK::BattleMage { // fire on a wooden bridge sets it alight now and then if self.rng.f() < 0.25 { self.ignite_at(tx, ty); } // fireball / fire breath: half damage to enemies around the target let me = e.side(); let splash: Vec = self .ents .iter() .enumerate() .filter(|(o, x)| *o != j && x.alive && !x.held && x.side() != me && x.captive == 0 && (x.x - tx).powi(2) + (x.y - ty).powi(2) < if crate::personality::knows(e, "inferno") { 6.25 } else { 1.44 }) .map(|(o, _)| o) .collect(); for o in splash { if self.ents[o].alive { self.ents[o].hp -= dmg * 0.5; self.ents[o].flash = 0.15; if self.ents[o].hp <= 0.0 { self.kill(o); } } } if !self.ents[j].alive { return; } } // learned abilities that come with a blow if crate::personality::knows(e, "drain") { e.hp = (e.hp + dmg * 0.3).min(e.max_hp); } if crate::personality::knows(e, "web") && self.rng.f() < 0.3 { self.ents[j].slow = self.ents[j].slow.max(3.0); } if crate::personality::knows(e, "shackle") && self.rng.f() < 0.2 { self.ents[j].stun = self.ents[j].stun.max(2.0); } if crate::personality::knows(e, "volley") { let me = e.side(); let second = self.ents.iter().enumerate().find(|(o, x)| *o != j && x.alive && !x.held && x.side() != me && x.captive == 0 && (x.x - tx).powi(2) + (x.y - ty).powi(2) < 6.0).map(|(o, _)| o); if let Some(o) = second { let (ox, oy) = (self.ents[o].x, self.ents[o].y); self.shot(e.x, e.y, 0.8, ox, oy, 0.45, 0.25, 0xffffa060); self.style_last_fx(crate::particles::ST_ARCANE); self.ents[o].hp -= dmg * 0.6; self.ents[o].flash = 0.15; if self.ents[o].hp <= 0.0 { self.kill(o); } } } let my_id = e.id; e.invis = 0.0; // attacking breaks invisibility let armour = { let t = &self.ents[j]; if !t.hero && Owner::MASTER_SIDES.contains(&t.side) { self.talents_of(t.side).armour() } else { 1.0 } }; let t = &mut self.ents[j]; t.hp -= if t.shield > 0.0 { dmg * 0.5 } else { dmg } * armour; t.flash = 0.15; t.bane = crate::gfx::sprite_index(e.kind) as u8 + 1; if t.hp <= 0.0 && t.hero && t.captive == 0 { e.kills += 1; } 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); } } // free places in the torture chamber (one prisoner per 2 tiles) pub fn prison_space(&self) -> usize { let cap = self.counts[5] / 2 + self.counts[crate::rooms::ROOM_PRISON]; let used = self.ents.iter().filter(|e| e.alive && e.hero && e.captive != 0).count(); cap.saturating_sub(used) } pub(crate) fn kill(&mut self, j: usize) { self.boss_loot(j); if self.try_tame(j) { return; } let e = &self.ents[j]; if e.hero && e.captive == 0 && self.prison_space() > 0 { // knocked out instead of killed: imps carry the hero to the torture chamber let (kind, x, y) = (e.kind, e.x, e.y); let id = e.id; self.release_all(id); let e = &mut self.ents[j]; e.captive = 1; e.hp = 1.0; e.carrier = 0; e.torture_t = 0.0; e.job = Job::None; e.path.clear(); e.guard = false; e.home = None; e.patrol.clear(); let b = st(kind).bounty as f32; let got = self.add_gold(b); self.count_local(crate::stats::HEROES); self.count_local(crate::stats::CAPTURED); self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff); self.sfx(Sfx::Capture, x, y); self.msg("msg.hero_captured", vec![format!("@{}", st(kind).name), (got as i32).to_string()]); return; } let e = &mut self.ents[j]; e.alive = false; let (id, kind, hero, x, y, side) = (e.id, e.kind, e.hero, e.x, e.y, e.side); // prisoners carried by this unit are dropped for o in self.ents.iter_mut() { if o.captive == 2 && o.carrier == id { o.captive = 1; o.carrier = 0; } } self.release_all(id); // a named creature of some standing gets a memorial stone let dead = self.ents[j].clone(); let remembered = self.raise_memorial(&dead); self.fx(3, x, y, 0.0, 0.0, 0.7, 0xff505070); if kind != EK::Worker && kind != EK::Revenant { self.bury(x, y); } let s = st(kind); if hero { self.count_local(crate::stats::HEROES); } else if side == self.local_side && !e_is_dwarf_side(side) { let me = self.me_side; let local = self.local_side; self.switch_to(local); self.count(crate::stats::LOST, 1.0); self.switch_to(me); } if hero { let b = s.bounty as f32 + if kind == EK::Thief { self.thief_loot(id) } else { 0.0 }; self.add_master_xp(s.bounty as f32 * crate::talents::HERO_XP); 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 if side == crate::dwarves::DWARF_SIDE { let got = self.add_gold(s.bounty as f32 * 0.5); self.sfx(Sfx::HeroDeath, x, y); self.msg("msg.dwarf_slain", vec![(got as i32).to_string()]); } else if side >= 2 { // a creature of a rival master: half its wage as booty let got = self.add_gold(s.wage as f32 * 0.5); self.sfx(Sfx::HeroDeath, x, y); self.msg("msg.rival_unit_slain", vec![format!("@{}", s.name), (got as i32).to_string()]); } else { self.sfx(Sfx::Death, x, y); if !remembered { self.msg("msg.creature_died", vec![crate::names::arg(&dead)]); } } } // falls into lava: dies at once (no capture), heroes still pay their bounty pub fn burn(&mut self, j: usize) { let e = &mut self.ents[j]; if !e.alive { return; } e.alive = false; e.held = false; let (id, kind, hero, x, y) = (e.id, e.kind, e.hero, e.x, e.y); for o in self.ents.iter_mut() { if o.captive == 2 && o.carrier == id { o.captive = 1; o.carrier = 0; } } self.release_all(id); self.hand.retain(|&h| h != id); // a named creature lost in lava or water is remembered too if !hero { let mut dead = self.ents[j].clone(); let lava = self.map.get(x as i32, y as i32).map_or(false, |t| t.river() == Some(Kind::Lava)); dead.bane = if lava { crate::names::BANE_LAVA } else { crate::names::BANE_WATER }; self.raise_memorial(&dead); } self.fx(3, x, y, 0.0, 0.0, 1.0, 0xff2060ff); self.fx(2, x, y, 0.0, 0.0, 0.5, 0xff30a0ff); self.sfx(Sfx::Death, x, y); let s = st(kind); if hero { let got = self.add_gold(s.bounty as f32); self.msg("msg.hero_burned", vec![format!("@{}", s.name), (got as i32).to_string()]); } else { self.msg("msg.creature_burned", vec![format!("@{}", s.name)]); } } // pushes a unit; into lava = burnt, never into water or walls pub fn knock(&mut self, j: usize, dx: f32, dy: f32) { let e = &self.ents[j]; let (nx, ny) = (e.x + dx, e.y + dy); let Some(t) = self.map.get(nx as i32, ny as i32).copied() else { return }; if flies(self.ents[j].kind) { if t.walkable() || t.liquid() { let e = &mut self.ents[j]; e.x = nx; e.y = ny; e.path.clear(); } return; } if t.kind == Kind::Lava { let e = &mut self.ents[j]; e.x = nx; e.y = ny; self.burn(j); } else if t.walkable() { let e = &mut self.ents[j]; e.x = nx; e.y = ny; e.path.clear(); } } // the ground of tile i turned into river (bridge removed): units on it fall in pub(crate) fn ground_lost(&mut self, i: usize) { let lava = self.map.t[i].river() == Some(Kind::Lava); let (x, y) = xy(i); for j in 0..self.ents.len() { let e = &self.ents[j]; if !e.alive || e.held || e.tile() != i || flies(e.kind) { continue; } if e.hero { self.count_local(crate::stats::DROWNED); } if lava { self.burn(j); continue; } // water: swim to the nearest bank, otherwise drown let bank = DIR8.iter().map(|&(dx, dy)| (x + dx, y + dy)).find(|&(bx, by)| self.map.walk(bx, by)); match bank { Some((bx, by)) => { let e = &mut self.ents[j]; e.x = bx as f32 + 0.5; e.y = by as f32 + 0.5; e.path.clear(); } None => self.burn(j), } } } // Bridges grow from own ground into the river: every pass adds the river tiles of the // rectangle that touch own walkable ground (including bridges built in the pass before) // Earth walls next to own ground become steel walls pub fn build_steel(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) { let (mut built, mut broke) = (0, 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 t = self.map.t[idx(x, y)]; if t.kind != Kind::Earth || t.owner == Owner::Hero { continue; } let own = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == self.me && n.walkable())); if !own { continue; } let cost = STEEL_COST[t.reinforced as usize] as f32; if self.gold < cost { broke = true; continue; } self.gold -= cost; let tl = &mut self.map.t[idx(x, y)]; tl.kind = Kind::Steel; tl.reinforced = true; tl.marked = false; tl.hp = STEEL_HP; built += 1; self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.5, 0xffd0d0d0); if built == 1 { self.sfx(Sfx::Reinforce, x as f32 + 0.5, y as f32 + 0.5); } } } if broke { self.msg("msg.no_gold", vec![]); self.sfx_ui(Sfx::Error); } else if built == 0 { self.hint(23, "hint.steel_where"); } } pub fn build_bridges(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) { self.build_bridges_of(x0, y0, x1, y1, false); } pub fn build_bridges_of(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, draw: bool) { let (mut built, mut broke, mut any_river, mut no_wood) = (0, false, false, false); loop { let mut add = Vec::new(); for y in y0.min(y1)..=y0.max(y1) { for x in x0.min(x1)..=x0.max(x1) { if !inb(x, y) || !self.map.t[idx(x, y)].liquid() { continue; } any_river = true; let own = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == self.me && n.walkable())); if own { add.push(idx(x, y)); } } } if add.is_empty() { break; } let mut progress = false; for i in add { let lava = self.map.t[i].kind == Kind::Lava; let cost = if draw { DRAWBRIDGE_COST[lava as usize] } else { BRIDGE_COST[lava as usize] } as f32; // wooden bridges also cost wood let wood = if lava { 0.0 } else if draw { crate::wood::DRAWBRIDGE_WOOD } else { crate::wood::BRIDGE_WOOD }; if self.gold < cost { broke = true; continue; } if self.wood + 1e-3 < wood { no_wood = true; continue; } self.gold -= cost; self.wood -= wood; let t = &mut self.map.t[i]; t.kind = if draw { Kind::Drawbridge } else if lava { Kind::LavaBridge } else { Kind::Bridge }; t.res = if draw && lava { DB_LAVA } else { 0 }; t.owner = self.me; built += 1; progress = true; let (x, y) = xy(i); self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.5, 0xff40d0ff); if built == 1 { self.sfx(Sfx::Build, x as f32 + 0.5, y as f32 + 0.5); } } if !progress { break; } } if broke { self.msg("msg.no_gold", vec![]); self.sfx_ui(Sfx::Error); } else if no_wood { self.msg("msg.no_wood", vec![]); self.sfx_ui(Sfx::Error); } else if built == 0 { self.hint(21, if any_river { "hint.bridge_where" } else { "hint.bridge_river" }); } } // An earth corner between two reinforced walls (and diagonal to own floor) cannot be // reached by the imps: it is reinforced together with the walls. The floor condition keeps // this from spreading into the earth behind the walls. pub fn reinforce_corners(&mut self) { let (w, h) = (mw() as i32, mh() as i32); let wall = |m: &Map, x: i32, y: i32| m.get(x, y).map_or(false, |n| n.kind == Kind::Earth && n.reinforced && n.owner != Owner::Hero); let floor = |m: &Map, x: i32, y: i32| m.get(x, y).map_or(false, |n| n.owner == self.me && n.walkable()); let mut add = Vec::new(); for y in 1..h - 1 { for x in 1..w - 1 { let t = &self.map.t[idx(x, y)]; if t.kind != Kind::Earth || t.reinforced || t.marked || t.owner == Owner::Hero { continue; } for (ax, by) in [(1, 1), (1, -1), (-1, 1), (-1, -1)] { if wall(&self.map, x + ax, y) && wall(&self.map, x, y + by) && floor(&self.map, x + ax, y + by) { add.push(idx(x, y)); break; } } } } for i in add { let t = &mut self.map.t[i]; t.reinforced = true; t.hp = t.hp.max(12.0); let (x, y) = xy(i); self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.5, 0xff4040e0); } } // tiles within two of a fighting hero fn update_contested(&mut self) { let n = self.map.t.len(); if self.contested.len() != n { self.contested = vec![false; n]; } else { self.contested.iter_mut().for_each(|c| *c = false); } for e in &self.ents { // any enemy of the player (heroes and the rivals' units) keeps ground from being claimed. // In co-op matches, allied master sides do not block the player's own ground claims. if !e.alive || e.held || e.captive != 0 || e.side() == 0 || (self.coop && Owner::MASTER_SIDES.contains(&e.side())) || self.allied(0, e.side()) { continue; } let (x, y) = (e.x as i32, e.y as i32); for dy in -2..=2 { for dx in -2..=2 { if inb(x + dx, y + dy) { self.contested[idx(x + dx, y + dy)] = true; } } } } } // a knight tile was claimed: vaults hand over their treasure, shards come back fn take_knight_tile(&mut self, i: usize) { let t = self.map.t[i]; let (x, y) = (xy(i).0 as f32 + 0.5, xy(i).1 as f32 + 0.5); if t.kind == Kind::Treasury && t.gold > 0 { // the content stays in the (now own) treasury tile and counts as own gold self.gold += t.gold as f32; self.loot += t.gold as f32; self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff); self.sfx(Sfx::Payday, x, y); self.msg("msg.vault_taken", vec![t.gold.to_string()]); } if t.kind == Kind::ManaSpring { self.fx(7, x, y, 0.0, 0.0, 1.2, 0xffffe060); self.sfx(Sfx::Magic, x, y); self.msg("msg.spring_taken", vec![]); } if t.kind == Kind::RuneChest { self.open_rune_chest(i); } if t.kind == Kind::Shard { self.shards_taken += 1; self.fx(7, x, y, 0.0, 0.0, 1.2, 0xff6060ff); self.sfx(Sfx::LevelUp, x, y); self.msg("msg.shard_taken", vec![]); } } // a dwarven chest: the plans of a rune forge tower (one free build) pub fn open_rune_chest(&mut self, i: usize) { let (x, y) = (xy(i).0 as f32 + 0.5, xy(i).1 as f32 + 0.5); self.map.t[i].kind = Kind::Floor; self.rune_free += 1; self.fx(7, x, y, 0.0, 0.0, 1.4, 0xff40c0ff); self.sfx(Sfx::LevelUp, x, y); self.msg("msg.rune_chest", vec![]); } // a fortress unit spotted an enemy: guards and patrols nearby join the fight fn alarm(&mut self, x: f32, y: f32, target: u32) { for o in self.ents.iter_mut() { if !o.alive || o.held || !o.active_hero() || !(o.guard || !o.patrol.is_empty()) { continue; } if matches!(o.job, Job::Attack(_)) || (o.x - x).powi(2) + (o.y - y).powi(2) > 13.0 * 13.0 { continue; } o.job = Job::Attack(target); o.path.clear(); o.repath = 0.0; } } // fortress units: a bridge of the player near the post (None if none) fn bridge_to_tear(&self, e: &Ent) -> Option { let (hx, hy) = e.home.unwrap_or((e.x, e.y)); let (cx, cy) = (hx as i32, hy as i32); for r in 0..=9i32 { for dy in -r..=r { for dx in -r..=r { if dx.abs() != r && dy.abs() != r { continue; } if let Some(t) = self.map.get(cx + dx, cy + dy) { if t.bridge() && !t.raised() && t.owner == self.me { return Some(idx(cx + dx, cy + dy)); } } } } } None } // a knight walks next to the bridge tile and tears it down fn tear_bridge(&mut self, e: &mut Ent, i: usize, dt: f32) -> bool { let t = self.map.t[i]; if !t.bridge() || t.raised() || t.owner != self.me { return false; } let (bx, by) = xy(i); let cur = e.tile(); let (cx, cy) = xy(cur); if (cx - bx).abs() + (cy - by).abs() == 1 { e.path.clear(); e.working = true; e.face = sgn(bx as f32 + 0.5 - e.x); // the bridge wears down step by step; imps can mend it while it stands e.timer += dt; if e.timer >= 1.0 { e.timer -= 1.0; self.sfx(Sfx::MeleeHit, bx as f32 + 0.5, by as f32 + 0.5); self.fx(2, bx as f32 + 0.5, by as f32 + 0.5, 0.0, 0.0, 0.2, 0xff3a5a7a); } return self.wear_bridge(i, dt); } if e.path.is_empty() && e.repath <= 0.0 { e.repath = 1.5; e.timer = 0.0; match self.map.search(cur, self.map.walk_cost(), |j| j != i && adj4(i, j), true) { Some(p) => e.path = p, None => return false, } } self.step_path(e, dt); true } // ---------------------------------------------------------------- pit imps (workers) // a neighbour of tile i that this imp may dig: marked by its own master (side) fn dig_neighbor(&self, i: usize, me: u32, side: u8) -> 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.mark_by == side && 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 } // a damaged tower next to walkable tile i fn damaged_tower_near(&self, i: usize) -> Option { let (x, y) = xy(i); self.towers.iter().find(|t| { let (tx, ty) = xy(t.tile); !t.hero && (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 { if self.map.t[i].owner != self.me { return None; } let (x, y) = xy(i); for &(dx, dy) in &DIR4 { if let Some(t) = self.map.get(x + dx, y + dy) { if t.kind == Kind::Earth && !t.reinforced && !t.marked && (t.reserved == 0 || t.reserved == me) { return Some(idx(x + dx, y + dy)); } } } None } pub(crate) fn claimable(&self, i: usize, me: u32) -> bool { let t = &self.map.t[i]; let ok = if t.owner == self.me { false } else if t.owner == Owner::None { matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Treasury | Kind::ManaSpring | Kind::RuneChest) } else if t.owner != Owner::Hero && self.allied(self.me_side, t.owner.side()) { false // an ally's ground is never taken } else { // knight and enemy masters' ground (also vaults and shard chambers), but not while enemies fight there matches!(t.kind, Kind::Floor | Kind::Treasury | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Lair | Kind::Training) && !self.contested.get(i).copied().unwrap_or(false) }; if !ok { 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 == self.me)) } fn is_deposit(&self, i: usize) -> bool { let t = &self.map.t[i]; (t.kind == Kind::Treasury && t.owner == self.me) || (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 } // flying creatures patrol far around (also over rivers and the land of the enemy) fn patrol_flight(&mut self, e: &mut Ent) { let (x, y) = xy(e.tile()); for _ in 0..8 { let nx = x + self.rng.range(-14, 14); let ny = y + self.rng.range(-14, 14); let ok = self.map.get(nx, ny).map_or(false, |t| t.walkable() || t.liquid()); if ok { if let Some(p) = self.map.fly_path_to(e.tile(), idx(nx, ny)) { if p.len() <= 40 { e.path = p; break; } } } } e.job = Job::Wander; e.timer = 2.0 + self.rng.f() * 3.0; } // badly hurt creatures flee to their bed (lair, flyers: aerie), or else next to the soul // core, and rest there until they are healed to 75% fn start_rest(&mut self, e: &mut Ent) -> bool { let cur = e.tile(); let fly = flies(e.kind); let bed_kind = if fly { Kind::Aerie } else { Kind::Lair }; let me = self.me; let target = e .bed .filter(|&b| self.map.t[b].kind == bed_kind && self.map.t[b].owner == me) .or_else(|| self.map.search(cur, self.map.creature_cost(fly), |i| self.map.t[i].kind == bed_kind && self.map.t[i].owner == me, true).map(|p| p.last().copied().unwrap_or(cur))) .or_else(|| self.map.search(cur, self.map.creature_cost(fly), |i| self.map.adj_core_of(i, me), true).map(|p| p.last().copied().unwrap_or(cur))); let Some(b) = target else { return false }; e.path = self.map.creature_path(fly, cur, b).unwrap_or_default(); e.job = Job::Rest(b); true } 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 == self.me { 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.mark_by != e.side || !tile.diggable() || !adj4(cur, t) { self.unreserve(me); e.job = Job::None; return; } // an imp carries one kind of treasure: other loads are delivered first if tile.vein().map_or(false, |r| e.carry > 0 && r != e.carry_res) { self.unreserve(me); e.job = Job::None; self.start_deliver(e); return; } let (tx, ty) = xy(t); e.face = sgn(tx as f32 + 0.5 - e.x); e.working = true; let skilled = if crate::personality::knows(e, "dig") { 1.25 } else { 1.0 }; e.timer += dt * self.help_speed(t) * skilled * self.talents.dig_speed(); 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 let Some(res) = tl.vein() { let take = tl.gold.min(RES_STRIKE[res as usize]); tl.gold -= take; e.carry += take; e.carry_res = res; 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 { if self.map.t[t].plant() { self.felled(t); } 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::Carry(id) => { self.carry_job(e, id, dt); } Job::Install(id) => { self.install_job(e, id, dt); } 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::RaiseWall(w) => { if !self.raise_wall_job(e, w, dt) { self.unreserve(me); e.job = Job::None; } } Job::MendBridge(b) => { let (bx, by) = xy(b); let (tx, ty) = (bx as f32 + 0.5, by as f32 + 0.5); if (tx - e.x).abs() > 1.6 || (ty - e.y).abs() > 1.6 || self.burning(b) { e.job = Job::None; return; } e.face = sgn(tx - e.x); e.working = true; if !self.mend_bridge(b, dt) { e.job = Job::None; } if (e.anim * 3.0).fract() < dt * 3.0 { self.sfx(Sfx::Reinforce, tx, ty); } } Job::Help(t) => { // the task is done or nobody works on it any more: back to thinking if self.task_worker(t).is_none() || self.helpers(t) > HELPERS_MAX { e.job = Job::None; e.think = 0.2; return; } let (tx, _) = xy(t); e.face = sgn(tx as f32 + 0.5 - e.x); e.working = true; if e.think <= 0.0 { // every 1.5 s: is there own work again? (if not, the imp comes back to help) e.job = Job::None; } } Job::Reinforce(w) => { let t = self.map.t[w]; if !adj4(cur, w) || t.kind != Kind::Earth || t.reinforced || t.marked { self.unreserve(me); e.job = Job::None; return; } let (wx, wy) = xy(w); e.face = sgn(wx as f32 + 0.5 - e.x); e.working = true; e.timer += dt * self.help_speed(w); if e.timer >= 2.5 { self.unreserve(me); e.job = Job::None; let tl = &mut self.map.t[w]; tl.reinforced = true; tl.hp = 12.0; // the player's imps need twice as long to dig it out again self.fx(8, wx as f32 + 0.5, wy as f32 + 0.5, 0.0, 0.0, 0.5, 0xff4040e0); self.sfx(Sfx::Reinforce, wx as f32 + 0.5, wy as f32 + 0.5); } } Job::Claim(t) => { if cur != t || !self.claimable(t, me) { self.unreserve(me); e.job = Job::None; return; } e.working = true; e.timer += dt * self.help_speed(t); // knight ground takes longer let prev = self.map.t[t].owner; let knight = prev == Owner::Hero; if e.timer >= if prev != Owner::None { 3.0 } else { 1.0 } { self.unreserve(me); e.job = Job::None; let (tx, ty) = xy(t); if knight { self.alarm(tx as f32 + 0.5, ty as f32 + 0.5, e.id); } // vaults, shards and springs (of the knights, rivals or nobody) self.take_knight_tile(t); if let (Kind::Portal, Some(pk)) = (self.map.t[t].kind, self.map.portal_at(tx, ty)) { let (px, py) = self.map.portals[pk]; for y in 0..3 { for x in 0..3 { self.map.t[idx(px + x, py + y)].owner = self.me; } } 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 = self.me; } 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 * self.talents.treasure(); let got = self.add_res_at(amount, e.carry_res, Some(cur)); 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; } // prisoners lying around are carried to the torture chamber first let lying = self.ents.iter().any(|o| o.alive && o.captive == 1 && o.carrier == 0); if lying && self.map.t.iter().any(|t| t.kind == Kind::Torture && t.bed == 0) { if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.captive_near(i).is_some(), true) { let end = p.last().copied().unwrap_or(cur); if let Some(id) = self.captive_near(end) { if let Some(k) = self.find(id) { self.ents[k].carrier = e.id; } e.path = p; e.job = Job::Carry(id); return; } } } // at least 10% of the imps (at least one) claim floor while there is floor to claim let imps = 1 + self.ents.iter().filter(|o| o.alive && !o.hero && o.kind == EK::Worker).count(); let claiming = self.ents.iter().filter(|o| o.alive && !o.hero && o.kind == EK::Worker && matches!(o.job, Job::Claim(_))).count(); let quota = ((imps as f32 * 0.1).ceil() as usize).max(1); if claiming < quota { 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; } } if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.dig_neighbor(i, me, e.side).is_some(), true) { let end = p.last().copied().unwrap_or(cur); if let Some(t) = self.dig_neighbor(end, me, e.side) { self.map.t[t].reserved = me; e.path = p; e.job = Job::Dig(t); return; } } if e.carry > 0 && self.start_deliver(e) { return; } // finished traps and doors are carried to their spots if let Some((id, p)) = self.crate_to_carry(cur) { if let Some(k) = self.traps.iter().position(|t| t.id == id) { self.traps[k].carrier = me; } e.path = p; e.job = Job::Install(id); e.timer = 0.0; return; } // damaged towers are repaired (free of charge) before more ground is claimed 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; } } // planned walls are raised if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.planned_wall_near(i, me).is_some(), true) { let end = p.last().copied().unwrap_or(cur); if let Some(w) = self.planned_wall_near(end, me) { self.map.t[w].reserved = me; e.path = p; e.job = Job::RaiseWall(w); e.timer = 0.0; return; } } // ... and so are bridges the knights worked on if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.worn_bridge_near(i).is_some(), true) { let end = p.last().copied().unwrap_or(cur); if let Some(b) = self.worn_bridge_near(end) { e.path = p; e.job = Job::MendBridge(b); 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; } 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) { self.map.t[w].reserved = me; e.path = p; e.job = Job::Reinforce(w); e.timer = 0.0; return; } } // nothing else to do: help another imp (up to five at one task) if self.start_help(e) { return; } e.think = 1.0; self.wander(e, 3); } _ => e.job = Job::None, } } // ---------------------------------------------------------------- creatures fn find_room_tile(&self, e: &Ent, kind: Kind, me: u32, need_food: bool) -> Option> { self.map.search( e.tile(), self.map.creature_cost(flies(e.kind)), |i| { let t = &self.map.t[i]; t.kind == kind && t.owner == self.me && (t.reserved == 0 || t.reserved == me) && (!need_food || t.food >= 1.0) }, true, ) } fn creature_ai(&mut self, e: &mut Ent, dt: f32) { if crate::siege::siege(e.kind) { // a siege engine has no needs: it only follows the attack flag e.hunger = 0.0; e.energy = 1.0; e.happy = 1.0; self.siege_ai(e, dt); return; } let me = e.id + 1; if e.kind == EK::Revenant { // the undead neither eat nor sleep e.hunger = 0.0; e.energy = 1.0; } else { 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.kind == EK::Priestess && e.skill_cd <= 0.0 { // flame priestess: heals the most hurt own creature nearby (or herself) let mut best: Option = None; let mut worst = if e.hp < e.max_hp * 0.8 { e.hp / e.max_hp } else { 0.8 }; for (j, o) in self.ents.iter().enumerate() { if o.alive && !o.held && o.creature() && (o.x - e.x).powi(2) + (o.y - e.y).powi(2) < 4.5 * 4.5 && o.hp / o.max_hp < worst { worst = o.hp / o.max_hp; best = Some(j); } } if let Some(j) = best { let o = &mut self.ents[j]; o.hp = (o.hp + 20.0).min(o.max_hp); let (ox, oy) = (o.x, o.y); self.fx(4, ox, oy, 0.0, 0.0, 0.8, 0xff60ff80); self.sfx(Sfx::Heal, ox, oy); e.skill_cd = 2.5; } else if e.hp < e.max_hp * 0.8 { e.hp = (e.hp + 20.0).min(e.max_hp); self.fx(4, e.x, e.y, 0.0, 0.0, 0.8, 0xff60ff80); e.skill_cd = 2.5; } else { e.skill_cd = 1.0; } } if e.think <= 0.0 { e.think = 0.4 + self.rng.f() * 0.3; // a new creature builds its own bed in the lair soon after its arrival let bed_kind = if flies(e.kind) { Kind::Aerie } else { Kind::Lair }; let beds = if flies(e.kind) { self.counts[7] } else { self.counts[1] }; if e.bed.is_none() && beds > 0 && !matches!(e.job, Job::Attack(_) | Job::Leave | Job::Rally | Job::March(_)) && self.rng.f() < 0.15 { let cur = e.tile(); let free = |i: usize| self.map.t[i].kind == bed_kind && self.map.t[i].bed == 0 && self.map.t[i].owner == self.me; let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), free, true) } else { self.map.search(cur, self.map.walk_cost(), free, true) }; if let Some(p) = found { let b = p.last().copied().unwrap_or(cur); self.map.t[b].bed = me; e.bed = Some(b); let (bx, by) = xy(b); self.fx(8, bx as f32 + 0.5, by as f32 + 0.5, 0.0, 0.0, 0.6, 0xff40d0ff); } } // badly hurt creatures may flee before they die (not all of them, and not at once) // and come back at 75% if e.hp < e.max_hp * 0.25 && !matches!(e.job, Job::Rest(_) | Job::Leave) && self.rng.f() < 0.3 { self.unreserve(me); if self.start_rest(e) { return; } } if !matches!(e.job, Job::Attack(_) | Job::Leave | Job::Rest(_) | Job::Brawl(_)) { let radius = if matches!(e.job, Job::Rally | Job::March(_)) { 8.5 } else if matches!(e.job, Job::Guard(_)) { 10.0 } else if flies(e.kind) { 9.0 } 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; } else if !matches!(e.job, Job::AttackTower(_) | Job::Sleep(_) | Job::Eat(_)) { // knight towers in reach are torn down if let Some(id) = self.enemy_tower_near(e.x, e.y, radius - 1.0) { self.unreserve(me); 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) { e.job = Job::None; e.path.clear(); } return; } // arrival: report at the portal and set up the bed before anything else if !e.settled { self.settle(e, dt); return; } // payday: the wage is fetched from a treasury if e.pay_due > 0.0 { e.pay_t -= dt; if e.pay_t <= 0.0 && !matches!(e.job, Job::FetchWage(_) | Job::Leave | Job::Rest(_)) { self.unreserve(me); self.start_fetch_wage(e); return; } } if let Job::FetchWage(t) = e.job { if self.step_path(e, dt) { return; } self.take_wage(e, Some(t)); e.job = Job::None; return; } if let Job::Rest(b) = e.job { if self.step_path(e, dt) { return; } let (bx, by) = xy(b); let there = (e.x - bx as f32 - 0.5).abs() < 1.5 && (e.y - by as f32 - 0.5).abs() < 1.5; if !there { // pushed away on the way: go on, or give up if there is no way match self.map.creature_path(flies(e.kind), e.tile(), b) { Some(p) if !p.is_empty() => e.path = p, _ => e.job = Job::None, } return; } // resting heals: quickly in a bed room, slower next to the core; back to work at 75% let bed_room = matches!(self.map.t[b].kind, Kind::Lair | Kind::Aerie); let rest = if bed_room { self.room_bonus(self.map.t[b].kind) } else { 1.0 }; e.hp = (e.hp + e.max_hp * dt * rest / if bed_room { 12.0 } else { 24.0 }).min(e.max_hp); e.energy = (e.energy + dt * rest / 14.0).min(1.0); if e.hp >= e.max_hp * 0.75 { e.job = Job::None; } return; } if e.kind == EK::Chainbreaker { // restless: without a fight for two minutes its mood sinks (skill_cd counts down as idle time) if matches!(e.job, Job::Attack(_)) { e.skill_cd = 0.0; } else if e.skill_cd < -120.0 { e.happy -= 0.005 * dt; self.hint(20, "hint.chainbreaker_restless"); } } self.mood_check(e, dt); self.passive_abilities(e); if let Job::Brawl(id) = e.job { self.brawl(e, id, dt); return; } 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 let Job::March(f) = e.job { if self.attack_marks.contains(&f) { self.rally_to(e, f, dt); } else { e.job = Job::None; e.path.clear(); } 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 * self.room_bonus(Kind::Temple)).min(e.max_hp); } self.unreserve(me); e.job = Job::None; } } Job::Torment(id) => { // working on a prisoner: makes it more likely to change sides (see captive_update) let Some(k) = self.ents.iter().position(|o| o.id == id && o.alive && o.captive == 3) else { e.job = Job::None; return; }; let px = self.ents[k].x; e.face = sgn(px - e.x); e.working = true; e.xp += 2.0 * dt; e.timer -= dt; if e.timer <= 0.0 { e.job = Job::None; } } Job::Train(t) => { // training is billed on payday; it stops when the bill would exceed the gold if cur != t || self.map.t[t].kind != Kind::Training || self.training_bill + 1.0 > self.gold { self.unreserve(me); e.job = Job::None; return; } e.working = true; self.training_bill += TRAIN_COST * dt; e.xp += 7.0 * dt * self.room_bonus(Kind::Training); e.timer -= dt; if e.timer <= 0.0 { self.unreserve(me); e.job = Job::None; } } Job::Guard(t) => { // a guard post, or a guard order in any room e.timer -= dt; if cur != t || !self.map.t[t].walkable() || self.map.t[t].owner != self.me || e.timer <= 0.0 { self.unreserve(me); e.job = Job::None; } } Job::Spar(t) => { if !self.spar(e, t, dt) { self.unreserve(me); e.job = Job::None; } } Job::Forge(t) => { if cur != t || self.map.t[t].kind != Kind::Forge || !self.forge_orders() { self.unreserve(me); e.job = Job::None; return; } e.working = true; let rate = if e.kind == EK::Cogling { 2.5 } else { 1.0 } * (1.0 + 0.1 * (e.level - 1) as f32); self.forge_work += rate * dt * self.room_bonus(Kind::Forge); e.xp += 2.5 * 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 * 0.5 * dt).min(self.mana_max()); // research: mages and owls are the scholars let points = match e.kind { EK::Mage => 1.6, EK::Owl => 1.3, _ => 0.5, } * (1.0 + 0.12 * (e.level - 1) as f32); self.add_research(points * dt * self.room_bonus(Kind::Library)); e.xp += 3.0 * dt; e.timer -= dt; if e.timer <= 0.0 { self.unreserve(me); e.job = Job::None; } } Job::Pray(t) => { if cur != t || self.map.t[t].kind != Kind::Temple { self.unreserve(me); e.job = Job::None; return; } e.working = true; let rate = if e.kind == EK::Priestess { 1.6 } else { 0.9 }; self.mana = (self.mana + rate * dt).min(self.mana_max()); e.happy = (e.happy + 0.01 * dt).min(1.0); e.xp += 1.0 * dt; e.timer -= dt; if e.timer <= 0.0 { self.unreserve(me); e.job = Job::None; } } Job::HealCore => { if !self.map.adj_core(cur) || self.core_hp >= CORE_HP { e.job = Job::None; return; } e.working = true; let (cx, cy) = self.map.core; e.face = sgn(cx as f32 + 1.5 - e.x); self.core_hp = (self.core_hp + 8.0 * dt).min(CORE_HP); if (e.anim * 1.5).fract() < dt * 1.5 { self.fx(4, cx as f32 + 1.5, cy as f32 + 1.5, 0.0, 0.0, 0.8, 0xff60ff80); } } 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; }; self.rally_to(e, f, dt); } // march to tile f and fight there (the attack flag, or an attack marker) pub(crate) fn rally_to(&mut self, e: &mut Ent, f: usize, dt: f32) { 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); // the flag next to a rival master's core: attack the core let rival_near = self.rivals.iter().filter(|r| r.alive && !self.allied(e.side(), r.side)).find(|r| { let (cx, cy) = r.centre(); (fx - cx).powi(2) + (fy - cy).powi(2) <= 8.0 * 8.0 }); // in a match the heart of an enemy master counts as well let master_near = self.enemy_master_near(e.side(), fx, fy); if let Some(side) = rival_near.map(|r| r.side).or(master_near) { e.target = Some(side); if self.march_on(e, dt) { return; } } 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; let p = if flies(e.kind) { self.map.fly_path_to(e.tile(), target) } else { self.map.path_to(e.tile(), target) }; match p { 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(); } } } pub fn add_mark(&mut self, x: f32, y: f32, kind: u8, side: u8, duration: f32, fixed: bool) { // the shields, swords and the flag show themselves; these markers are for debugging only let _ = fixed; if !self.debug_markers || duration <= 0.0 || !x.is_finite() || !y.is_finite() { return; } let until = self.sim_steps + (duration / STEP).ceil() as u64; if let Some(m) = self.marks.iter_mut().find(|m| (m.x - x).abs() < 0.75 && (m.y - y).abs() < 0.75 && m.kind == kind && m.side == side && m.fixed == fixed) { m.until = m.until.max(until); return; } if self.marks.len() >= 80 { self.marks.remove(0); } self.marks.push(Mark { x, y, kind, side, until, fixed }); } 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 { // flying creatures sleep in the aerie let bed_kind = if flies(e.kind) { Kind::Aerie } else { Kind::Lair }; if let Some(b) = e.bed { let t = &self.map.t[b]; // a bed in somebody else's lair (an ally's, or ground that changed hands) is given up if t.kind != bed_kind || t.bed != me || t.owner != self.me { if t.bed == me { self.map.t[b].bed = 0; } e.bed = None; } } if e.bed.is_none() { let free = |i: usize| { let t = &self.map.t[i]; t.kind == bed_kind && t.bed == 0 && t.owner == self.me }; let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), free, true) } else { self.map.search(cur, self.map.walk_cost(), free, 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 { let way = if flies(e.kind) { self.map.fly_path_to(cur, b) } else { self.map.path_to(cur, b) }; if let Some(p) = way { 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(e, 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; } if e.kind == EK::Priestess && self.core_hp < CORE_HP * 0.9 && self.nearest_enemy(e, 8.0).is_none() { if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.map.adj_core(i), true) { e.path = p; e.job = Job::HealCore; return; } } if self.counts[6] > 0 { let r2 = self.rng.f(); if r2 < 0.3 || (e.kind == EK::Priestess && r2 < 0.7) { if let Some(p) = self.find_room_tile(e, Kind::Temple, me, false) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; e.job = Job::Pray(t); e.timer = 8.0 + self.rng.f() * 6.0; return; } } } // the jailer's favourite work: prisoners nobody is working on yet if e.kind == EK::Jailer && self.rng.f() < 0.85 { let busy: Vec = self.ents.iter().filter_map(|o| if let Job::Torment(p) = o.job { Some(p) } else { None }).collect(); let prisoner = self.ents.iter().filter(|o| o.alive && o.captive == 3 && !busy.contains(&o.id)).map(|o| (o.id, o.tile())).next(); if let Some((id, pt)) = prisoner { if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| i != pt && adj4(i, pt) || i == pt, true) { e.path = p; e.job = Job::Torment(id); e.timer = 12.0 + self.rng.f() * 8.0; return; } } } // guard posts and the arena if self.counts[crate::rooms::ROOM_GUARD] > 0 && self.rng.f() < 0.2 { if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.free_post(i, me), true) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; e.job = Job::Guard(t); e.timer = 35.0 + self.rng.f() * 20.0; return; } } if self.counts[crate::rooms::ROOM_ARENA] > 0 && e.hp > e.max_hp * 0.8 && self.rng.f() < 0.25 { if let Some(p) = self.find_room_tile(e, Kind::Arena, me, false) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; e.job = Job::Spar(t); e.timer = 14.0 + self.rng.f() * 6.0; return; } } // the forge: orders waiting (coglings love it, others lend a hand now and then) if self.forge_orders() && self.counts[ROOM_FORGE] > 0 { let r = self.rng.f(); if (e.kind == EK::Cogling && r < 0.9) || r < 0.25 { if let Some(p) = self.find_room_tile(e, Kind::Forge, me, false) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; e.job = Job::Forge(t); e.timer = 10.0 + self.rng.f() * 6.0; 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(e, 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(e, 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; } } if flies(e.kind) { self.patrol_flight(e); } else { self.wander(e, 4); } } // ---------------------------------------------------------------- prisoners fn captive_update(&mut self, e: &mut Ent, dt: f32) { e.moving = false; match e.captive { 1 => { // unconscious: bleeds out if nobody carries the hero away e.torture_t += dt; if e.torture_t > 60.0 { e.alive = false; self.release_all(e.id); self.fx(3, e.x, e.y, 0.0, 0.0, 0.7, 0xff505070); } } 2 => { // carried: follows the imp match self.find(e.carrier) { Some(k) => { e.x = self.ents[k].x; e.y = self.ents[k].y; } None => { e.captive = 1; e.carrier = 0; } } } _ if self.map.t[e.tile()].kind == Kind::Prison => { // in prison: no torture, but hunger wears the prisoner down e.hp -= e.max_hp / 180.0 * dt; if e.hp <= 0.0 { e.alive = false; self.release_all(e.id); self.fx(3, e.x, e.y, 0.0, 0.0, 0.7, 0xff505070); self.msg("msg.prisoner_starved", vec![format!("@{}", st(e.kind).name)]); self.bury(e.x, e.y); } } _ => { let t = e.tile(); if self.map.t[t].kind != Kind::Torture { // the chamber is gone: the prisoner breaks free e.captive = 0; e.hp = e.max_hp * 0.3; self.release_all(e.id); return; } e.working = true; e.hp -= e.max_hp / 100.0 * dt; e.torture_t += dt; if e.torture_t >= 5.0 { e.torture_t -= 5.0; // ~40% defect over the whole torture (paladins ~25%); a jailer working on // the prisoner makes it 2.5 times as likely let tormented = self.ents.iter().any(|o| o.alive && o.kind == EK::Jailer && o.job == Job::Torment(e.id) && o.path.is_empty() && (o.x - e.x).abs() < 1.6 && (o.y - e.y).abs() < 1.6); let p = if e.kind == EK::Paladin { 0.0143 } else { 0.0252 } * if tormented { 2.5 } else { 1.0 }; if tormented { self.fx(2, e.x, e.y, 0.0, 0.0, 0.3, 0xff3040ff); } if self.rng.f() < p { self.defect(e); return; } } if e.hp <= 0.0 { e.alive = false; self.release_all(e.id); self.fx(3, e.x, e.y, 0.0, 0.0, 0.7, 0xff505070); self.sfx(Sfx::HeroDeath, e.x, e.y); self.msg("msg.prisoner_died", vec![format!("@{}", st(e.kind).name)]); self.bury(e.x, e.y); } } } } // a prisoner joins the player's side fn defect(&mut self, e: &mut Ent) { self.release_all(e.id); e.hero = false; e.captive = 0; e.carrier = 0; e.guard = false; e.home = None; e.hp = e.max_hp * 0.5; e.happy = 0.6; e.hunger = 0.5; e.energy = 0.4; e.job = Job::None; e.path.clear(); e.bed = None; self.fx(7, e.x, e.y, 0.0, 0.0, 1.0, 0xff4040e0); self.sfx(Sfx::Convert, e.x, e.y); self.msg("msg.defected", vec![format!("@{}", st(e.kind).name)]); } // unconscious hero lying next to tile i that no imp takes care of yet fn captive_near(&self, i: usize) -> Option { let (x, y) = xy(i); self.ents.iter().find(|o| { o.alive && o.hero && o.captive == 1 && o.carrier == 0 && (o.x as i32 - x).abs() <= 1 && (o.y as i32 - y).abs() <= 1 }).map(|o| o.id) } fn carry_job(&mut self, e: &mut Ent, id: u32, dt: f32) { let Some(k) = self.find(id) else { e.job = Job::None; return; }; let cur = e.tile(); match self.ents[k].captive { 1 => { let (cx, cy) = (self.ents[k].x, self.ents[k].y); if (cx - e.x).abs() < 1.2 && (cy - e.y).abs() < 1.2 { // pick up and look for a free place in the torture chamber match self.map.search(cur, self.map.walk_cost(), |i| self.free_cell(i), true) { Some(p) => { let target = p.last().copied().unwrap_or(cur); self.map.t[target].bed = id + 1; let c = &mut self.ents[k]; c.captive = 2; c.carrier = e.id; e.path = p; } None => { self.ents[k].carrier = 0; e.job = Job::None; } } } else if e.path.is_empty() { match self.map.path_to(cur, idx(cx as i32, cy as i32)) { Some(p) if !p.is_empty() => e.path = p, _ => { self.ents[k].carrier = 0; e.job = Job::None; } } } } 2 if self.ents[k].carrier == e.id => { if !self.step_path(e, dt) { // arrived (or blocked): put the prisoner down on the reserved torture tile let target = self.map.t.iter().position(|t| t.bed == id + 1); match target { Some(t) if t == cur || adj4(t, cur) || t == e.tile() => { let (tx, ty) = xy(t); let c = &mut self.ents[k]; c.captive = 3; c.carrier = 0; c.hp = c.max_hp; // the torture drains it over ~100 s c.x = tx as f32 + 0.5; c.y = ty as f32 + 0.5; c.torture_t = 0.0; self.sfx(Sfx::Capture, tx as f32 + 0.5, ty as f32 + 0.5); e.job = Job::None; } Some(t) => { if let Some(p) = self.map.path_to(cur, t) { e.path = p; } else { self.ents[k].captive = 1; self.ents[k].carrier = 0; e.job = Job::None; } } None => { self.ents[k].captive = 1; self.ents[k].carrier = 0; e.job = Job::None; } } } return; } _ => { e.job = Job::None; return; } } self.step_path(e, dt); } // ---------------------------------------------------------------- heroes fn hero_ai(&mut self, e: &mut Ent, dt: f32) { if e.boss > 0 { self.boss_ability(e, dt); } // the rogues' tricks: a thief who is busy stealing or running does nothing else if self.rogue_ai(e, dt) { return; } if e.kind == EK::Fairy { // fairy dust: the heroes around are healed a little all the time for o in self.ents.iter_mut() { if o.alive && o.active_hero() && o.hp < o.max_hp && (o.x - e.x).powi(2) + (o.y - e.y).powi(2) < 9.0 { o.hp = (o.hp + 6.0 * dt).min(o.max_hp); } } if self.vrng.f() < 5.0 * dt { let vx = self.vrng.f() * 0.6 - 0.3; self.part(crate::particles::EMBER, (e.x, e.y, 0.9), (vx, 0.0, 0.2), 1.0, crate::particles::abgr(255, 240, 180), 0.03); } } 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; } } let fort = e.guard || !e.patrol.is_empty(); // a catapult aims at towers first if e.kind == EK::Catapult && !matches!(e.job, Job::AttackTower(_)) { let reach = st(e.kind).range + 1.0; let near = self.towers.iter().filter(|t| !t.hero).map(|t| (t.id, (t.pos().0 - e.x).powi(2) + (t.pos().1 - e.y).powi(2))).filter(|&(_, d2)| d2 < reach * reach).min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)).map(|t| t.0); if let Some(id) = near { e.job = Job::AttackTower(id); e.path.clear(); e.repath = 0.0; } } if !matches!(e.job, Job::Attack(_)) && !matches!((e.kind, e.job), (EK::Catapult, Job::AttackTower(_))) { let radius = if fort { 8.0 } else { 6.5 }; // a ram does not fight creatures let foe = if e.kind == EK::Ram { None } else { self.nearest_enemy(e, radius) }; if let Some(id) = foe { e.job = Job::Attack(id); e.path.clear(); e.repath = 0.0; if fort { self.alarm(e.x, e.y, id); } } else if e.guard && !matches!(e.job, Job::TearBridge(_)) && matches!(e.kind, EK::Knight | EK::Dwarf | EK::Paladin) && self.rng.f() < 0.05 { // knights cut the player's way in: bridges near their post are torn down if let Some(b) = self.bridge_to_tear(e) { e.job = Job::TearBridge(b); e.path.clear(); e.repath = 0.0; e.timer = 0.0; } } else if !matches!(e.job, Job::AttackTower(_) | Job::TearBridge(_)) { // no unit around: towers within reach are attacked let near = self.towers.iter().filter(|t| !t.hero).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::TearBridge(b) = e.job { if self.tear_bridge(e, b, dt) { return; } e.job = Job::None; e.path.clear(); } if let Job::Attack(id) = e.job { // guards and patrols do not follow too far let (gx, gy) = match (e.home, e.patrol.get(e.patrol_k % e.patrol.len().max(1))) { (Some(h), _) => h, (None, Some(&(px, py))) => (px as f32 + 0.5, py as f32 + 0.5), _ => self.gate_center(), }; let far = (e.guard || !e.patrol.is_empty()) && (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.patrol.is_empty() { // patrols walk their route in a loop let k = e.patrol_k % e.patrol.len(); let (px, py) = e.patrol[k]; if (e.x - px as f32 - 0.5).powi(2) + (e.y - py as f32 - 0.5).powi(2) < 0.8 { e.patrol_k = (k + 1) % e.patrol.len(); e.path.clear(); } if e.path.is_empty() && e.repath <= 0.0 { e.repath = 1.0; let (px, py) = e.patrol[e.patrol_k % e.patrol.len()]; match self.map.path_to(e.tile(), idx(px, py)) { Some(p) => e.path = p, None => e.patrol_k = (e.patrol_k + 1) % e.patrol.len(), // blocked: next point } } self.step_path(e, dt); return; } if e.guard { // guards stay at their post (gate or treasure cave) let (gx, gy) = e.home.unwrap_or_else(|| 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; } // a wave sent against a rival master if e.target.map_or(false, |t| t >= 2) && self.march_on(e, dt) { return; } let cur = e.tile(); if e.path.is_empty() && self.map.adj_core(cur) { e.working = true; // the core next to it is the first master's (whoever is being processed in a match) let (cx, cy) = self.core_of_side(0); e.face = sgn(cx as f32 + 1.5 - e.x); if e.cd <= 0.0 { e.cd = st(e.kind).interval; self.damage_master_core(0, 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 let Some((wx, wy)) = e.waypoint { if (e.x - wx as f32 - 0.5).abs() < 2.5 && (e.y - wy as f32 - 0.5).abs() < 2.5 { e.waypoint = None; // broke into the realm: now for the core e.path.clear(); } } if e.path.is_empty() && e.repath <= 0.0 { e.repath = 1.0; let path = match e.waypoint { Some((wx, wy)) => { let goal = idx(wx, wy); let p = self.map.search(cur, self.map.hero_cost(), |i| i == goal, true); if p.is_none() { e.waypoint = None; } p } None => self.map.hero_path_to_core(cur), }; if let Some(p) = path { e.path = p; } } self.step_path(e, dt); } // Wave AI: choose where the next wave breaks into the realm. Candidates are border tiles of // the player's territory on the side facing the hero gate; towers, creatures and reinforced // walls nearby make a place less attractive, as does a long way and recent use. pub fn plan_entries(&mut self) { self.planned_entries.clear(); let (cx, cy) = (self.map.core.0 + 1, self.map.core.1 + 1); let (gx, gy) = (self.map.gate.0 + 1, self.map.gate.1 + 1); let dir = norm((gx - cx) as f32, (gy - cy) as f32); let mut cands = Vec::new(); for i in 0..self.map.t.len() { let t = &self.map.t[i]; if t.owner != self.me || !t.walkable() { continue; } let (x, y) = xy(i); let border = DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner != self.me)); if !border { continue; } let rel = ((x - cx) as f32, (y - cy) as f32); let len = (rel.0 * rel.0 + rel.1 * rel.1).sqrt(); if len < 2.0 || (rel.0 * dir.0 + rel.1 * dir.1) / len < 0.0 { continue; } cands.push((x, y)); } if cands.is_empty() { return; } let step = (cands.len() / 300).max(1); let mut scored: Vec<((i32, i32), f32)> = Vec::new(); for &(x, y) in cands.iter().step_by(step) { let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5); let near = |px: f32, py: f32, r: f32| (px - fx).powi(2) + (py - fy).powi(2) < r * r; let towers: u32 = self.towers.iter().filter(|t| near(t.pos().0, t.pos().1, 6.0)).map(|t| t.level).sum(); let creatures = self.ents.iter().filter(|e| e.alive && e.creature() && near(e.x, e.y, 6.0)).count(); let mut reinf = 0; for dy in -2..=2 { for dx in -2..=2 { if self.map.get(x + dx, y + dy).map_or(false, |t| t.reinforced) { reinf += 1; } } } let dist = (((x - gx).pow(2) + (y - gy).pow(2)) as f32).sqrt(); let recent: f32 = self.entry_history.iter().rev().take(2).filter(|&&(hx, hy)| ((hx - x).pow(2) + (hy - y).pow(2)) < 100).count() as f32 * -60.0; let score = -dist * 0.8 - 25.0 * towers as f32 - 12.0 * creatures as f32 - 6.0 * reinf as f32 + recent + self.rng.f() * 25.0; scored.push(((x, y), score)); } scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); let first = scored[0].0; self.planned_entries.push(first); // second entry point at least 10 tiles away (used by waves of 6+ heroes) if let Some(&(second, _)) = scored.iter().find(|((x, y), _)| (x - first.0).pow(2) + (y - first.1).pow(2) >= 100) { self.planned_entries.push(second); } } // compass direction of a tile as seen from the core (language key) pub fn direction_key(&self, (x, y): (i32, i32)) -> &'static str { let (cx, cy) = (self.map.core.0 + 1, self.map.core.1 + 1); let a = ((y - cy) as f32).atan2((x - cx) as f32).to_degrees(); // 0 = east, 90 = south (map y grows downwards) let k = (((a + 360.0 + 22.5) % 360.0) / 45.0) as usize; ["dir.e", "dir.se", "dir.s", "dir.sw", "dir.w", "dir.nw", "dir.n", "dir.ne"][k % 8] } // ---------------------------------------------------------------- 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)]; // a master sets and clears only his own marks let side = self.me.side().min(7); if t.diggable() && (!t.marked || t.mark_by == side) { t.marked = on; t.mark_by = side; } } } } } 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 * self.talents.room_cost()).round(); 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 == self.me { 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.count(crate::stats::ROOMS, built as f32); 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 i = idx(x, y); if self.remove_trap_at(i) { continue; } if let Some(k) = self.towers.iter().position(|t| t.tile == i && !t.hero) { refund += self.towers[k].invested() as f32 * 0.5; if self.towers[k].kind == TowerKind::Rune && !self.towers[k].rune_final { // the base tower comes back once: in the next mission (free play: at once) if self.mission.is_some() { self.rune_next += 1; self.msg("msg.rune_next", vec![]); } else { self.rune_final += 1; self.msg("msg.rune_back", vec![]); } } self.towers.remove(k); self.map.t[i].kind = Kind::Floor; continue; } let t = &mut self.map.t[i]; if t.kind == Kind::Steel { t.kind = Kind::Earth; t.reinforced = true; t.hp = 12.0; refund += STEEL_COST[1] as f32 * 0.5; continue; } if t.owner == self.me && t.bridge() { let lava = t.river() == Some(Kind::Lava); let cost = if t.kind == Kind::Drawbridge { DRAWBRIDGE_COST[lava as usize] } else { BRIDGE_COST[lava as usize] }; t.kind = if lava { Kind::Lava } else { Kind::Water }; t.owner = Owner::None; t.reserved = 0; t.res = 0; refund += cost as f32 * 0.5; self.ground_lost(i); continue; } let t = &mut self.map.t[i]; if t.owner == self.me { 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 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.hero { self.hint(16, "hint.tower_where"); self.sfx_ui(Sfx::Error); return; } 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 != self.me { 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; } // rune forge towers: one build per opened chest (fresh plans first) let mut rune_final = false; if kind == TowerKind::Rune { if self.rune_free > 0 { self.rune_free -= 1; } else if self.rune_final > 0 { self.rune_final -= 1; rune_final = true; } else { self.hint(24, "hint.rune_none"); 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, hero: false, rune_final }; let (px, py) = tower.pos(); self.towers.push(tower); self.count(crate::stats::TOWERS, 1.0); // 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); if t.hero { self.towers_razed += 1; self.msg("msg.enemy_tower_destroyed", vec![format!("@{}", tower_name(t.kind))]); } else { self.msg("msg.tower_destroyed", vec![format!("@{}", tower_name(t.kind))]); } } // a knight tower near the unit (for the player's creatures) fn enemy_tower_near(&self, x: f32, y: f32, radius: f32) -> Option { self.towers .iter() .filter(|t| t.hero) .map(|t| (t.id, (t.pos().0 - x).powi(2) + (t.pos().1 - y).powi(2))) .filter(|&(_, d)| d < radius * radius) .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) .map(|(id, _)| id) } // 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 -= if e.shield > 0.0 { dmg * 0.5 } else { dmg }; e.flash = 0.15; if e.hp <= 0.0 && e.alive { self.kill(j); } } fn tower_target(&self, t: &Tower) -> Option { 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() { // knight towers shoot the player's units, the player's towers shoot heroes // knight towers shoot everybody but the humans, the player's towers everybody else if !e.alive || e.held || !self.tower_enemy(t, e) || e.invis > 0.0 { 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 } // an enemy tower or the gate of the order within reach: (x, y, height, tower id or None = gate) fn tower_structure_target(&self, t: &Tower) -> Option<(f32, f32, f32, Option)> { let (px, py) = t.pos(); let r = t.range(); let minr = tower_stats(t.kind).min_range; let in_reach = |x: f32, y: f32| { let d = (x - px).powi(2) + (y - py).powi(2); d <= r * r && d >= minr * minr }; let mut best: Option<(f32, (f32, f32, f32, Option))> = None; for o in self.towers.iter().filter(|o| o.hero != t.hero) { let (ox, oy) = o.pos(); if in_reach(ox, oy) && self.los_to_tile(px, py, o.tile) { let d = (ox - px).powi(2) + (oy - py).powi(2); if best.map_or(true, |b| d < b.0) { best = Some((d, (ox, oy, o.top() * 0.6, Some(o.id)))); } } } if best.is_none() && !t.hero && self.has_gate() && self.gate_hp > 0.0 { // the nearest tile of the gate counts let (gx, gy) = self.map.gate; let (nx, ny) = ((px - 0.5).clamp(gx as f32, gx as f32 + 2.0) + 0.5, (py - 0.5).clamp(gy as f32, gy as f32 + 2.0) + 0.5); if in_reach(nx, ny) && self.los_to_tile(px, py, idx(nx as i32, ny as i32)) { best = Some((0.0, (nx, ny, 1.0, None))); } } best.map(|b| b.1) } // line of sight from a tower to a target tile: walls and other towers block, // the tower's own tile and the target tile do not fn los_to_tile(&self, x0: f32, y0: f32, target: usize) -> bool { let start = idx(x0 as i32, y0 as i32); let (tx, ty) = xy(target); let (x1, y1) = (tx as f32 + 0.5, ty as f32 + 0.5); let n = (((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt() * 3.0).ceil().max(1.0) as i32; for k in 1..n { let f = k as f32 / n as f32; let i = idx((x0 + (x1 - x0) * f) as i32, (y0 + (y1 - y0) * f) as i32); if i == start || i == target { continue; } let tl = &self.map.t[i]; if tl.solid() || tl.kind == Kind::Tower { return false; } } true } fn tower_fire_at_structure(&mut self, k: usize, (ex, ey, eh, target): (f32, f32, f32, Option)) { let t = self.towers[k].clone(); let s = tower_stats(t.kind); if s.mana > 0.0 && !t.hero && self.mana < s.mana { self.towers[k].cd = 1.0; return; } if !t.hero { self.mana -= s.mana; } let (px, py) = t.pos(); let aim = (ey - py).atan2(ex - px); let tw = &mut self.towers[k]; tw.cd = s.interval; tw.flash = 0.15; tw.aim = aim; let (mx, my, mh) = match t.kind { TowerKind::Arrow => (px, py, t.top() + 0.25), TowerKind::Cannon => (px + aim.cos() * 0.55, py + aim.sin() * 0.55, t.top() + 0.2), TowerKind::Magic => (px, py, t.top() + 0.45), TowerKind::Rune => (px, py, t.top() + 0.55), }; let col = match t.kind { TowerKind::Arrow => 0xffd0e0f0, TowerKind::Cannon => 0xff303030, TowerKind::Magic => 0xffff60d0, TowerKind::Rune => 0xff40c0ff, }; self.shot(mx, my, mh, ex, ey, eh, 0.3, col); self.style_last_fx(match t.kind { TowerKind::Arrow => crate::particles::ST_ARROW, TowerKind::Cannon => crate::particles::ST_CANNON, TowerKind::Magic => crate::particles::ST_MAGIC, TowerKind::Rune => crate::particles::ST_RUNE, }); self.sfx(match t.kind { TowerKind::Arrow => Sfx::Shoot, TowerKind::Cannon => Sfx::Cannon, TowerKind::Magic | TowerKind::Rune => Sfx::Magic, }, px, py); let dmg = t.dmg(); match target { Some(id) => { if let Some(o) = self.towers.iter().position(|o| o.id == id) { self.towers[o].hp -= dmg; if self.towers[o].hp <= 0.0 { self.destroy_tower(o); } } } None => { self.gate_hp -= dmg; self.gate_hit_t = self.time; self.sfx(Sfx::GateHit, ex, ey); if self.mission.as_ref().map_or(true, |m| m.gate_defenders) { self.check_gate_stage(); } } } } pub(crate) 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 { // no unit in reach: enemy towers and the enemy gate are shot at if let Some(target) = self.tower_structure_target(&t) { self.tower_fire_at_structure(k, target); } continue; }; let s = tower_stats(t.kind); if s.mana > 0.0 && !t.hero && self.mana < s.mana { self.hint(17, "hint.tower_mana"); self.towers[k].cd = 1.0; continue; } if !t.hero { 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); let aim = (ey - py).atan2(ex - px); self.towers[k].aim = aim; // where the shot leaves the tower (see the tower models in render3d.rs) let (mx, my, mh) = match t.kind { TowerKind::Arrow => (px, py, t.top() + 0.25), TowerKind::Cannon => (px + aim.cos() * 0.55, py + aim.sin() * 0.55, t.top() + 0.2), TowerKind::Magic => (px, py, t.top() + 0.45), TowerKind::Rune => (px, py, t.top() + 0.55), }; 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); // what the eye sees: past the target the spent arrow comes down at a random // distance (not all on a circle at the end of the range); the damage below // still reaches along the whole range let to_target = ((ex - px).powi(2) + (ey - py).powi(2)).sqrt(); let reach = to_target + 0.8 + self.vrng.f() * (t.range() + 1.5 - to_target - 0.8).max(0.5); let (endx, endy, wall) = self.arrow_end(px, py, dir.0, dir.1, reach); self.shot(mx, my, mh, endx, endy, 0.45, 0.3, 0xffd0e0f0); self.style_last_fx(if wall { crate::particles::ST_ARROW_WALL } else { crate::particles::ST_ARROW_FALL }); 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.active_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.shot(mx, my, mh, ex, ey, 0.4, 0.35, 0xff303030); self.style_last_fx(crate::particles::ST_CANNON); 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.active_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) }; self.knock(o, away.0 * 0.7, away.1 * 0.7); if self.ents[o].alive { self.tower_damage(o, d); } } } TowerKind::Magic => { // sure hit, slows, jumps to a second hero self.shot(mx, my, mh, ex, ey, 0.45, 0.2, 0xffff60d0); self.style_last_fx(crate::particles::ST_MAGIC); 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.active_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); } } } TowerKind::Rune => { // a rune bolt that jumps on to up to three more enemies nearby self.shot(mx, my, mh, ex, ey, 0.45, 0.25, 0xff40c0ff); self.style_last_fx(crate::particles::ST_RUNE); self.fx(2, ex, ey, 0.0, 0.0, 0.35, 0xffffc040); self.sfx(Sfx::Magic, px, py); let mut hit = vec![j]; let (mut lx, mut ly) = (ex, ey); self.tower_damage(j, dmg); for n in 0..3 { let next = self .ents .iter() .enumerate() .filter(|(o, e)| !hit.contains(o) && e.alive && !e.held && self.tower_enemy(&t, e) && (e.x - lx).powi(2) + (e.y - ly).powi(2) < 3.5 * 3.5) .min_by(|a, b| { let da = (a.1.x - lx).powi(2) + (a.1.y - ly).powi(2); let db = (b.1.x - lx).powi(2) + (b.1.y - ly).powi(2); da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal) }) .map(|(o, _)| o); let Some(o) = next else { break }; let (ox, oy) = (self.ents[o].x, self.ents[o].y); self.fx(0, lx, ly, ox, oy, 0.25, 0xff40c0ff); hit.push(o); if self.ents[o].alive { self.tower_damage(o, dmg * (0.75 - 0.1 * n as f32)); } lx = ox; ly = oy; } } } } } // a unit the tower shoots at fn tower_enemy(&self, t: &Tower, e: &Ent) -> bool { let target = if t.hero { e.side() != 1 } else if self.coop { e.side() == 1 } else if !self.masters.is_empty() { // a match: a tower belongs to the master whose ground it stands on and spares his allies let owner = self.map.t.get(t.tile).map_or(0, |tl| crate::netplay::side_of_owner(tl.owner)); !self.allied(owner, e.side()) } else { e.side() != 0 }; e.captive == 0 && !e.chained && target } // a hero attacks a tower; false = tower gone or unreachable pub(crate) 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 hit_h = self.towers[k].top() * 0.6; 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) * crate::siege::tower_breaker(e.kind); if s.range > 1.5 { self.shot(e.x, e.y, 0.45, tx, ty, hit_h, 0.25, 0xff70b0d0); self.style_last_fx(if e.kind == EK::Archer { crate::particles::ST_ARROW } else { crate::particles::ST_PLAIN }); } else { self.fx_at(2, tx, ty, hit_h, 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) { // already next to the tower: step to the middle of the tile (in reach) Some(p) if p.is_empty() => e.path = vec![e.tile()], Some(p) => e.path = p, None => return false, } } self.step_path(e, dt); true } fn on_own_ground(&self, x: f32, y: f32) -> bool { self.map.get(x as i32, y as i32).map_or(false, |t| t.owner == self.me) } // Left click with the hand: own units anywhere, unconscious heroes only on own ground pub fn pickup(&mut self, target: Option) -> bool { if self.hand.len() >= 8 { self.hint(8, "hint.hand_full"); return false; } let Some(j) = target.filter(|&j| j < self.ents.len() && self.ents[j].alive && !self.ents[j].held) else { return false }; if self.buy_from_trader(j) { return false; } let e = &self.ents[j]; if e.rival() || e.dwarf() { return false; // the rivals' creatures and the dwarves cannot be picked up } if e.hero { if e.captive != 1 && e.captive != 3 { return false; } if !self.on_own_ground(e.x, e.y) { self.hint(18, "hint.pickup_prisoner_where"); self.sfx_ui(Sfx::Error); return false; } } let id = self.ents[j].id; let me = id + 1; self.unreserve(me); if self.ents[j].captive == 3 { // taken out of its cell for t in self.map.t.iter_mut().filter(|t| t.bed == me && matches!(t.kind, Kind::Torture | Kind::Prison)) { t.bed = 0; } } for o in self.ents.iter_mut() { if o.carrier == id && o.captive != 0 { o.captive = 1; o.carrier = 0; } } let e = &mut self.ents[j]; e.held = true; e.carrier = 0; e.job = Job::None; e.path.clear(); self.hand.push(id); self.sfx_ui(Sfx::Pickup); true } // Right click: drop the last picked unit on own walkable ground. Prisoners dropped on a // free torture place are locked up at once. 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 i = idx(x, y); let t = self.map.t[i]; if !t.walkable() || t.owner != self.me { self.hint(9, "hint.drop_where"); self.sfx_ui(Sfx::Error); return false; } self.hand.pop(); let imprison = matches!(t.kind, Kind::Torture | Kind::Prison) && t.bed == 0; // the sacrifice altar takes creatures if t.kind == Kind::Altar { if let Some(k) = self.ents.iter().position(|e| e.id == id && !e.hero) { self.ents[k].held = false; self.ents[k].x = wx; self.ents[k].y = wy; self.sacrifice(k); return true; } } if let Some(k) = self.ents.iter().position(|e| e.id == id) { let e = &mut self.ents[k]; e.held = false; e.x = wx; e.y = wy; e.job = Job::None; e.think = 0.0; e.path.clear(); if e.hero { e.carrier = 0; e.torture_t = 0.0; if imprison { e.captive = 3; e.hp = e.max_hp; e.x = x as f32 + 0.5; e.y = y as f32 + 0.5; self.map.t[i].bed = id + 1; self.sfx(Sfx::Capture, wx, wy); } else { e.captive = 1; } } } self.fx(3, wx, wy, 0.0, 0.0, 0.4, 0xff707090); self.sfx(Sfx::Drop, wx, wy); true } // Shift + left click: backhand for own units (speed-up), fist for heroes on own ground. // Returns 1 = backhand, 2 = punch, 0 = nothing. pub fn slap(&mut self, target: Option) -> u8 { let Some(j) = target.filter(|&j| j < self.ents.len() && self.ents[j].alive && !self.ents[j].held) else { return 0 }; let (x, y) = (self.ents[j].x, self.ents[j].y); if self.ents[j].dwarf() { return 0; } if !self.ents[j].hero && !self.ents[j].rival() { let e = &mut self.ents[j]; e.boost = 15.0; e.hp = (e.hp - e.max_hp * 0.05).max(1.0); e.happy -= 0.04; e.flash = 0.15; if matches!(e.job, Job::Sleep(_)) { e.job = Job::None; // woken up } self.fx(2, x, y, 0.0, 0.0, 0.3, 0xffffffff); self.sfx(Sfx::Slap, x, y); self.script_event(crate::mscript::ScriptEvent::Slap); self.count(crate::stats::SLAPS, 1.0); return 1; } if self.ents[j].captive != 0 { return 0; } if !self.on_own_ground(x, y) { self.hint(19, "hint.punch_where"); self.sfx_ui(Sfx::Error); return 0; } if self.time < self.punch_ready { return 0; } self.punch_ready = self.time + 0.5; self.ents[j].stun = 1.0; // the punch throws the hero back (into lava = burnt) let back = -self.ents[j].face; self.knock(j, back * 0.8, 0.0); if !self.ents[j].alive { self.fx(2, x, y, 0.0, 0.0, 0.35, 0xff40e0ff); self.sfx(Sfx::Punch, x, y); return 2; } self.fx(2, x, y, 0.0, 0.0, 0.35, 0xff40e0ff); self.fx(7, x, y, 0.0, 0.0, 0.4, 0xff40e0ff); self.sfx(Sfx::Punch, x, y); self.tower_damage(j, 15.0); 2 } pub fn lightning(&mut self, wx: f32, wy: f32) { let cost = 50.0 * self.talents.spell_cost(); if self.mana < cost { self.msg("msg.no_mana_bolt", vec![]); self.sfx_ui(Sfx::Error); return; } self.mana -= cost; self.count(crate::stats::SPELLS, 1.0); self.sfx(Sfx::Lightning, wx, wy); self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa); self.ignite_at(wx, wy); let mut kills = Vec::new(); for (j, e) in self.ents.iter_mut().enumerate() { if e.alive && !e.held && e.active_hero() && !crate::rogues::spell_proof(e) && (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) { let cost = 60.0 * self.talents.spell_cost(); if self.mana < cost { self.msg("msg.no_mana_heal", vec![]); self.sfx_ui(Sfx::Error); return; } self.mana -= cost; self.count(crate::stats::SPELLS, 1.0); 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 != self.me { 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); } } // ------------------------------------------------------------------ defence towers #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum TowerKind { Arrow, Cannon, Magic, Rune, // dwarven rune forge tower: from a chest of a dwarf cave, built for free } 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 }, TowerKind::Rune => TowerStats { cost: [0, 800, 1200], hp: 1400.0, dmg: 55.0, range: 10.0, min_range: 0.0, interval: 1.3, mana: 0.0 }, } } pub fn tower_name(k: TowerKind) -> &'static str { match k { TowerKind::Arrow => "tower.arrow", TowerKind::Cannon => "tower.cannon", TowerKind::Magic => "tower.magic", TowerKind::Rune => "tower.rune", } } #[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, #[serde(default)] pub hero: bool, // tower of a knight fortress: shoots the player's units #[serde(default)] pub rune_final: bool, // rune forge tower built from a refund: selling it gives nothing back } 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) } // height of the tower body (3D units); grows with the level pub fn top(&self) -> f32 { match self.kind { TowerKind::Arrow => 1.0 + 0.3 * (self.level - 1) as f32, TowerKind::Cannon => 0.6 + 0.15 * (self.level - 1) as f32, TowerKind::Magic => 1.2 + 0.3 * (self.level - 1) as f32, TowerKind::Rune => 1.4 + 0.25 * (self.level - 1) as f32, } } // 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 { Job::None if !e.patrol.is_empty() => "job.patrol", Job::None if e.guard => "job.guard", 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::Forge(_) => "job.forge", Job::Brawl(_) => "job.brawl", Job::Guard(_) => "job.guard_post", Job::Spar(_) => "job.spar", Job::Install(_) => "job.install", Job::Study(_) => "job.study", Job::Attack(_) => "job.fight", Job::TearBridge(_) => "job.tear_bridge", Job::Rest(_) => "job.rest", Job::Torment(_) => "job.torment", Job::Register => "job.register", Job::MakeBed(_) => "job.make_bed", Job::FetchWage(_) => "job.fetch_wage", Job::Help(_) => "job.help", Job::AttackCore => { if e.working { "job.core_attack" } else { "job.core_march" } } Job::Leave => "job.leave", Job::Reinforce(_) => "job.reinforce", Job::Repair(_) => "job.repair", Job::MendBridge(_) => "job.mend_bridge", Job::RaiseWall(_) => "job.raise_wall", Job::March(_) => "job.march", Job::Carry(_) => "job.carry", Job::Pray(_) => "job.pray", Job::HealCore => "job.heal_core", Job::AttackTower(_) => "job.attack_tower", 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, 64, 64); let (cx, cy) = g.map.core; let (px, py) = g.map.portals[0]; // 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.portals_owned() > 0 && 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; } } } } // a vertical river right next to the start area (east of it) fn river_game(lava: bool) -> (Game, i32) { let mut g = Game::new(7, 64, 64); let (cx, cy) = g.map.core; let rx = cx + 6; // the start area reaches to cx + 5 for y in cy - 6..=cy + 8 { for x in rx..rx + 3 { let t = &mut g.map.t[idx(x, y)]; t.kind = if lava { Kind::Lava } else { Kind::Water }; t.owner = Owner::None; } } (g, rx) } #[test] fn bridges_grow_from_own_ground() { let (mut g, rx) = river_game(false); let (_, cy) = g.map.core; g.gold = 1000.0; // not touching own ground: nothing happens g.build_bridges(rx + 1, cy, rx + 2, cy); assert_eq!(g.map.t[idx(rx + 1, cy)].kind, Kind::Water); // from the bank across the river: three tiles at 30 gold g.build_bridges(rx - 1, cy, rx + 2, cy); for x in rx..rx + 3 { let t = g.map.t[idx(x, cy)]; assert_eq!(t.kind, Kind::Bridge); assert_eq!(t.owner, Owner::Player); assert!(t.walkable()); } assert_eq!(g.gold, 1000.0 - 3.0 * BRIDGE_COST[0] as f32); // the far bank can be reached on foot now let (cx, _) = g.map.core; assert!(g.map.path_to(idx(cx + 4, cy), idx(rx + 3, cy)).is_some() || !g.map.walk(rx + 3, cy)); // selling a bridge tile: units on it swim back to a bank, half the cost is refunded let id = g.spawn(EK::Kobold, false, rx as f32 + 1.5, cy as f32 + 0.5, 1); g.sell_rect(rx + 1, cy, rx + 1, cy); assert_eq!(g.map.t[idx(rx + 1, cy)].kind, Kind::Water); let e = g.ents.iter().find(|e| e.id == id).unwrap(); assert!(e.alive && g.map.walk(e.x as i32, e.y as i32)); } #[test] fn a_match_gives_every_master_his_core_and_his_ground() { let mut g = Game::new(7, 96, 128); g.init_fog(); assert!(g.start_masters(&[0, 2, 3, 4], 2), "four masters, and I am the green one"); assert_eq!(g.local_side, 2); assert_eq!(g.masters.len(), 3); for side in Owner::MASTER_SIDES { let owner = Owner::of_side(side); assert!(g.is_master(side)); let cores = g.map.t.iter().filter(|t| t.kind == Kind::Core && t.owner == owner).count(); assert_eq!(cores, 9, "master {side} has his soul core"); let ground = g.map.t.iter().filter(|t| t.kind == Kind::Floor && t.owner == owner).count(); assert!(ground >= 40, "master {side} has a starting area of his own"); g.switch_to(side); assert_eq!(g.core_hp, CORE_HP, "every core starts intact"); assert_eq!(g.gold, 1500.0, "and every master with the same gold"); } g.switch_to(2); let green = g.map.core; g.switch_to(4); assert_ne!(g.map.core, green, "the masters live apart"); for _ in 0..50 { g.update(STEP); } assert_eq!(g.sim_steps, 50, "every master is simulated"); assert_eq!(g.me_side, 2, "the saddle goes back to the master who plays here"); } #[test] fn lava_burns_and_needs_stone_bridges() { let (mut g, rx) = river_game(true); let (_, cy) = g.map.core; g.gold = 1000.0; g.build_bridges(rx - 1, cy, rx, cy); assert_eq!(g.map.t[idx(rx, cy)].kind, Kind::LavaBridge); assert_eq!(g.gold, 1000.0 - BRIDGE_COST[1] as f32); // a hero pushed off the bridge into the lava burns, the bounty is paid let id = g.spawn(EK::Knight, true, rx as f32 + 0.5, cy as f32 + 0.5, 1); let j = g.ents.iter().position(|e| e.id == id).unwrap(); let gold = g.gold; g.knock(j, 0.0, 1.0); assert!(!g.ents[j].alive); assert!(g.gold > gold); // removing the stone bridge under a unit burns it as well let id = g.spawn(EK::Kobold, false, rx as f32 + 0.5, cy as f32 + 0.5, 1); g.sell_rect(rx, cy, rx, cy); assert!(!g.ents.iter().any(|e| e.id == id && e.alive)); // heroes cannot path through lava assert_eq!(g.map.hero_cost()(idx(rx + 1, cy)), None); } fn treasury_total(g: &Game) -> f32 { g.reserve + g.map.t.iter().filter(|t| t.kind == Kind::Treasury && t.owner == Owner::Player).map(|t| t.gold as f32).sum::() } #[test] fn treasury_shows_its_treasures() { let mut g = Game::new(3, 64, 64); g.update(STEP); // the starting gold lies in the treasury ring assert!((treasury_total(&g) - g.gold).abs() < 2.0); assert_eq!(g.reserve, 0.0); // rubies fill their own tiles g.add_res(1200.0, 2); g.update(STEP); let rubies: i32 = g.map.t.iter().filter(|t| t.kind == Kind::Treasury && t.owner == Owner::Player && t.res == 2).map(|t| t.gold).sum(); assert_eq!(rubies, 1200); assert!(g.map.t.iter().all(|t| t.gold <= TREASURY_TILE as i32 || t.vein().is_some())); // spending empties tiles (randomly), the total always matches the gold counter g.gold -= 1700.0; g.update(STEP); assert!((treasury_total(&g) - g.gold).abs() < 2.0); // more than the treasury holds: the core keeps the rest g.gold = g.gold_cap(); g.update(STEP); assert!((treasury_total(&g) - g.gold).abs() < 2.0); // (tiles partly filled with rubies take no gold, so the core may hold a little more) let gem_room: f32 = g.map.t.iter().filter(|t| t.kind == Kind::Treasury && t.owner == Owner::Player && t.res != 0).map(|t| TREASURY_TILE - t.gold as f32).sum(); assert!(g.reserve > 0.0 && g.reserve <= CORE_STORE + gem_room + 1.0, "reserve {} gold {} cap {}", g.reserve, g.gold, g.gold_cap()); } #[test] fn treasure_lands_in_the_nearest_treasury() { let mut g = Game::new(3, 64, 64); let (cx, cy) = g.map.core; // a second treasury far from the core ring for y in cy - 9..=cy - 7 { for x in cx + 10..=cx + 12 { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Treasury; t.owner = Owner::Player; t.gold = 0; } } g.recount(); g.update(STEP); let far = |g: &Game| -> i32 { (cy - 9..=cy - 7).flat_map(|y| (cx + 10..=cx + 12).map(move |x| (x, y))).map(|(x, y)| g.map.t[idx(x, y)].gold).sum() }; let before = far(&g); let at = idx(cx + 11, cy - 8); g.add_res_at(900.0, 2, Some(at)); g.update(STEP); assert_eq!(far(&g) - before, 900, "the rubies lie where they were delivered"); } #[test] fn training_is_billed_on_payday() { let mut g = Game::new(3, 64, 64); let (cx, cy) = g.map.core; g.spawn(EK::Kobold, false, (cx + 4) as f32 + 0.5, (cy + 4) as f32 + 0.5, 1); g.training_bill = 50.0; let cost = g.next_payday_cost(); assert_eq!(cost, 50.0 + st(EK::Kobold).wage as f32); let gold = g.gold; g.payday = PAYDAY_WARN + 0.05; g.update(0.1); assert!(g.payday_warned); g.payday = 0.01; g.update(0.1); // the training bill is paid at once, the wage when the kobold fetches it assert_eq!(g.training_bill, 0.0); assert!((gold - g.gold - 50.0).abs() < 1.0); let mut fetched = false; for _ in 0..(70 * 20) { g.update(0.05); fetched |= g.ents.iter().any(|e| matches!(e.job, Job::FetchWage(_))); } assert!(fetched, "the kobold walks to the treasury"); assert!((gold - g.gold - cost).abs() < 1.0, "paid {}", gold - g.gold); assert!(g.ents.iter().all(|e| e.pay_due == 0.0)); } #[test] fn new_creatures_report_and_set_up_their_bed() { let mut g = Game::new(3, 64, 64); let (cx, cy) = g.map.core; g.gold = 3000.0; g.build_rect(1, cx - 3, cy - 3, cx - 2, cy + 1); g.build_rect(2, cx + 4, cy - 3, cx + 5, cy + 1); // arrives like from a portal (the test map's portal is not connected yet) let id = g.spawn(EK::Kobold, false, (cx + 4) as f32 + 0.5, (cy + 4) as f32 + 0.5, 1); if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) { e.settled = false; e.job = Job::Register; e.timer = 2.0; } let mut made_bed = false; for _ in 0..(90 * 20) { g.update(0.05); let e = g.ents.iter().find(|e| e.id == id).unwrap(); made_bed |= matches!(e.job, Job::MakeBed(_)) && e.path.is_empty(); if e.settled { break; } } let e = g.ents.iter().find(|e| e.id == id).unwrap(); assert!(made_bed && e.settled); assert_eq!(g.map.t[e.bed.unwrap()].kind, Kind::Lair); } // a fortress east of the start area, joined to it by a claimed corridor fn fort_game(spec: impl Fn(i32, i32) -> crate::fortress::FortSpec) -> (Game, crate::fortress::FortSpec) { let mut g = Game::new(11, 96, 64); let (cx, cy) = g.map.core; let f = spec(cx + 14, cy - 6); g.build_fortress(&f); for x in cx + 5..f.x { for y in cy..=cy + 2 { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Floor; t.owner = Owner::Player; } } (g, f) } fn spec(x: i32, y: i32) -> crate::fortress::FortSpec { crate::fortress::FortSpec { x, y, w: 15, h: 15, doors: vec![3], towers: vec![], tower_level: 1, guards: vec![], patrols: vec![], vaults: 0, vault_value: 0, shard: false, heart: false } } #[test] fn knight_towers_and_creatures_fight() { let (mut g, f) = fort_game(|x, y| crate::fortress::FortSpec { towers: vec![TowerKind::Arrow], ..spec(x, y) }); assert_eq!(g.towers.len(), 1); assert!(g.towers[0].hero); let (tx, ty) = g.towers[0].pos(); let id = g.spawn(EK::Ogre, false, tx + 3.0, ty + 2.0, 4); let hp0 = g.ents.iter().find(|e| e.id == id).unwrap().hp; let thp0 = g.towers[0].hp; for _ in 0..200 { g.update(0.05); } let hurt = g.ents.iter().find(|e| e.id == id).map_or(true, |e| e.hp < hp0); let tower_hurt = g.towers.first().map_or(true, |t| t.hp < thp0); assert!(hurt, "the knight tower shoots the creature"); assert!(tower_hurt, "the creature attacks the knight tower"); let _ = f; } #[test] fn towers_fight_towers_and_the_gate() { // a tower of the player in the courtyard of a fortress, no units around let (mut g, _) = fort_game(|x, y| crate::fortress::FortSpec { towers: vec![TowerKind::Arrow], ..spec(x, y) }); g.ents.clear(); let (kx, ky) = xy(g.towers[0].tile); let (px, py) = (kx + 3, ky + 2); g.map.t[idx(px, py)].owner = Owner::Player; g.gold = 5000.0; g.build_tower(TowerKind::Arrow, px, py); assert_eq!(g.towers.len(), 2); let hp: Vec = g.towers.iter().map(|t| t.hp).collect(); for _ in 0..100 { g.update(0.05); } assert!(g.towers.iter().zip(&hp).all(|(t, h)| t.hp < *h), "both towers are hit"); // free game: a tower in reach of the hero gate shoots at the gate let mut g = Game::new(5, 64, 64); g.ents.clear(); let (gx, gy) = g.map.gate; let (tx, ty) = (gx - 2, gy + 1); g.map.t[idx(tx, ty)].kind = Kind::Floor; g.map.t[idx(tx, ty)].owner = Owner::Player; g.gold = 5000.0; g.build_tower(TowerKind::Arrow, tx, ty); for _ in 0..60 { g.update(0.05); } assert!(g.gate_hp < GATE_HP, "the gate is shot at"); } #[test] fn more_lair_room_brings_creatures_faster() { let mut g = Game::new(3, 64, 64); g.counts[1] = 4; let tight = g.lair_factor(); g.counts[1] = 12; let normal = g.lair_factor(); g.counts[1] = 30; let roomy = g.lair_factor(); assert!(tight > normal && normal > roomy, "{} {} {}", tight, normal, roomy); // creatures fill the lair: arrivals slow down again for _ in 0..20 { g.spawn(EK::Kobold, false, 10.0, 10.0, 1); } assert!(g.lair_factor() > roomy); } #[test] fn vaults_are_claimed_and_looted() { let (mut g, _) = fort_game(|x, y| crate::fortress::FortSpec { vaults: 2, vault_value: 1800, ..spec(x, y) }); let gold0 = g.gold; for _ in 0..(240 * 20) { g.update(0.05); if g.loot >= 3000.0 { break; } } println!("loot after claiming: {}", g.loot); assert!(g.loot > 1000.0); assert!(g.gold > gold0); } #[test] fn patrols_walk_and_guards_raise_the_alarm() { let (mut g, f) = fort_game(|x, y| crate::fortress::FortSpec { guards: vec![(EK::Knight, 1); 4], patrols: vec![vec![(EK::Archer, 1)]], ..spec(x, y) }); // no imps around (the patrol would shoot them through the door) g.ents.retain(|e| e.hero); let pj = g.ents.iter().position(|e| !e.patrol.is_empty()).unwrap(); let pid = g.ents[pj].id; let start = (g.ents[pj].x, g.ents[pj].y); let mut far = 0.0f32; for _ in 0..200 { g.update(0.05); let p = g.ents.iter().find(|e| e.id == pid).unwrap(); far = far.max((p.x - start.0).abs() + (p.y - start.1).abs()); } assert!(far > 6.0, "the patrol walks its round"); // an intruder next to one guard: the others join let (hx, hy) = g.ents.iter().find(|e| e.guard).unwrap().home.unwrap(); g.spawn(EK::Ogre, false, hx + 1.0, hy, 8); for _ in 0..20 { g.update(0.05); } let fighting = g.ents.iter().filter(|e| e.guard && matches!(e.job, Job::Attack(_))).count(); assert!(fighting >= 3, "alarm: {} guards fight", fighting); let _ = f; } #[test] fn knights_tear_down_bridges() { let (mut g, f) = fort_game(|x, y| crate::fortress::FortSpec { guards: vec![(EK::Knight, 1)], ..spec(x, y) }); // a bridge of the player in the courtyard, on a small pond let (bx, by) = (f.x + f.w / 2 + 2, f.y + f.h / 2 + 2); for y in by - 1..=by + 1 { g.map.t[idx(bx, y)].kind = Kind::Water; g.map.t[idx(bx, y)].owner = Owner::None; } g.map.t[idx(bx, by)].kind = Kind::Bridge; g.map.t[idx(bx, by)].owner = Owner::Player; // no imps around to mend it (and to be attacked instead) g.ents.retain(|e| e.hero); let mut torn = false; for _ in 0..(90 * 20) { g.update(0.05); if g.map.t[idx(bx, by)].kind == Kind::Water { torn = true; break; } } assert!(torn); } #[test] fn some_imps_always_claim() { // lots of digging work: still at least 10% (min. one) of the imps claim the dug-out floor let mut g = Game::new(11, 64, 64); let (cx, cy) = g.map.core; for _ in 0..8 { let (x, y) = (g.ents[0].x, g.ents[0].y); g.spawn(EK::Worker, false, x, y, 1); } g.mark_rect(cx - 12, cy - 12, cx + 12, cy + 12, true); let (mut samples, mut ok) = (0, 0); for step in 0..(150 * 20) { g.update(0.05); if step % 20 != 0 || step < 200 { continue; } let digging = g.map.t.iter().any(|t| t.marked); let claimable = (0..g.map.t.len()).any(|i| g.claimable(i, u32::MAX)); if digging && claimable { let imps = g.worker_count(); let quota = ((imps as f32 * 0.1).ceil() as usize).max(1); let claiming = g.ents.iter().filter(|e| e.alive && e.kind == EK::Worker && matches!(e.job, Job::Claim(_))).count(); samples += 1; if claiming >= quota { ok += 1; } } } println!("claim quota met in {}/{} samples", ok, samples); assert!(samples > 10); assert!(ok * 10 >= samples * 8); } #[test] fn wall_corners_are_reinforced_too() { let mut g = Game::new(3, 64, 64); let (cx, cy) = g.map.core; // the start area spans cx-3..cx+5 / cy-3..cy+5: its walls are at cx-4 and cy-4 let (x0, y0) = (cx - 4, cy - 4); for k in 1..4 { g.map.t[idx(x0 + k, y0)].reinforced = true; g.map.t[idx(x0, y0 + k)].reinforced = true; } assert!(g.map.t[idx(x0, y0)].kind == Kind::Earth && !g.map.t[idx(x0, y0)].reinforced); g.reinforce_corners(); assert!(g.map.t[idx(x0, y0)].reinforced, "the corner is reinforced"); // nothing spreads into the earth behind the walls g.reinforce_corners(); assert!(!g.map.t[idx(x0 - 1, y0)].reinforced && !g.map.t[idx(x0, y0 - 1)].reinforced && !g.map.t[idx(x0 - 1, y0 - 1)].reinforced); } #[test] fn idle_imps_help_and_speed_up_work() { let mut g = Game::new(3, 64, 64); let (cx, cy) = g.map.core; for _ in 0..6 { g.spawn(EK::Worker, false, cx as f32 + 4.5, cy as f32 + 1.5, 1); } // a single earth tile to dig, next to the start area: one digs, the others help let (dx, dy) = (cx + 6, (cy - 3..=cy + 5).find(|&y| g.map.t[idx(cx + 6, y)].kind == Kind::Earth).expect("earth next to the start area")); // no other work: everything around is claimed already, no walls to reinforce for t in g.map.t.iter_mut() { if t.kind == Kind::Earth { t.reinforced = true; } } g.map.t[idx(dx, dy)].reinforced = false; g.map.t[idx(dx, dy)].marked = true; let mut best = 0.0f32; for _ in 0..200 { g.update(0.05); best = best.max(g.help_speed(idx(dx, dy))); if g.map.t[idx(dx, dy)].kind != Kind::Earth { break; } } println!("best working speed {}", best); assert!(best >= 1.75, "helpers join: {}", best); assert!(best <= 2.0); } #[test] fn imps_set_up_torch_stands_in_big_rooms_on_their_own_ground() { let mut g = Game::new(5, 96, 96); let (cx, cy) = g.map.core; // a big hall of own ground east of the core, and as big a neutral cave south of it let hall = (cx + 8, cy - 7, cx + 22, cy + 7); let cave = (cx - 7, cy + 12, cx + 7, cy + 26); for (x0, y0, x1, y1, own) in [(hall.0, hall.1, hall.2, hall.3, true), (cave.0, cave.1, cave.2, cave.3, false)] { for y in y0..=y1 { for x in x0..=x1 { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Floor; t.owner = if own { g.me } else { Owner::None }; } } } g.torches.clear(); g.place_all_torches(); let inside = |t: &Torch, r: (i32, i32, i32, i32)| { let (x, y) = xy(t.wall); x >= r.0 && x <= r.2 && y >= r.1 && y <= r.3 }; let stands: Vec = g.torches.iter().copied().filter(|t| t.stand).collect(); assert!(stands.iter().any(|t| inside(t, hall)), "the big hall gets torch stands"); assert!(!stands.iter().any(|t| inside(t, cave)), "never on neutral ground"); for t in &stands { assert_eq!(g.map.t[t.wall].owner, g.me, "only on own ground"); let (x, y) = xy(t.wall); assert!((-1..=1).all(|dy| (-1..=1).all(|dx| !g.map.t[idx(x + dx, y + dy)].solid())), "not next to a wall"); } // every tile of the hall's middle is within reach of some torch for y in hall.1 + 2..=hall.3 - 2 { for x in hall.0 + 2..=hall.2 - 2 { let lit = g.torches.iter().any(|t| { let (px, pz) = t.pos(); (px - x as f32 - 0.5).powi(2) + (pz - y as f32 - 0.5).powi(2) <= (STAND_GAP + 1.0).powi(2) }); assert!(lit, "({}, {}) lies in the dark", x, y); } } // ground lost: the stand goes let s0 = stands.iter().find(|t| inside(t, hall)).unwrap().wall; g.map.t[s0].owner = Owner::Hero; for _ in 0..60 { g.update(0.05); } assert!(!g.torches.iter().any(|t| t.stand && t.wall == s0), "a stand on lost ground is removed"); } #[test] fn badly_hurt_creatures_flee_to_the_lair_and_heal() { let mut g = Game::new(5, 64, 64); let (cx, cy) = g.map.core; g.gold = 5000.0; g.build_rect(1, cx - 3, cy - 3, cx - 2, cy + 2); assert!(g.counts[1] >= 8, "a lair"); let id = g.spawn(EK::Kobold, false, cx as f32 + 5.5, cy as f32 + 0.5, 2); let j = g.ents.iter().position(|e| e.id == id).unwrap(); g.ents[j].hp = g.ents[j].max_hp * 0.2; let mut fled = false; for _ in 0..(20 * 6) { g.update(0.05); fled |= matches!(g.ents[j].job, Job::Rest(_)); } assert!(fled, "it fled to rest"); for _ in 0..(20 * 20) { g.update(0.05); } assert!(g.ents[j].hp > g.ents[j].max_hp * 0.5, "and healed: {} of {}", g.ents[j].hp, g.ents[j].max_hp); } #[test] fn flyers_take_every_way_across_rivers() { let (mut g, rx) = river_game(false); let (cx, cy) = g.map.core; // open floor on the far bank with one training tile of the player for y in cy - 2..=cy + 2 { for x in rx + 3..rx + 7 { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Floor; t.owner = Owner::None; } } let room = idx(rx + 4, cy); g.map.t[room].kind = Kind::Training; g.map.t[room].owner = g.me; let owl = g.spawn(EK::Owl, false, (cx + 4) as f32 + 0.5, cy as f32 + 0.5, 1); let kobold = g.spawn(EK::Kobold, false, (cx + 4) as f32 + 0.5, cy as f32 + 0.5, 1); let owl = g.ents.iter().find(|e| e.id == owl).unwrap().clone(); let kobold = g.ents.iter().find(|e| e.id == kobold).unwrap().clone(); // room searches: the owl flies over the river to the training room, the kobold cannot get there let p = g.find_room_tile(&owl, Kind::Training, owl.id + 1, false).expect("the owl finds the room across the river"); assert_eq!(p.last().copied(), Some(room)); assert!(g.find_room_tile(&kobold, Kind::Training, kobold.id + 1, false).map_or(true, |p| p.last() != Some(&room))); // flyers cut corners over water: a diagonal crossing is as short as on open ground let (a, b) = (idx(rx - 1, cy - 1), idx(rx + 3, cy + 1)); let p = g.map.creature_path(true, a, b).expect("flight path"); assert!(p.len() <= 5, "diagonal flight over the river: {} steps", p.len()); assert!(p.iter().any(|&i| g.map.t[i].liquid())); // attacking flyers (the fairy spy) also fly over rivers instead of searching for a bridge assert!(g.map.hero_cost_as(true)(idx(rx + 1, cy)).is_some()); assert_eq!(g.map.hero_cost()(idx(rx + 1, cy)), None); // a locked door stops flyers too g.map.t[idx(rx + 5, cy)].kind = Kind::Door; g.map.t[idx(rx + 5, cy)].res = 1; assert_eq!(g.map.fly_cost()(idx(rx + 5, cy)), None); } #[test] fn flyers_cross_rivers_flee_and_heal() { let (mut g, rx) = river_game(true); let (cx, cy) = g.map.core; // an aerie in the start area g.gold = 5000.0; g.build_rect(7, cx - 3, cy - 3, cx - 2, cy + 2); assert!(g.counts[7] >= 8); // a knight on the far side of the lava: an owl reaches it, a kobold does not let far = (rx + 4, cy); for x in rx + 3..rx + 7 { let t = &mut g.map.t[idx(x, cy)]; t.kind = Kind::Floor; t.owner = Owner::None; } let k = g.spawn(EK::Knight, true, far.0 as f32 + 0.5, far.1 as f32 + 0.5, 1); let owl = g.spawn(EK::Owl, false, (cx + 4) as f32 + 0.5, cy as f32 + 0.5, 3); assert!(g.map.fly_path_to(idx(cx + 4, cy), idx(far.0, far.1)).is_some()); assert!(g.map.path_to(idx(cx + 4, cy), idx(far.0, far.1)).is_none()); let hp0 = g.ents.iter().find(|e| e.id == k).unwrap().hp; for _ in 0..300 { g.update(0.05); } let hit = g.ents.iter().find(|e| e.id == k).map_or(true, |e| e.hp < hp0); assert!(hit, "the owl attacks across the lava"); // a badly hurt flyer flees to the aerie and heals up to 75% g.ents.retain(|e| !e.hero); let j = g.ents.iter().position(|e| e.id == owl).unwrap(); g.ents[j].hp = g.ents[j].max_hp * 0.2; let mut rested = false; for _ in 0..800 { g.update(0.05); let e = g.ents.iter().find(|e| e.id == owl).unwrap(); rested |= matches!(e.job, Job::Rest(_)); if e.hp >= e.max_hp * 0.75 { break; } } let e = g.ents.iter().find(|e| e.id == owl).unwrap(); assert!(rested && e.hp >= e.max_hp * 0.74, "fled and healed: {} / {}", e.hp, e.max_hp); // lava does not burn flyers let j = g.ents.iter().position(|e| e.id == owl).unwrap(); g.ents[j].x = (rx - 1) as f32 + 0.5; g.ents[j].y = cy as f32 + 0.5; g.knock(j, 1.0, 0.0); assert!(g.ents[j].alive); } #[test] fn aerie_attracts_flyers() { let mut g = Game::new(3, 64, 64); g.counts = [16, 0, 9, 0, 6, 4, 0, 16, 0, 0, 0, 0, 0, 0]; let mut seen = std::collections::HashSet::new(); for _ in 0..200 { g.try_portal_for_test(); for e in g.ents.iter().filter(|e| e.creature()) { seen.insert(e.kind); } g.ents.retain(|e| !e.creature()); } assert!(seen.contains(&EK::Bat) && seen.contains(&EK::Gargoyle) && seen.contains(&EK::Owl), "{:?}", seen); assert!(seen.iter().all(|&k| flies(k)), "no lair: only flyers come ({:?})", seen); } #[test] fn steel_walls_hold_heroes_back() { let mut g = Game::new(3, 64, 64); let (cx, cy) = g.map.core; let (wx, wy) = (cx + 6, (cy - 3..=cy + 5).find(|&y| g.map.t[idx(cx + 6, y)].kind == Kind::Earth).unwrap()); g.gold = 1000.0; g.build_steel(wx, wy, wx, wy); assert_eq!(g.map.t[idx(wx, wy)].kind, Kind::Steel); assert_eq!(g.gold, 1000.0 - STEEL_COST[0] as f32); // far from own ground: nothing happens g.build_steel(wx + 5, wy, wx + 5, wy); assert_ne!(g.map.t[idx(wx + 5, wy)].kind, Kind::Steel); // heroes avoid it and dig 20 times slower than through earth assert_eq!(g.map.hero_cost()(idx(wx, wy)), Some(300)); // selling: a reinforced earth wall again, half of the plating back let gold = g.gold; g.sell_rect(wx, wy, wx, wy); assert_eq!(g.map.t[idx(wx, wy)].kind, Kind::Earth); assert!(g.map.t[idx(wx, wy)].reinforced); assert!(g.gold > gold); } #[test] fn imps_reinforce_walls() { let mut g = Game::new(3, 64, 64); for _ in 0..(120 * 20) { g.update(0.05); } let n = g.map.t.iter().filter(|t| t.reinforced).count(); println!("reinforced walls after 2 minutes: {}", n); assert!(n >= 10); // heroes avoid them: the hero path cost through reinforced earth is 10x let (cx, cy) = g.map.core; let w = (cx - 4, cy); if g.map.t[idx(w.0, w.1)].reinforced { 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())); } #[test] fn prisoners_are_tortured_and_resolved() { let mut converted = 0; let mut died = 0; for seed in 1..=40u32 { let mut g = Game::new(seed, 64, 64); g.gold = 5000.0; let (cx, cy) = g.map.core; g.build_rect(5, cx - 3, cy - 3, cx - 2, cy + 1); // torture chamber 2x5 = 5 places... 10 tiles assert!(g.prison_space() >= 2, "space {}", g.prison_space()); let id = g.spawn(EK::Knight, true, (cx + 4) as f32 + 0.5, (cy + 4) as f32 + 0.5, 1); let j = g.ents.iter().position(|e| e.id == id).unwrap(); g.kill(j); assert_eq!(g.ents[j].captive, 1); let mut reached = false; for _ in 0..(200 * 20) { g.update(0.05); match g.ents.iter().find(|e| e.id == id) { Some(e) if e.captive == 3 => reached = true, Some(e) if !e.hero => { converted += 1; break; } None => { died += 1; break; } _ => {} } } assert!(reached, "seed {}: prisoner never reached the torture chamber", seed); } println!("40 prisoners: {} defected, {} died", converted, died); assert_eq!(converted + died, 40); assert!((8..=26).contains(&converted), "defect rate off: {}/40", converted); } #[test] fn jailer_makes_prisoners_defect_more_often() { let mut converted = 0; let mut worked = 0; for seed in 1..=40u32 { let mut g = Game::new(seed, 64, 64); g.gold = 5000.0; let (cx, cy) = g.map.core; g.build_rect(5, cx - 3, cy - 3, cx - 2, cy + 1); g.build_rect(1, cx + 4, cy - 3, cx + 5, cy - 2); // a lair for the jailer let jailer = g.spawn(EK::Jailer, false, (cx - 1) as f32 + 0.5, (cy - 3) as f32 + 0.5, 1); let id = g.spawn(EK::Knight, true, (cx + 4) as f32 + 0.5, (cy + 4) as f32 + 0.5, 1); let j = g.ents.iter().position(|e| e.id == id).unwrap(); g.kill(j); for _ in 0..(200 * 20) { g.update(0.05); if let Some(jl) = g.ents.iter_mut().find(|e| e.id == jailer) { // keep the jailer fresh so that it works jl.hunger = 0.0; jl.energy = 1.0; jl.happy = 1.0; if jl.job == Job::Torment(id) { worked += 1; } } match g.ents.iter().find(|e| e.id == id) { Some(e) if !e.hero => { converted += 1; break; } None => break, _ => {} } } } println!("with a jailer: {} of 40 defected (jailer worked {} ticks)", converted, worked); assert!(worked > 0, "the jailer works on the prisoners"); assert!(converted >= 20, "defections with a jailer: {}/40", converted); } #[test] fn temple_prayer_and_priestess_heals_core() { let mut g = Game::new(9, 64, 64); g.gold = 5000.0; let (cx, cy) = g.map.core; g.build_rect(6, cx - 3, cy - 3, cx - 2, cy + 1); // temple g.build_rect(1, cx + 4, cy - 3, cx + 5, cy - 1); // lair g.build_rect(2, cx + 4, cy + 3, cx + 5, cy + 5); // farm g.spawn(EK::Priestess, false, (cx + 4) as f32 + 0.5, (cy + 2) as f32 + 0.5, 1); for k in 0..3 { g.spawn(EK::Kobold, false, (cx - 1) as f32 + 0.5, (cy + k) as f32 + 0.5, 1); } g.core_hp = 1000.0; let mut prayed = false; for _ in 0..(120 * 20) { g.update(0.05); prayed |= g.ents.iter().any(|e| matches!(e.job, Job::Pray(_)) && e.working); } println!("core after 2 min: {:.0}, prayed: {}", g.core_hp, prayed); assert!(prayed); assert!(g.core_hp > 1300.0); } #[test] fn waves_vary_their_entry_points() { let mut g = Game::new(11, 96, 96); let (cx, cy) = g.map.core; // a bigger realm so there are several possible entry points for y in cy - 8..=cy + 10 { for x in cx - 6..=cx + 14 { if inb(x, y) && g.map.t[idx(x, y)].kind != Kind::Core && g.map.t[idx(x, y)].kind != Kind::Rock { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Floor; t.owner = Owner::Player; } } } let mut seen = Vec::new(); for _ in 0..4 { g.plan_entries(); let e = g.planned_entries[0]; println!("entry {:?} ({})", e, g.direction_key(e)); seen.push(e); g.entry_history.push(e); } // consecutive waves use different places for w in seen.windows(2) { assert!((w[0].0 - w[1].0).pow(2) + (w[0].1 - w[1].1).pow(2) >= 16, "same entry twice: {:?}", w); } // spawning a big wave gives heroes two different waypoints g.warned = true; g.planned_entries.clear(); g.plan_entries(); assert_eq!(g.planned_entries.len(), 2); } #[test] fn core_starts_with_treasury_ring() { let g = Game::new(21, 64, 64); let (cx, cy) = g.map.core; let ring = (cy - 1..=cy + 3).flat_map(|y| (cx - 1..=cx + 3).map(move |x| (x, y))).filter(|&(x, y)| g.map.t[idx(x, y)].kind == Kind::Treasury).count(); assert_eq!(ring, 16); assert_eq!(g.counts[0], 16); } #[test] fn hand_rules() { let mut g = Game::new(4, 64, 64); g.gold = 5000.0; let (cx, cy) = g.map.core; // backhand on an own imp: faster for 15 s, costs a little HP let imp = g.ents.iter().position(|e| e.kind == EK::Worker).unwrap(); let hp = g.ents[imp].hp; assert_eq!(g.slap(Some(imp)), 1); assert_eq!(g.ents[imp].boost, 15.0); assert!(g.ents[imp].hp < hp); // fist: only on own ground let far = g.spawn(EK::Knight, true, 40.5, 40.5, 1); let jf = g.ents.iter().position(|e| e.id == far).unwrap(); if g.map.t[idx(40, 40)].owner != Owner::Player { assert_eq!(g.slap(Some(jf)), 0); } let near = g.spawn(EK::Knight, true, (cx - 2) as f32 + 0.5, (cy - 2) as f32 + 0.5, 1); let jn = g.ents.iter().position(|e| e.id == near).unwrap(); let hp = g.ents[jn].hp; assert_eq!(g.slap(Some(jn)), 2); assert!(g.ents[jn].hp < hp && g.ents[jn].stun > 0.0); assert_eq!(g.slap(Some(jn)), 0, "cooldown"); // prisoners: pick up on own ground, drop onto a torture place = locked up g.build_rect(5, cx - 3, cy - 3, cx - 2, cy + 1); let jn = g.ents.iter().position(|e| e.id == near).unwrap(); g.kill(jn); assert_eq!(g.ents[jn].captive, 1); assert!(g.pickup(Some(jn))); let t = (cx - 3, cy + 1); assert!(g.drop(t.0 as f32 + 0.5, t.1 as f32 + 0.5)); let e = g.ents.iter().find(|e| e.id == near).unwrap(); assert_eq!(e.captive, 3); // own units cannot be dropped outside own ground let imp = g.ents.iter().position(|e| e.kind == EK::Worker).unwrap(); assert!(g.pickup(Some(imp))); assert!(!g.drop(40.5, 40.5) || g.map.t[idx(40, 40)].owner == Owner::Player); } #[test] fn chainbreaker_visits_only_great_dungeons() { let mut g = Game::new(8, 64, 64); g.gold = 9000.0; // conditions not met: no Chainbreaker among many portal visits let (px, py) = g.map.portals[0]; for y in 0..3 { for x in 0..3 { g.map.t[idx(px + x, py + y)].owner = Owner::Player; } } let (cx, cy) = g.map.core; g.build_rect(1, cx + 4, cy - 3, cx + 5, cy + 5); // lair 18 g.build_rect(2, cx - 3, cy - 3, cx - 2, cy + 5); // farm 18 g.recount(); for _ in 0..60 { g.try_portal(); } assert!(!g.ents.iter().any(|e| e.kind == EK::Chainbreaker)); // a veteran creature makes it possible (rooms 16 ring + 36 = 52 tiles, gold 9000) if let Some(e) = g.ents.iter_mut().find(|e| e.creature()) { e.level = 5; } for e in g.ents.iter_mut().filter(|e| e.creature()) { e.alive = false; // free beds } g.ents.retain(|e| e.alive); g.spawn(EK::Kobold, false, cx as f32, cy as f32, 6); let mut seen = false; for _ in 0..400 { g.gold = 9000.0; g.try_portal(); seen |= g.ents.iter().any(|e| e.kind == EK::Chainbreaker); g.ents.retain(|e| !e.creature() || e.kind == EK::Chainbreaker || e.level >= 5); } assert!(seen); assert!(g.ents.iter().filter(|e| e.kind == EK::Chainbreaker).count() <= 1); } #[test] fn the_beds_decide_how_many_creatures_come_but_never_more_than_100() { let mut g = Game::new(9, 96, 96); let (cx, cy) = g.map.core; // a lair of 40 beds: room for 40 let mut n = 0; for y in cy + 4..cy + 20 { for x in cx - 6..cx + 8 { if n < 40 && inb(x, y) { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Lair; t.owner = Owner::Player; n += 1; } } } g.recount(); assert_eq!(g.creature_cap(), 40); assert_eq!(g.free_lair_aerie().0, 40, "more than the old 30"); // a huge lair: the cap is 100 for y in cy + 4..cy + 20 { for x in cx - 10..cx + 12 { if inb(x, y) && g.map.t[idx(x, y)].kind != Kind::Core { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Lair; t.owner = Owner::Player; } } } g.recount(); assert!(g.counts[1] > 100); assert_eq!(g.creature_cap(), MAX_CREATURES); for k in 0..100 { g.spawn(EK::Kobold, false, cx as f32 + 0.5 + (k % 10) as f32, cy as f32 + 5.5 + (k / 10) as f32, 1); } assert_eq!(g.free_lair_aerie(), (0, 0), "no room for a 101st"); } #[test] fn creatures_never_make_their_bed_in_an_allys_lair() { let mut g = Game::new(11, 96, 128); g.coop = true; assert!(g.start_masters(&[0, 2], 0)); let (cx, cy) = g.map.core; for y in cy + 4..cy + 7 { for x in cx - 1..cx + 4 { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Lair; t.owner = Owner::Player; } } g.recount(); let id = g.spawn(EK::Kobold, false, cx as f32 + 1.5, cy as f32 + 5.5, 1); if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) { e.side = 2; e.energy = 0.1; } for _ in 0..(40 * 20) { g.update(0.05); } let e = g.ents.iter().find(|e| e.id == id).expect("still there"); assert!(e.bed.map_or(true, |b| g.map.t[b].owner != Owner::Player), "no bed in the ally's lair"); assert!(!g.map.t.iter().any(|t| t.kind == Kind::Lair && t.bed == id + 1)); } #[test] fn every_master_of_a_coop_mission_has_an_own_portal() { let m = crate::campaign::coop_mission_indexes()[2]; let mut g = crate::campaign::start_mission(m, None); g.coop = true; assert!(g.start_masters(&[0, 2, 3], 0)); for side in [2u8, 3] { let owner = Owner::of_side(side); let core = g.masters.iter().find(|m| m.side == side).unwrap().core; let k = (0..g.map.portals.len()).find(|&k| g.map.portal_owner(k) == owner && g.map.portals[k] == (core.0 + 6, core.1)).expect("a portal of his own"); let (px, py) = g.map.portals[k]; let from = idx(core.0 + 1, core.1 + 4); assert!(g.map.creature_path(false, from, idx(px, py + 1)).is_some(), "reachable from his dungeon"); } for n in crate::lobby::MATCH_SIZES { let seats = crate::lobby::seats_for(n); let mut m = Game::new(3, n, n); assert!(m.start_masters(seats, 0), "a {}x{} match with {} seats", n, n, seats.len()); } } #[test] fn a_coop_ai_seat_fights_beside_the_host_not_against_him() { let mut g = Game::new(11, 96, 128); g.init_fog(); g.coop = true; assert!(g.start_masters(&[0, 2, 3], 2), "a co-op match may keep an empty side as AI"); g.sync_ai_sides(&[3], 0); assert!(g.allied(g.me_side, 3), "side 3 is an allied master seat, not a rival"); g.ents.clear(); let (cx, cy) = g.map.core; let local = g.spawn(EK::Kobold, false, cx as f32 + 3.5, cy as f32 + 3.5, 1); let ally = g.spawn(EK::Kobold, false, cx as f32 + 4.5, cy as f32 + 3.5, 1); if let Some(e) = g.ents.iter_mut().find(|e| e.id == ally) { e.side = 3; } let local = g.ents.iter().find(|e| e.id == local).unwrap().clone(); let ally = g.ents.iter().find(|e| e.id == ally).unwrap().clone(); assert!(!g.is_hostile(&ally)); assert_eq!(g.nearest_enemy(&local, 5.0), None, "the allied AI creature is no target for the local master"); } }