// Creature personality: every kind loves one room and cannot stand certain other kinds (bad // mood, now and then a brawl - never to the death), and learns an ability at a certain level. use crate::game::*; use crate::map::*; use crate::particles::{abgr, RING}; pub struct Persona { pub likes: Kind, pub hates: &'static [EK], pub ability: Option<(u32, &'static str)>, // level, id (ability..name / .desc) } pub fn persona(k: EK) -> Persona { let p = |likes, hates, ability| Persona { likes, hates, ability }; match k { EK::Worker => p(Kind::Treasury, &[], Some((3, "dig"))), EK::Kobold => p(Kind::Training, &[EK::Mage], Some((5, "dash"))), EK::Spider => p(Kind::Farm, &[EK::Bat], Some((4, "web"))), EK::Mage => p(Kind::Library, &[EK::Kobold, EK::Ogre], Some((6, "teleport"))), EK::Ogre => p(Kind::Training, &[EK::Mage, EK::Cogling], Some((4, "stomp"))), EK::Jailer => p(Kind::Torture, &[EK::Priestess], Some((7, "shackle"))), EK::Priestess => p(Kind::Temple, &[EK::Jailer], Some((5, "pulse"))), EK::Chainbreaker => p(Kind::Training, &[], Some((6, "whirl"))), EK::Bat => p(Kind::Aerie, &[EK::Spider], Some((5, "shriek"))), EK::Gargoyle => p(Kind::Torture, &[EK::Owl], Some((5, "stoneform"))), EK::Owl => p(Kind::Library, &[EK::Gargoyle], Some((5, "volley"))), EK::Wyrm => p(Kind::Treasury, &[EK::Bat], Some((7, "inferno"))), EK::Cogling => p(Kind::Forge, &[EK::Ogre], Some((4, "bomb"))), _ => p(Kind::Lair, &[], None), } } // has this unit learned its ability? pub fn knows(e: &Ent, id: &str) -> bool { !e.hero && persona(e.kind).ability.map_or(false, |(lv, a)| a == id && e.level >= lv) } fn works_in(e: &Ent, g: &Game, kind: Kind) -> bool { match e.job { Job::Train(t) | Job::Study(t) | Job::Pray(t) | Job::Forge(t) | Job::Sleep(t) | Job::Eat(t) => g.map.t[t].kind == kind && e.path.is_empty(), Job::Torment(_) => kind == Kind::Torture && e.path.is_empty(), _ => g.map.t[e.tile()].kind == kind, } } impl Game { // every few seconds: the favourite room cheers up, hated company sours the mood pub(crate) fn mood_check(&mut self, e: &mut Ent, dt: f32) { e.mood_t -= dt; if e.mood_t > 0.0 { return; } e.mood_t = 4.0; let p = persona(e.kind); if works_in(e, self, p.likes) { e.happy = (e.happy + 0.03).min(1.0); } if p.hates.is_empty() || matches!(e.job, Job::Brawl(_) | Job::Attack(_)) { return; } let foe = self.ents.iter().position(|o| o.alive && !o.held && o.creature() && p.hates.contains(&o.kind) && (o.x - e.x).powi(2) + (o.y - e.y).powi(2) < 9.0); let Some(j) = foe else { return }; e.happy -= 0.015; let idle = |job: Job| matches!(job, Job::None | Job::Wander); if idle(e.job) && idle(self.ents[j].job) && self.rng.f() < 0.15 { // a brawl breaks out let other = self.ents[j].id; e.job = Job::Brawl(other); e.timer = 0.0; e.path.clear(); self.ents[j].job = Job::Brawl(e.id); self.ents[j].timer = 0.0; self.ents[j].path.clear(); let (a, b) = (st(e.kind).name, st(self.ents[j].kind).name); self.msg("msg.brawl", vec![format!("@{}", a), format!("@{}", b)]); } } // two creatures scuffling: they stop at 30% health, after eight seconds or when slapped pub(crate) fn brawl(&mut self, e: &mut Ent, id: u32, dt: f32) { e.timer += dt; let Some(j) = self.find(id) else { e.job = Job::None; return; }; let over = e.timer > 8.0 || e.hp < e.max_hp * 0.3 || self.ents[j].hp < self.ents[j].max_hp * 0.3 || !matches!(self.ents[j].job, Job::Brawl(_)); if over { e.job = Job::None; if self.ents[j].job == Job::Brawl(e.id) { self.ents[j].job = Job::None; } e.happy -= 0.03; return; } let (tx, ty) = (self.ents[j].x, self.ents[j].y); let d = ((tx - e.x).powi(2) + (ty - e.y).powi(2)).sqrt(); if d > 1.1 { if e.path.is_empty() || e.repath <= 0.0 { e.repath = 0.8; if let Some(p) = self.map.path_to(e.tile(), idx(tx as i32, ty as i32)) { e.path = p; } } self.step_path(e, dt); return; } e.path.clear(); e.working = true; e.face = if tx > e.x { 1.0 } else { -1.0 }; if e.cd <= 0.0 { e.cd = st(e.kind).interval; let o = &mut self.ents[j]; o.hp = (o.hp - e.atk() * 0.4).max(o.max_hp * 0.25); o.flash = 0.15; self.fx(2, tx, ty, 0.0, 0.0, 0.25, 0xffffffff); self.sfx(Sfx::MeleeHit, tx, ty); } } // active abilities in a fight; true = used instead of a normal blow pub(crate) fn use_ability(&mut self, e: &mut Ent, j: usize) -> bool { if e.ab_cd > 0.0 || e.hero || e.side != 0 { return false; } let (tx, ty) = (self.ents[j].x, self.ents[j].y); let me = e.side(); let foes_near = |g: &Game, x: f32, y: f32, r: f32| -> Vec { (0..g.ents.len()).filter(|&o| { let u = &g.ents[o]; u.alive && !u.held && u.side() != me && u.captive == 0 && !u.chained && (u.x - x).powi(2) + (u.y - y).powi(2) < r * r }).collect() }; let hurt = |g: &mut Game, o: usize, dmg: f32| { let u = &mut g.ents[o]; u.hp -= if u.shield > 0.0 { dmg * 0.5 } else { dmg }; u.flash = 0.15; if u.hp <= 0.0 { g.kill(o); } }; if knows(e, "stomp") || knows(e, "whirl") || knows(e, "shriek") { let (r, cd) = if knows(e, "shriek") { (2.0, 10.0) } else if knows(e, "whirl") { (1.6, 7.0) } else { (1.6, 8.0) }; let hit = foes_near(self, e.x, e.y, r); if hit.len() < 2 && !knows(e, "shriek") { return false; } for o in hit { if knows(e, "shriek") { self.ents[o].stun = self.ents[o].stun.max(1.2); } else { if knows(e, "stomp") { self.ents[o].stun = self.ents[o].stun.max(1.0); } let dmg = e.atk() * if knows(e, "whirl") { 0.8 } else { 0.6 }; hurt(self, o, dmg); } } self.part(RING, (e.x, e.y, 0.05), (0.0, 0.0, 0.0), 0.4, abgr(230, 200, 150), r); self.sfx(Sfx::Cannon, e.x, e.y); e.ab_cd = cd; e.cd = st(e.kind).interval; return true; } if knows(e, "bomb") { self.shot(e.x, e.y, 0.5, tx, ty, 0.2, 0.4, 0xff303030); self.style_last_fx(crate::particles::ST_CANNON); for o in foes_near(self, tx, ty, 1.3) { hurt(self, o, 40.0); } e.ab_cd = 10.0; e.cd = st(e.kind).interval; return true; } if knows(e, "dash") { let d = ((tx - e.x).powi(2) + (ty - e.y).powi(2)).sqrt(); if d > 2.5 && d < 6.0 && self.los(e.x, e.y, tx, ty) { let (nx, ny) = (tx - (tx - e.x) / d * 0.8, ty - (ty - e.y) / d * 0.8); if self.map.walk(nx as i32, ny as i32) { self.fx(0, e.x, e.y, nx, ny, 0.15, 0xffffffff); e.x = nx; e.y = ny; e.path.clear(); hurt(self, j, e.atk() * 1.2); e.ab_cd = 8.0; return true; } } } false } // abilities that work on their own (escape, protection, healing) pub(crate) fn passive_abilities(&mut self, e: &mut Ent) { if e.ab_cd > 0.0 || e.hero || e.side != 0 { return; } let fighting = matches!(e.job, Job::Attack(_)); if knows(e, "teleport") && fighting && e.hp < e.max_hp * 0.3 { let (cx, cy) = self.map.core; let to = e.bed.map(|b| (xy(b).0 as f32 + 0.5, xy(b).1 as f32 + 0.5)).unwrap_or((cx as f32 - 0.5, cy as f32 + 1.5)); self.fx(7, e.x, e.y, 0.0, 0.0, 0.6, 0xffff66cc); e.x = to.0; e.y = to.1; e.path.clear(); e.job = Job::None; self.fx(7, e.x, e.y, 0.0, 0.0, 0.6, 0xffff66cc); self.sfx(Sfx::Summon, e.x, e.y); e.ab_cd = 30.0; } else if knows(e, "stoneform") && e.hp < e.max_hp * 0.4 { e.shield = 6.0; e.ab_cd = 25.0; self.fx(7, e.x, e.y, 0.0, 0.0, 0.6, 0xffa0a0a0); } else if knows(e, "pulse") { let hurt: Vec = (0..self.ents.len()).filter(|&o| { let u = &self.ents[o]; u.alive && !u.held && u.creature() && u.hp < u.max_hp * 0.7 && (u.x - e.x).powi(2) + (u.y - e.y).powi(2) < 9.0 }).collect(); if hurt.len() >= 2 { for o in hurt { let u = &mut self.ents[o]; u.hp = (u.hp + 30.0).min(u.max_hp); let (ux, uy) = (u.x, u.y); self.fx(4, ux, uy, 0.0, 0.0, 0.8, 0xff60ff80); } self.part(RING, (e.x, e.y, 0.05), (0.0, 0.0, 0.0), 0.5, abgr(120, 255, 140), 3.0); e.ab_cd = 12.0; } } } } #[cfg(test)] mod tests { use super::*; #[test] fn hated_neighbours_brawl_but_nobody_dies() { let mut g = Game::new(8, 64, 64); let (cx, cy) = g.map.core; let a = g.spawn(EK::Mage, false, cx as f32 - 0.5, cy as f32 + 0.5, 2); let b = g.spawn(EK::Ogre, false, cx as f32 - 0.5, cy as f32 + 1.5, 2); for id in [a, b] { let j = g.find(id).unwrap(); g.ents[j].settled = true; } let mut brawled = false; for _ in 0..(20 * 120) { g.update(0.05); brawled |= g.ents.iter().any(|e| matches!(e.job, Job::Brawl(_))); for id in [a, b] { let j = g.find(id).unwrap(); g.ents[j].hunger = 0.0; g.ents[j].energy = 1.0; if !matches!(g.ents[j].job, Job::Brawl(_)) { g.ents[j].job = Job::None; g.ents[j].think = 99.0; } } } assert!(brawled, "they quarrel"); assert!(g.find(a).is_some() && g.find(b).is_some(), "nobody died"); } #[test] fn ogres_learn_to_stomp() { let mut g = Game::new(8, 64, 64); let (cx, cy) = g.map.core; let o = g.spawn(EK::Ogre, false, cx as f32 - 0.5, cy as f32 + 1.5, 4); for k in 0..2 { g.spawn(EK::Knight, true, cx as f32 - 0.5 + k as f32 * 0.3, cy as f32 + 2.3, 1); } let j = g.find(o).unwrap(); let mut e = g.ents[j].clone(); let target = g.ents.iter().position(|x| x.hero).unwrap(); assert!(g.use_ability(&mut e, target)); assert!(g.ents.iter().filter(|x| x.hero).all(|x| x.stun > 0.0)); } }