From a58497e3a9f5789f72a018be5d3ce7873306794a Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Wed, 4 Nov 2026 07:35:12 +0100 Subject: [PATCH] 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 --- lang/de.lang | 33 +++++++ lang/en.lang | 33 +++++++ src/campaign.rs | 2 +- src/game.rs | 152 +++++++++++++++++++++++++---- src/gfx.rs | 60 +++++++++++- src/main.rs | 1 + src/map.rs | 17 +++- src/menu/bestiary.rs | 4 +- src/personality.rs | 1 + src/props.rs | 80 +++++++++++++++ src/render3d.rs | 5 + src/research.rs | 5 + src/rooms.rs | 225 +++++++++++++++++++++++++++++++++++++++++++ src/ui.rs | 16 ++- 14 files changed, 606 insertions(+), 28 deletions(-) create mode 100644 src/rooms.rs diff --git a/lang/de.lang b/lang/de.lang index 0f8b3cb..363ba52 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -860,3 +860,36 @@ ability.inferno.name = Inferno ability.inferno.desc = sein Feueratem breitet sich viel weiter aus. ability.bomb.name = Bombe ability.bomb.desc = wirft eine selbstgebaute Bombe, die zwischen den Feinden explodiert. + +# more rooms +room.prison = Gefängnis +room.graveyard = Friedhof +room.guardpost = Wachposten +room.arena = Arena +room.altar = Opferaltar +tool.prison.label = Gefängnis +tool.prison.desc = Gefangene Helden sitzen hier in Zellen - getrennt von der Folterkammer. Ohne Folter laufen sie nicht über, und der Hunger zehrt an ihnen. Mit der Hand kannst du einen Gefangenen in die Folterkammer bringen. +tool.graveyard.label = Friedhof +tool.graveyard.desc = Gefallene - Helden wie Kreaturen - werden hier begraben. Nach einer Weile erheben sich manche als Wiedergänger und dienen dir. +tool.guardpost.label = Wachposten +tool.guardpost.desc = Kreaturen halten hier eine Weile Stellung und blicken weit: Sie greifen jeden Feind an, der in Sicht kommt. +tool.arena.label = Arena +tool.arena.desc = Kreaturen kämpfen paarweise zur Übung: doppelt so viel Erfahrung wie im Training und kostenlos, aber mit blauen Flecken. +tool.altar.label = Altar +tool.altar.desc = Lass eine Kreatur auf den Altar fallen, um sie zu opfern. Bestimmte Opferpaare bringen eine seltene Kreatur, sonst wird das Verlies gesegnet: bessere Laune und Mana. +unit.revenant = Wiedergänger +job.guard_post = Hält Wache +job.spar = Übungskampf +msg.revenant_rises = Ein Wiedergänger erhebt sich vom Friedhof und dient dir! +msg.prisoner_starved = Ein gefangener {0} ist im Gefängnis verhungert. +msg.altar_gift = Der Altar nimmt das Paar an: ein {0} erscheint! +msg.altar_blessing = {0} wird geopfert. Das Verlies ist gesegnet. +best.revenant.lure = erhebt sich vom Friedhof +best.revenant.text = Ein Wiedergänger isst nicht, schläft nicht und beschwert sich nicht - der perfekte Angestellte, wenn man den Geruch aushält. | Mit etwas Erfahrung trinkt er die Kraft derer, die er trifft. Mit der Priesterin versteht er sich aus naheliegenden Gründen nicht. +best.b.prison.text = Zellenreihen. Helden verrotten hier in Ruhe, bis jemand entscheidet, was mit ihnen geschieht. +best.b.graveyard.text = Moos, Steine und frische Erde. Hier unten bleibt niemand lange tot. +best.b.guardpost.text = Ein Feuerbecken, ein Banner und eine gelangweilte Kreatur mit scharfen Augen. +best.b.arena.text = Sand, Schweiß und gelegentlich ein Zahn. Erfahrung kommt hier schnell. +best.b.altar.text = Schwarzer Marmor, der trinkt. Man sagt, er höre auf Paare. Oger und Spinne, Magier und Fledermaus, Kerkermeister und Priesterin, Kobold und Cogling... +ability.drain.name = Lebensraub +ability.drain.desc = heilt sich um ein Drittel des Schadens, den er austeilt. diff --git a/lang/en.lang b/lang/en.lang index 1ea9378..9e33596 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -861,3 +861,36 @@ ability.inferno.name = Inferno ability.inferno.desc = its fire breath spreads much wider. ability.bomb.name = Bomb ability.bomb.desc = throws a home-made bomb that explodes among the enemies. + +# more rooms +room.prison = Prison +room.graveyard = Graveyard +room.guardpost = Guard Post +room.arena = Arena +room.altar = Sacrifice Altar +tool.prison.label = Prison +tool.prison.desc = Captured heroes sit in cells here - apart from the torture chamber. Without torture they do not change sides, and hunger wears them down. Pick a prisoner up with the hand to move it to the torture chamber. +tool.graveyard.label = Graveyard +tool.graveyard.desc = The fallen - heroes and creatures - are buried here. After a while some of them rise as revenants and serve you. +tool.guardpost.label = Guard post +tool.guardpost.desc = Creatures hold the position here for a while and watch far: they attack every enemy that comes into view. +tool.arena.label = Arena +tool.arena.desc = Creatures spar in pairs: twice as much experience as training and free of charge, but they get bruised. +tool.altar.label = Altar +tool.altar.desc = Drop a creature on the altar to sacrifice it. Certain pairs of sacrifices bring a rare creature, otherwise the dungeon is blessed: better mood and mana. +unit.revenant = Revenant +job.guard_post = On guard +job.spar = Sparring +msg.revenant_rises = A revenant rises from the graveyard and serves you! +msg.prisoner_starved = A captured {0} starved in prison. +msg.altar_gift = The altar accepts the pair: a {0} appears! +msg.altar_blessing = The {0} is sacrificed. The dungeon is blessed. +best.revenant.lure = rises from the graveyard +best.revenant.text = A revenant does not eat, does not sleep and does not complain - the perfect employee, if you can stand the smell. | It drinks the strength of those it strikes once it has some experience. The priestess and it do not get along, for obvious reasons. +best.b.prison.text = Rows of cells. Heroes rot here quietly until somebody decides what to do with them. +best.b.graveyard.text = Moss, stones and fresh earth. Nobody stays dead for long down here. +best.b.guardpost.text = A brazier, a banner and a bored creature with sharp eyes. +best.b.arena.text = Sand, sweat and the occasional tooth. Experience comes fast here. +best.b.altar.text = Black marble that drinks. Some say it listens to pairs. Ogre and spider, mage and bat, jailer and priestess, kobold and cogling... +ability.drain.name = Life drain +ability.drain.desc = heals itself by a third of the damage it deals. diff --git a/src/campaign.rs b/src/campaign.rs index ac8d217..33182a4 100644 --- a/src/campaign.rs +++ b/src/campaign.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; // From which mission (0-based) on each tool is available (same order as ui::TOOLS): // dig, hand, treasury, lair, farm, training, library, sell, flag, lightning, heal, imp, // torture, temple, arrow tower, cannon tower, magic tower, bridge, aerie, steel -pub const TOOL_UNLOCK: [usize; 40] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17, 0, 7, 7, 10, 12, 12, 7, 16, 16, 7, 10, 16, 9, 13, 6, 10, 14, 17, 19, 22]; +pub const TOOL_UNLOCK: [usize; 45] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17, 0, 7, 7, 10, 12, 12, 7, 16, 16, 7, 10, 16, 9, 13, 6, 10, 14, 17, 19, 22, 9, 13, 6, 11, 18]; pub struct FortDef { pub at: (f32, f32), // centre as a fraction of the map size diff --git a/src/game.rs b/src/game.rs index 865ebce..656df53 100644 --- a/src/game.rs +++ b/src/game.rs @@ -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, + #[serde(default)] + pub altar_last: Option, // 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(); diff --git a/src/gfx.rs b/src/gfx.rs index ab45229..0858c46 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -73,6 +73,11 @@ pub const I_RUNE: u32 = 143; // rune forge tower pub const T_FORGE: u32 = 144; // 4 variants: sooty flagstones with glowing cracks pub const I_TRAPS: u32 = 148; // 10 icons in TRAP_KINDS order pub const I_SPELLS: u32 = 158; // 8 icons in SPELLS order +pub const T_PRISON: u32 = 166; // 4 variants each +pub const T_GRAVEYARD: u32 = 170; +pub const T_GUARDPOST: u32 = 174; +pub const T_ARENA: u32 = 178; +pub const T_ALTAR: u32 = 182; pub struct Gfx { pub atlas: Texture2D, @@ -765,6 +770,40 @@ fn gen_atlas() -> Image { } }); } + // prison: dark flagstones with drain grates + for v in 0..4u32 { + cv.cell(T_PRISON + v, |x, y| { + if (5..=10).contains(&x) && (5..=10).contains(&y) && (x == 5 || x == 10 || y == 5 || y == 10 || x % 2 == 0) && v % 2 == 0 { + Some(c(30, 30, 34)) + } else { + Some(stones(x, y, v + 310, (58, 56, 60), (28, 26, 30), 6)) + } + }); + // graveyard: dark earth, moss and pebbles + cv.cell(T_GRAVEYARD + v, |x, y| { + let r = rnd(x, y, v + 320); + Some(if r > 0.86 { c(70, 96, 52) } else if r > 0.8 { c(110, 106, 100) } else { shade((52, 40, 34), (rnd(x / 2, y / 2, v + 321) * 16.0) as i32 - 8) }) + }); + // guard post: flagstones with a red border + cv.cell(T_GUARDPOST + v, |x, y| { + if x == 0 || y == 0 || x == 15 || y == 15 { + Some(c(120, 30, 34)) + } else { + Some(stones(x, y, v + 330, (76, 66, 62), (36, 30, 30), 8)) + } + }); + // arena: raked sand with dark stains + cv.cell(T_ARENA + v, |x, y| { + let rake = (y + (x / 4 + v as i32) % 2) % 3 == 0; + let stain = rnd(x / 3, y / 3, v + 340) > 0.9; + Some(if stain { c(110, 60, 40) } else if rake { c(170, 140, 96) } else { shade((196, 164, 112), (rnd(x, y, v + 341) * 14.0) as i32 - 7) }) + }); + // altar: black marble with red veins + cv.cell(T_ALTAR + v, |x, y| { + let vein = ((x as f32 * 0.7 + (y as f32 * 0.5 + v as f32).sin() * 3.0) as i32 + y) % 11 == 0; + Some(if vein { c(150, 20, 30) } else { shade((30, 26, 32), (rnd(x, y, v + 350) * 10.0) as i32 - 5) }) + }); + } // trap and door icons let icons: [fn(i32, i32) -> Option; 10] = [ // spikes @@ -1226,6 +1265,19 @@ fn sprite_rows(k: EK) -> [&'static str; 10] { "...kO.Ok..", "...kk.kk..", ], + // revenant: a pale, hollow-eyed corpse with a rusty blade and green grave-light + EK::Revenant => [ + "...kkkk...", + "..kwwwwk..", + "..kwgwgk..", + "..kwwwwk.S", + "...kCCk..S", + "..kCcCCk.S", + ".kCkcCkCkb", + "..kCcCCk..", + "..kCk.kCk.", + "..kk...kk.", + ], EK::Wyrm => [ "R.......kw", "RR....krrk", @@ -1295,7 +1347,7 @@ fn pal(ch: char) -> Option { }) } -pub const SPRITE_KINDS: [EK; 18] = [ +pub const SPRITE_KINDS: [EK; 19] = [ EK::Worker, EK::Kobold, EK::Spider, @@ -1314,6 +1366,7 @@ pub const SPRITE_KINDS: [EK; 18] = [ EK::Owl, EK::Wyrm, EK::Cogling, + EK::Revenant, ]; // width of the sprite texture in pixels pub const SPRITE_TEX_W: f32 = (SPRITE_KINDS.len() * 12) as f32; @@ -1727,6 +1780,11 @@ impl Gfx { Kind::ManaSpring => T_PORTALFLOOR + v, Kind::RuneChest => T_FLOOR + v, Kind::Forge => T_FORGE + v, + Kind::Prison => T_PRISON + v, + Kind::Graveyard => T_GRAVEYARD + v, + Kind::GuardPost => T_GUARDPOST + v, + Kind::Arena => T_ARENA + v, + Kind::Altar => T_ALTAR + v, Kind::Door => { if claimed { T_CLAIM + v diff --git a/src/main.rs b/src/main.rs index 97912a4..ca86bd4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ mod props; mod research; mod rival; mod render3d; +mod rooms; mod save; mod settings; mod sfx; diff --git a/src/map.rs b/src/map.rs index 5447088..8a8080a 100644 --- a/src/map.rs +++ b/src/map.rs @@ -63,6 +63,11 @@ pub enum Kind { RuneChest, // dwarven chest with the plans of a rune forge tower (claim it: one free build) Forge, // room: creatures manufacture traps and doors Door, // floor with an installed door (res bit 0: locked) + Prison, // room: prisoners wait in cells + Graveyard, // room: the fallen are buried, some rise again + GuardPost, // room: creatures hold the position + Arena, // room: sparring + Altar, // room: sacrifices } // options for the map generator (campaign missions) @@ -91,7 +96,7 @@ pub struct River { } // order matters for the packed savegame codec: only append new kinds -const KINDS: [Kind; 29] = [ +const KINDS: [Kind; 34] = [ Kind::Rock, Kind::Earth, Kind::Gold, @@ -121,6 +126,11 @@ const KINDS: [Kind; 29] = [ Kind::RuneChest, Kind::Forge, Kind::Door, + Kind::Prison, + Kind::Graveyard, + Kind::GuardPost, + Kind::Arena, + Kind::Altar, ]; // treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond @@ -219,6 +229,11 @@ impl Tile { | Kind::RuneChest | Kind::Forge | Kind::Door + | Kind::Prison + | Kind::Graveyard + | Kind::GuardPost + | Kind::Arena + | Kind::Altar ) } // river tiles (open, but nobody can walk there) diff --git a/src/menu/bestiary.rs b/src/menu/bestiary.rs index 5dec21b..6c7b8be 100644 --- a/src/menu/bestiary.rs +++ b/src/menu/bestiary.rs @@ -7,10 +7,10 @@ use crate::ui::{tool_cost_static, BLUE as BLUE_C, GREEN as GREEN_C, RED as RED_C pub const TABS: [&str; 4] = ["best.tab.creatures", "best.tab.humans", "best.tab.dungeon", "best.tab.ranks"]; -const CREATURES: [EK; 13] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Cogling, EK::Bat, EK::Gargoyle, EK::Owl, EK::Wyrm]; +const CREATURES: [EK; 14] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Cogling, EK::Revenant, EK::Bat, EK::Gargoyle, EK::Owl, EK::Wyrm]; const HUMANS: [EK; 5] = [EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Paladin]; // buildings: indices into ui::TOOLS -const BUILDINGS: [usize; 25] = [2, 3, 4, 5, 6, 12, 13, 18, 21, 17, 19, 14, 15, 16, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]; +const BUILDINGS: [usize; 30] = [2, 3, 4, 5, 6, 12, 40, 13, 18, 21, 41, 42, 43, 44, 17, 19, 14, 15, 16, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]; #[derive(Clone, Copy)] enum Entry { diff --git a/src/personality.rs b/src/personality.rs index ce3e589..9e6dcc2 100644 --- a/src/personality.rs +++ b/src/personality.rs @@ -27,6 +27,7 @@ pub fn persona(k: EK) -> Persona { 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"))), + EK::Revenant => p(Kind::Graveyard, &[EK::Priestess], Some((5, "drain"))), _ => p(Kind::Lair, &[], None), } } diff --git a/src/props.rs b/src/props.rs index d5c747c..602cc9f 100644 --- a/src/props.rs +++ b/src/props.rs @@ -143,11 +143,26 @@ pub fn draw_props_solid(g: &Game, cam: &Cam, t: f32) { Kind::ManaSpring => spring_basin(c, l), Kind::RuneChest => rune_chest(c, l), Kind::Forge if (x + z) % 2 == 0 => anvil(c, h, l), + Kind::Prison => prison_bars(g, x, z, c, l), + Kind::Graveyard if h % 4 == 0 => tombstone(c + vec3(0.25, 0.0, -0.25), h, l, 0.6), + Kind::GuardPost if x % 2 == 0 && z % 2 == 0 => guard_brazier(c, l), + Kind::Arena if h % 5 == 0 => arena_post(c, h, l), + Kind::Altar if x % 3 == 1 && z % 3 == 1 => altar(c, l), Kind::Forge if h % 4 == 1 => furnace(c, l), _ => {} } } } + // graves: a mound and a stone, the fresher the darker the earth + for gr in &g.graves { + let (gx, gz) = xy(gr.tile); + if gx < x0 || gx > x1 || gz < z0 || gz > z1 { + continue; + } + let c = vec3(gx as f32 + 0.5, 0.0, gz as f32 + 0.5); + cube(c + vec3(0.0, 0.05, 0.08), vec3(0.34, 0.1, 0.6), rgb(70, 52, 40, 0.7)); + tombstone(c + vec3(0.0, 0.0, -0.28), gr.tile as u32, 0.8, 1.0); + } // creatures in chains: an iron post with chains to the creature for e in g.ents.iter().filter(|e| e.alive && e.chained) { if (e.x as i32) < x0 || (e.x as i32) > x1 || (e.y as i32) < z0 || (e.y as i32) > z1 { @@ -174,6 +189,59 @@ fn rune_chest(c: Vec3, l: f32) { cube(c + vec3(0.0, 0.22, 0.18), vec3(0.08, 0.1, 0.03), rgb(200, 170, 80, l)); } +// prison: iron bars along the edges of the room (cell walls) +fn prison_bars(g: &Game, x: i32, z: i32, c: Vec3, l: f32) { + let iron = rgb(70, 70, 78, l); + for (dx, dz) in [(0i32, -1i32), (1, 0), (0, 1), (-1, 0)] { + let open = g.map.get(x + dx, z + dz).map_or(false, |t| t.kind != Kind::Prison && t.walkable()); + if !open { + continue; + } + let edge = c + vec3(dx as f32 * 0.46, 0.0, dz as f32 * 0.46); + let side = vec3(dz as f32, 0.0, dx as f32); + for k in 0..5 { + let o = (k as f32 - 2.0) * 0.2; + cube(edge + side * o + vec3(0.0, 0.4, 0.0), vec3(0.04, 0.8, 0.04), iron); + } + cube(edge + vec3(0.0, 0.78, 0.0), side * 1.0 + vec3(0.05, 0.04, 0.05), iron); + } +} + +fn tombstone(c: Vec3, h: u32, l: f32, size: f32) { + let tilt = ((h >> 3) % 3) as f32 * 0.04 - 0.04; + cube(c + vec3(tilt, 0.2 * size, 0.0), vec3(0.24, 0.4, 0.08) * size, rgb(110, 108, 112, l)); + cube(c + vec3(tilt, 0.42 * size, 0.0), vec3(0.16, 0.06, 0.08) * size, rgb(120, 118, 122, l)); + cube(c + vec3(tilt, 0.28 * size, 0.045), vec3(0.06, 0.16, 0.01) * size, rgb(60, 58, 62, l)); +} + +// guard post: a brazier on a tripod (its fire glows in the glow pass) and a small banner +fn guard_brazier(c: Vec3, l: f32) { + for k in 0..3 { + let a = k as f32 * 2.1; + cube(c + vec3(a.cos() * 0.12, 0.2, a.sin() * 0.12), vec3(0.04, 0.4, 0.04), rgb(60, 60, 66, l)); + } + cube(c + vec3(0.0, 0.44, 0.0), vec3(0.3, 0.08, 0.3), rgb(70, 66, 70, l)); + cube(c + vec3(0.35, 0.5, 0.0), vec3(0.03, 1.0, 0.03), rgb(90, 64, 40, l)); + cube(c + vec3(0.45, 0.82, 0.0), vec3(0.2, 0.28, 0.02), rgb(150, 24, 30, l)); +} + +// arena: a wooden post with a training weapon leaning against it +fn arena_post(c: Vec3, h: u32, l: f32) { + let o = vec3(((h >> 4) % 3) as f32 * 0.2 - 0.2, 0.0, ((h >> 6) % 3) as f32 * 0.2 - 0.2); + cube(c + o + vec3(0.0, 0.35, 0.0), vec3(0.1, 0.7, 0.1), rgb(110, 76, 44, l)); + cube(c + o + vec3(0.12, 0.35, 0.0), vec3(0.03, 0.6, 0.03), rgb(150, 150, 160, l)); +} + +// sacrifice altar: a black block with a channel, candles at the corners (they glow) +fn altar(c: Vec3, l: f32) { + cube(c + vec3(0.0, 0.08, 0.0), vec3(1.6, 0.16, 1.2), rgb(36, 30, 38, l)); + cube(c + vec3(0.0, 0.32, 0.0), vec3(1.0, 0.32, 0.6), rgb(28, 24, 30, l)); + cube(c + vec3(0.0, 0.49, 0.0), vec3(0.8, 0.02, 0.1), rgb(90, 10, 20, l)); + for (dx, dz) in [(-0.7f32, -0.5f32), (0.7, -0.5), (0.7, 0.5), (-0.7, 0.5)] { + cube(c + vec3(dx, 0.25, dz), vec3(0.06, 0.18, 0.06), rgb(220, 210, 190, l)); + } +} + // forge: an anvil on a block, a hammer leaning against it fn anvil(c: Vec3, h: u32, l: f32) { let o = vec3(((h >> 3) % 3) as f32 * 0.08 - 0.08, 0.0, ((h >> 5) % 3) as f32 * 0.08 - 0.08); @@ -258,6 +326,18 @@ pub fn draw_props_glow(g: &Game, cam: &Cam, t: f32) { } } } + Kind::GuardPost if x % 2 == 0 && z % 2 == 0 => { + let f = 0.8 + 0.2 * (t * 8.0 + k).sin(); + draw_sphere(c + vec3(0.0, 0.55, 0.0), 0.11 * f, None, Color::new(1.0, 0.55, 0.1, 0.95)); + draw_sphere(c + vec3(0.0, 0.6, 0.0), 0.25 * f, None, Color::new(1.0, 0.35, 0.05, 0.2)); + } + Kind::Altar if x % 3 == 1 && z % 3 == 1 => { + for (dx, dz) in [(-0.7f32, -0.5f32), (0.7, -0.5), (0.7, 0.5), (-0.7, 0.5)] { + flame(c + vec3(dx, 0.38, dz), t, k + dx); + } + let p = 0.5 + 0.5 * (t * 1.5 + k).sin(); + draw_cube(c + vec3(0.0, 0.5, 0.0), vec3(0.8, 0.02, 0.1), None, Color::new(1.0, 0.1, 0.15, 0.4 + 0.5 * p)); + } Kind::Forge if h % 4 == 1 && (x + z) % 2 != 0 => { // furnace fire let f = 0.7 + 0.3 * (t * 9.0 + k).sin(); diff --git a/src/render3d.rs b/src/render3d.rs index 548bfe4..d2c3cb0 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -2574,6 +2574,11 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] { Kind::ManaSpring => [90, 230, 255, 255], Kind::RuneChest => [80, 200, 255, 255], Kind::Forge => [150, 70, 40, 255], + Kind::Prison => [90, 90, 100, 255], + Kind::Graveyard => [70, 90, 60, 255], + Kind::GuardPost => [150, 50, 50, 255], + Kind::Arena => [200, 170, 110, 255], + Kind::Altar => [120, 20, 40, 255], Kind::Door => [140, 90, 50, 255], } } diff --git a/src/research.rs b/src/research.rs index 01e67d4..fa31c76 100644 --- a/src/research.rs +++ b/src/research.rs @@ -15,7 +15,9 @@ pub const RESEARCH: &[(usize, f32)] = &[ (29, 250.0), // wooden door (32, 300.0), // seer's eye (15, 400.0), // cannon tower + (42, 300.0), // guard post (12, 450.0), // torture chamber + (40, 450.0), // prison (26, 300.0), // alarm bell (35, 400.0), // stoneskin (11, 500.0), // summon @@ -24,6 +26,8 @@ pub const RESEARCH: &[(usize, f32)] = &[ (30, 500.0), // iron door (33, 550.0), // cave-in (18, 550.0), // aerie + (43, 550.0), // arena + (41, 600.0), // graveyard (16, 650.0), // magic tower (36, 600.0), // gold rain (24, 650.0), // fire jet @@ -34,6 +38,7 @@ pub const RESEARCH: &[(usize, f32)] = &[ (38, 850.0), // plague (27, 900.0), // rune mine (39, 950.0), // quake + (44, 900.0), // sacrifice altar (31, 1100.0), // magic door ]; diff --git a/src/rooms.rs b/src/rooms.rs new file mode 100644 index 0000000..6410e08 --- /dev/null +++ b/src/rooms.rs @@ -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 = 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 = (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); + } +} diff --git a/src/ui.rs b/src/ui.rs index 283dd7a..35cf0c6 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -162,7 +162,7 @@ pub enum Tool { } // New tools are appended so that existing key bindings ("tool") keep their meaning -pub const TOOLS: [Tool; 40] = [ +pub const TOOLS: [Tool; 45] = [ Tool::Dig, Tool::Hand, Tool::Room(0), @@ -203,6 +203,11 @@ pub const TOOLS: [Tool; 40] = [ Tool::Spell(crate::spells::Spell::Invisible), Tool::Spell(crate::spells::Spell::Plague), Tool::Spell(crate::spells::Spell::Quake), + Tool::Room(9), + Tool::Room(10), + Tool::Room(11), + Tool::Room(12), + Tool::Room(13), ]; @@ -225,7 +230,12 @@ pub fn tool_info(t: Tool) -> ToolInfo { Tool::Room(5) => ("torture", T_TORTURE, Some(5)), Tool::Room(6) => ("temple", T_TEMPLE, Some(6)), Tool::Room(7) => ("aerie", T_AERIE, Some(7)), - Tool::Room(_) => ("forge", T_FORGE, Some(8)), + Tool::Room(8) => ("forge", T_FORGE, Some(8)), + Tool::Room(9) => ("prison", T_PRISON, Some(9)), + Tool::Room(10) => ("graveyard", T_GRAVEYARD, Some(10)), + Tool::Room(11) => ("guardpost", T_GUARDPOST, Some(11)), + Tool::Room(12) => ("arena", T_ARENA, Some(12)), + Tool::Room(_) => ("altar", T_ALTAR, Some(13)), Tool::Trap(k) => (k.id(), I_TRAPS + crate::traps::TRAP_KINDS.iter().position(|&t| t == k).unwrap_or(0) as u32, None), Tool::Spell(sp) => (sp.id(), I_SPELLS + crate::spells::SPELLS.iter().position(|&t| t == sp).unwrap_or(0) as u32, None), Tool::Tower(TowerKind::Arrow) => ("arrow", I_ARROW, None), @@ -299,7 +309,7 @@ pub fn tool_cost(t: Tool, g: &Game) -> String { pub const CATS: [(&str, &[usize]); 7] = [ ("cat.dig", &[0]), ("cat.hand", &[1]), - ("cat.rooms", &[2, 3, 4, 5, 6, 12, 13, 18, 21, 17]), + ("cat.rooms", &[2, 3, 4, 5, 6, 12, 40, 13, 18, 21, 41, 42, 43, 44, 17]), ("cat.towers", &[14, 15, 16, 20]), ("cat.traps", &[22, 23, 24, 25, 26, 27, 28, 29, 30, 31]), ("cat.spells", &[9, 10, 11, 32, 33, 34, 35, 36, 37, 38, 39]),