226 lines
8.7 KiB
Rust
226 lines
8.7 KiB
Rust
// 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<usize> = 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<u32> = (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);
|
|
}
|
|
}
|