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:
1 parent
2c8db33a69
commit
a58497e3a9
14 files changed
+606
-28
No files matched your search
+132
-20
@@ -108,6 +108,7 @@ pub enum EK {
|
||||
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
|
||||
}
|
||||
|
||||
pub struct St {
|
||||
@@ -145,6 +146,7 @@ pub fn st(k: EK) -> St {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +186,8 @@ pub enum Job {
|
||||
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
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@@ -489,6 +493,10 @@ pub struct Game {
|
||||
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<crate::rooms::Grave>,
|
||||
#[serde(default)]
|
||||
pub altar_last: Option<EK>, // the last sacrifice that waits for its pair
|
||||
#[serde(skip)]
|
||||
pub fog_t: f32, // fog of war: seconds until the next update
|
||||
#[serde(skip)]
|
||||
@@ -533,11 +541,41 @@ impl Torch {
|
||||
}
|
||||
}
|
||||
|
||||
pub const N_ROOMS: usize = 9;
|
||||
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];
|
||||
pub const ROOM_COST: [i32; N_ROOMS] = [60, 80, 100, 140, 160, 200, 180, 120, 150];
|
||||
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"];
|
||||
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] = [30, 75];
|
||||
@@ -663,6 +701,8 @@ impl Game {
|
||||
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(),
|
||||
@@ -1116,6 +1156,7 @@ impl Game {
|
||||
self.update_forge(dt);
|
||||
self.update_research();
|
||||
self.update_reveals(dt);
|
||||
self.update_graves(dt);
|
||||
self.update_fog(dt);
|
||||
if !self.rivals.is_empty() {
|
||||
self.update_rivals(dt);
|
||||
@@ -1850,7 +1891,7 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
fn release_all(&mut self, id: u32) {
|
||||
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 {
|
||||
@@ -2034,6 +2075,9 @@ impl Game {
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
@@ -2070,7 +2114,7 @@ impl Game {
|
||||
|
||||
// free places in the torture chamber (one prisoner per 2 tiles)
|
||||
pub fn prison_space(&self) -> usize {
|
||||
let cap = self.counts[5] / 2;
|
||||
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)
|
||||
}
|
||||
@@ -2111,6 +2155,9 @@ impl Game {
|
||||
}
|
||||
self.release_all(id);
|
||||
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 {
|
||||
let b = s.bounty as f32;
|
||||
@@ -2906,9 +2953,15 @@ impl Game {
|
||||
|
||||
fn creature_ai(&mut self, e: &mut Ent, dt: f32) {
|
||||
let me = e.id + 1;
|
||||
e.hunger += dt / 75.0;
|
||||
if !matches!(e.job, Job::Sleep(_)) {
|
||||
e.energy -= dt / 110.0;
|
||||
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;
|
||||
@@ -2962,8 +3015,8 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if !matches!(e.job, Job::Attack(_) | Job::Leave | Job::Rest(_)) {
|
||||
let radius = if e.job == Job::Rally { 8.5 } else if flies(e.kind) { 9.0 } else { 7.0 };
|
||||
if !matches!(e.job, Job::Attack(_) | Job::Leave | Job::Rest(_) | Job::Brawl(_)) {
|
||||
let radius = if e.job == Job::Rally { 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);
|
||||
@@ -3120,6 +3173,19 @@ impl Game {
|
||||
e.job = Job::None;
|
||||
}
|
||||
}
|
||||
Job::Guard(t) => {
|
||||
e.timer -= dt;
|
||||
if cur != t || self.map.t[t].kind != Kind::GuardPost || 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);
|
||||
@@ -3432,6 +3498,27 @@ impl Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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(cur, 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();
|
||||
@@ -3501,6 +3588,17 @@ impl Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ 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 {
|
||||
@@ -3533,6 +3631,7 @@ impl Game {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3566,11 +3665,6 @@ impl Game {
|
||||
}).map(|o| o.id)
|
||||
}
|
||||
|
||||
fn free_torture_tile(&self, i: usize) -> bool {
|
||||
let t = &self.map.t[i];
|
||||
t.kind == Kind::Torture && t.owner == Owner::Player && t.bed == 0
|
||||
}
|
||||
|
||||
fn carry_job(&mut self, e: &mut Ent, id: u32, dt: f32) {
|
||||
let Some(k) = self.find(id) else {
|
||||
e.job = Job::None;
|
||||
@@ -3582,7 +3676,7 @@ impl Game {
|
||||
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_torture_tile(i), true) {
|
||||
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;
|
||||
@@ -4497,7 +4591,7 @@ impl Game {
|
||||
return false; // the rivals' creatures and the dwarves cannot be picked up
|
||||
}
|
||||
if e.hero {
|
||||
if e.captive != 1 {
|
||||
if e.captive != 1 && e.captive != 3 {
|
||||
return false;
|
||||
}
|
||||
if !self.on_own_ground(e.x, e.y) {
|
||||
@@ -4509,6 +4603,12 @@ impl Game {
|
||||
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;
|
||||
@@ -4541,7 +4641,17 @@ impl Game {
|
||||
return false;
|
||||
}
|
||||
self.hand.pop();
|
||||
let imprison = t.kind == Kind::Torture && t.bed == 0;
|
||||
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;
|
||||
@@ -4790,6 +4900,8 @@ pub fn job_name(e: &Ent) -> &'static str {
|
||||
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",
|
||||
@@ -5362,7 +5474,7 @@ mod tests {
|
||||
#[test]
|
||||
fn aerie_attracts_flyers() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
g.counts = [16, 0, 9, 0, 6, 4, 0, 16, 0];
|
||||
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();
|
||||
|
||||
Reference in new issue
Block a user