// More rooms: prison (prisoners sit there, apart from the torture, and starve in time), // graveyard (the fallen are buried and may rise as revenants), guard post (creatures hold the // position and watch far), arena (sparring: fast experience, but bruises) and the sacrifice // altar (creatures dropped on it are sacrificed: certain pairs bring rare creatures, otherwise // a blessing). use crate::game::*; use crate::map::*; use crate::particles::{abgr, EMBER, MOTE, RING}; use serde::{Deserialize, Serialize}; pub const ROOM_PRISON: usize = 9; pub const ROOM_GRAVEYARD: usize = 10; pub const ROOM_GUARD: usize = 11; pub const ROOM_ARENA: usize = 12; const RISE_AFTER: f32 = 75.0; // seconds a grave rests before the dead may rise const CRUMBLE_AFTER: f32 = 240.0; // graves that stay quiet disappear #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Grave { pub tile: usize, pub t: f32, pub tried: bool, } // sacrifice recipes: two kinds (in any order) -> a rare creature const RECIPES: [(EK, EK, EK, u32); 4] = [ (EK::Ogre, EK::Spider, EK::Gargoyle, 3), (EK::Mage, EK::Bat, EK::Owl, 3), (EK::Jailer, EK::Priestess, EK::Chainbreaker, 2), (EK::Kobold, EK::Cogling, EK::Ogre, 4), ]; impl Game { // ---- prison pub(crate) fn free_cell(&self, i: usize) -> bool { let t = &self.map.t[i]; matches!(t.kind, Kind::Torture | Kind::Prison) && t.owner == self.me && t.bed == 0 } // ---- graveyard: a fallen unit is buried in a free grave pub(crate) fn bury(&mut self, x: f32, y: f32) { if self.counts[ROOM_GRAVEYARD] == 0 { return; } let taken: Vec = self.graves.iter().map(|g| g.tile).collect(); let spot = self .map .t .iter() .enumerate() .filter(|(i, t)| t.kind == Kind::Graveyard && t.owner == self.me && !taken.contains(i)) .min_by_key(|(i, _)| { let (tx, ty) = xy(*i); ((tx as f32 - x).powi(2) + (ty as f32 - y).powi(2)) as i32 }) .map(|(i, _)| i); if let Some(tile) = spot { self.graves.push(Grave { tile, t: 0.0, tried: false }); let (gx, gy) = xy(tile); self.fx(6, gx as f32 + 0.5, gy as f32 + 0.5, 0.0, 0.0, 0.5, 0xff3a5a7a); } } pub(crate) fn update_graves(&mut self, dt: f32) { let mut rise = Vec::new(); for gr in self.graves.iter_mut() { gr.t += dt; if gr.t > RISE_AFTER && !gr.tried { gr.tried = true; if self.rng.f() < 0.5 { rise.push(gr.tile); } } } let map = &self.map; self.graves.retain(|g| !rise.contains(&g.tile) && g.t < CRUMBLE_AFTER && map.t[g.tile].kind == Kind::Graveyard); for tile in rise { let (x, y) = xy(tile); let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5); let level = 1 + self.rng.next() % 3; let id = self.spawn(EK::Revenant, false, fx, fy, level); if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) { e.happy = 0.8; } for _ in 0..12 { let a = self.vrng.f() * 6.28; self.part(MOTE, (fx + a.cos() * 0.3, fy + a.sin() * 0.3, 0.1), (0.0, 0.0, 0.8), 1.4, abgr(150, 255, 170), 0.04); } self.sfx(Sfx::Summon, fx, fy); self.msg("msg.revenant_rises", vec![]); } } // ---- guard post: a free post for a creature pub(crate) fn free_post(&self, i: usize, me: u32) -> bool { let t = &self.map.t[i]; t.kind == Kind::GuardPost && t.owner == self.me && (t.reserved == 0 || t.reserved == me) } // ---- arena: sparring with a partner nearby; fast experience, some bruises pub(crate) fn spar(&mut self, e: &mut Ent, t: usize, dt: f32) -> bool { if e.tile() != t || self.map.t[t].kind != Kind::Arena { return false; } e.timer -= dt; if e.timer <= 0.0 || e.hp < e.max_hp * 0.5 { return false; } let partner = self.ents.iter().find(|o| o.alive && o.id != e.id && matches!(o.job, Job::Spar(_)) && o.path.is_empty() && (o.x - e.x).powi(2) + (o.y - e.y).powi(2) < 6.0).map(|o| (o.x, o.y)); if let Some((px, py)) = partner { e.working = true; e.face = if px > e.x { 1.0 } else { -1.0 }; e.dir = (py - e.y).atan2(px - e.x); e.xp += 14.0 * dt; e.hp = (e.hp - e.max_hp * 0.02 * dt).max(e.max_hp * 0.5); if self.vrng.f() < 1.5 * dt { self.fx(2, (e.x + px) * 0.5, (e.y + py) * 0.5, 0.0, 0.0, 0.25, 0xffffffff); } } true } // ---- altar: the creature dropped on it is sacrificed pub(crate) fn sacrifice(&mut self, j: usize) { let (kind, level, x, y) = (self.ents[j].kind, self.ents[j].level, self.ents[j].x, self.ents[j].y); let id = self.ents[j].id; self.release_all(id); self.ents[j].alive = false; for _ in 0..20 { let v = (self.vrng.f() * 2.0 - 1.0, self.vrng.f() * 2.0 - 1.0, 2.0 + self.vrng.f() * 2.0); self.part(EMBER, (x, y, 0.5), v, 1.2, abgr(255, 60, 40), 0.05); } self.part(RING, (x, y, 0.1), (0.0, 0.0, 0.0), 0.6, abgr(255, 40, 40), 1.5); self.sfx(Sfx::HeroDeath, x, y); // the last two sacrifices make a pair let pair = self.altar_last.take(); let recipe = pair.and_then(|p| RECIPES.iter().find(|r| (r.0 == p && r.1 == kind) || (r.1 == p && r.0 == kind))); match recipe { Some(&(_, _, gift, lv)) => { let nid = self.spawn(gift, false, x, y, lv); if let Some(e) = self.ents.iter_mut().find(|e| e.id == nid) { e.happy = 1.0; } self.fx(7, x, y, 0.0, 0.0, 1.4, 0xff4040ff); self.msg("msg.altar_gift", vec![format!("@{}", st(gift).name)]); } None => { self.altar_last = Some(kind); // a blessing: everybody cheers up, some mana for the strength of the sacrifice for o in self.ents.iter_mut().filter(|o| o.alive && o.creature()) { o.happy = (o.happy + 0.2).min(1.0); } self.mana = (self.mana + 15.0 * level as f32).min(self.mana_max()); self.msg("msg.altar_blessing", vec![format!("@{}", st(kind).name)]); } } } } #[cfg(test)] mod tests { use super::*; fn room(g: &mut Game, kind: Kind, x0: i32, y0: i32, w: i32, h: i32) { for y in y0..y0 + h { for x in x0..x0 + w { let t = &mut g.map.t[idx(x, y)]; t.kind = kind; t.owner = Owner::Player; } } g.recount(); } #[test] fn the_dead_rise_from_the_graveyard() { let mut g = Game::new(10, 64, 64); let (cx, cy) = g.map.core; room(&mut g, Kind::Graveyard, cx + 4, cy - 6, 3, 3); for k in 0..6 { g.bury(cx as f32 + 5.0, cy as f32 - 5.0 + k as f32 * 0.1); } assert_eq!(g.graves.len(), 6); for _ in 0..(20 * 90) { g.update(0.05); } assert!(g.ents.iter().any(|e| e.alive && e.kind == EK::Revenant), "a revenant rose"); } #[test] fn altar_pairs_bring_rare_creatures() { let mut g = Game::new(10, 64, 64); let (cx, cy) = g.map.core; let a = g.spawn(EK::Ogre, false, cx as f32 - 0.5, cy as f32, 2); let b = g.spawn(EK::Spider, false, cx as f32 - 0.5, cy as f32, 2); let j = g.find(a).unwrap(); g.sacrifice(j); g.ents.retain(|e| e.alive); let j = g.find(b).unwrap(); g.sacrifice(j); assert!(g.ents.iter().any(|e| e.alive && e.kind == EK::Gargoyle)); } #[test] fn sparring_gives_experience() { let mut g = Game::new(10, 64, 64); let (cx, cy) = g.map.core; room(&mut g, Kind::Arena, cx + 4, cy - 6, 3, 3); let ids: Vec = (0..2).map(|k| g.spawn(EK::Kobold, false, cx as f32 + 4.5 + k as f32, cy as f32 - 5.5, 1)).collect(); for &id in &ids { let j = g.find(id).unwrap(); let t = g.ents[j].tile(); g.ents[j].job = Job::Spar(t); g.ents[j].timer = 10.0; } let xp0: f32 = ids.iter().map(|&id| g.ents[g.find(id).unwrap()].xp).sum(); for _ in 0..40 { g.update(0.05); } let xp1: f32 = ids.iter().map(|&id| g.find(id).map_or(0.0, |j| g.ents[j].xp + 100.0 * (g.ents[j].level - 1) as f32)).sum(); assert!(xp1 > xp0 + 10.0, "{} -> {}", xp0, xp1); } }