From 6e257e4034fc33326f99daee227753db871974a7 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Tue, 3 Nov 2026 13:04:07 +0100 Subject: [PATCH] The jailer works on prisoners in the torture chamber: 2.5 times the chance to defect Jailers pick a prisoner nobody works on yet (their favourite work), stand next to it and torment it; with a jailer about 28 of 40 prisoners defect instead of about 18. Test. Co-Authored-By: Claude Opus 5.5 --- lang/de.lang | 1 + lang/en.lang | 1 + src/game.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/lang/de.lang b/lang/de.lang index 13863ee..2eead33 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -527,3 +527,4 @@ unit.gargoyle = Kettengargoyle unit.owl = Arkane Eule unit.wyrm = Glutwyrm job.rest = Erholt sich im Horst +job.torment = Bearbeitet einen Gefangenen diff --git a/lang/en.lang b/lang/en.lang index 4204e68..0876d68 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -528,3 +528,4 @@ unit.gargoyle = Chain Gargoyle unit.owl = Arcane Owl unit.wyrm = Ember Wyrm job.rest = Recovering in the aerie +job.torment = Working on a prisoner diff --git a/src/game.rs b/src/game.rs index aaf43e3..eeb40f7 100644 --- a/src/game.rs +++ b/src/game.rs @@ -170,6 +170,7 @@ pub enum Job { HealCore, // flame priestess heals the soul core TearBridge(usize), // knight tears down a bridge of the player Rest(usize), // flying creature recovers in the aerie (flees when badly hurt) + Torment(u32), // the jailer works on a prisoner in the torture chamber Help(usize), // imp with nothing else to do helps another imp at this tile } @@ -2541,6 +2542,21 @@ impl Game { e.job = Job::None; } } + Job::Torment(id) => { + // working on a prisoner: makes it more likely to change sides (see captive_update) + let Some(k) = self.ents.iter().position(|o| o.id == id && o.alive && o.captive == 3) else { + e.job = Job::None; + return; + }; + let px = self.ents[k].x; + e.face = sgn(px - e.x); + e.working = true; + e.xp += 2.0 * dt; + e.timer -= dt; + if e.timer <= 0.0 { + e.job = Job::None; + } + } Job::Train(t) => { // training is billed on payday; it stops when the bill would exceed the gold if cur != t || self.map.t[t].kind != Kind::Training || self.training_bill + 1.0 > self.gold { @@ -2822,6 +2838,19 @@ impl Game { } } } + // the jailer's favourite work: prisoners nobody is working on yet + if e.kind == EK::Jailer && self.rng.f() < 0.85 { + let busy: Vec = self.ents.iter().filter_map(|o| if let Job::Torment(p) = o.job { Some(p) } else { None }).collect(); + let prisoner = self.ents.iter().filter(|o| o.alive && o.captive == 3 && !busy.contains(&o.id)).map(|o| (o.id, o.tile())).next(); + if let Some((id, pt)) = prisoner { + if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| i != pt && adj4(i, pt) || i == pt, true) { + e.path = p; + e.job = Job::Torment(id); + e.timer = 12.0 + self.rng.f() * 8.0; + return; + } + } + } let r = self.rng.f(); if (e.kind == EK::Mage && r < 0.8) || r < 0.2 { if let Some(p) = self.find_room_tile(cur, Kind::Library, me, false) { @@ -2891,8 +2920,13 @@ impl Game { e.torture_t += dt; if e.torture_t >= 5.0 { e.torture_t -= 5.0; - // ~40% defect over the whole torture (paladins ~25%) - let p = if e.kind == EK::Paladin { 0.0143 } else { 0.0252 }; + // ~40% defect over the whole torture (paladins ~25%); a jailer working on + // the prisoner makes it 2.5 times as likely + let tormented = self.ents.iter().any(|o| o.alive && o.kind == EK::Jailer && o.job == Job::Torment(e.id) && o.path.is_empty() && (o.x - e.x).abs() < 1.6 && (o.y - e.y).abs() < 1.6); + let p = if e.kind == EK::Paladin { 0.0143 } else { 0.0252 } * if tormented { 2.5 } else { 1.0 }; + if tormented { + self.fx(2, e.x, e.y, 0.0, 0.0, 0.3, 0xff3040ff); + } if self.rng.f() < p { self.defect(e); return; @@ -4066,6 +4100,7 @@ pub fn job_name(e: &Ent) -> &'static str { Job::Attack(_) => "job.fight", Job::TearBridge(_) => "job.tear_bridge", Job::Rest(_) => "job.rest", + Job::Torment(_) => "job.torment", Job::Help(_) => "job.help", Job::AttackCore => { if e.working { @@ -4705,6 +4740,46 @@ mod tests { assert!((8..=26).contains(&converted), "defect rate off: {}/40", converted); } + #[test] + fn jailer_makes_prisoners_defect_more_often() { + let mut converted = 0; + let mut worked = 0; + for seed in 1..=40u32 { + let mut g = Game::new(seed, 64, 64); + g.gold = 5000.0; + let (cx, cy) = g.map.core; + g.build_rect(5, cx - 3, cy - 3, cx - 2, cy + 1); + g.build_rect(1, cx + 4, cy - 3, cx + 5, cy - 2); // a lair for the jailer + let jailer = g.spawn(EK::Jailer, false, (cx - 1) as f32 + 0.5, (cy - 3) as f32 + 0.5, 1); + let id = g.spawn(EK::Knight, true, (cx + 4) as f32 + 0.5, (cy + 4) as f32 + 0.5, 1); + let j = g.ents.iter().position(|e| e.id == id).unwrap(); + g.kill(j); + for _ in 0..(200 * 20) { + g.update(0.05); + if let Some(jl) = g.ents.iter_mut().find(|e| e.id == jailer) { + // keep the jailer fresh so that it works + jl.hunger = 0.0; + jl.energy = 1.0; + jl.happy = 1.0; + if jl.job == Job::Torment(id) { + worked += 1; + } + } + match g.ents.iter().find(|e| e.id == id) { + Some(e) if !e.hero => { + converted += 1; + break; + } + None => break, + _ => {} + } + } + } + println!("with a jailer: {} of 40 defected (jailer worked {} ticks)", converted, worked); + assert!(worked > 0, "the jailer works on the prisoners"); + assert!(converted >= 20, "defections with a jailer: {}/40", converted); + } + #[test] fn temple_prayer_and_priestess_heals_core() { let mut g = Game::new(9, 64, 64);