Five new rooms: prison, graveyard, guard post, arena, sacrifice altar; new creature: the revenant

- prison: captured heroes sit in cells apart from the torture chamber, do not defect and starve in time; the hand can move prisoners between cells and torture
- graveyard: the fallen are buried there; after a while some rise as revenants (undead: no food, no sleep, life drain from level 5)
- guard post: creatures hold the position and attack whatever comes into view
- arena: creatures spar in pairs for fast, free experience, at the cost of bruises
- sacrifice altar: creatures dropped on it are sacrificed; certain pairs bring a rare creature, otherwise a blessing (mood and mana)
- researched in the library, campaign unlocks, textures, furniture, texts EN/DE, bestiary

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-04 07:35:12 +01:00
1 parent 2c8db33a69
commit a58497e3a9
14 files changed
+606 -28

No files matched your search

+225
View File
@@ -0,0 +1,225 @@
// 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 == Owner::Player && 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 == Owner::Player && !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 == Owner::Player && (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);
}
}