Chainbreaker: rare, very strong original creature (twin-axe cleave, restless without fights)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 09:14:39 +01:00
1 parent 05ba88359e
commit c5b7896609
7 files changed
+111 -7

No files matched your search

+76
View File
@@ -94,6 +94,7 @@ pub enum EK {
Paladin,
Jailer, // attracted by the torture chamber
Priestess, // attracted by the temple
Chainbreaker, // rare and very strong
}
pub struct St {
@@ -123,6 +124,7 @@ pub fn st(k: EK) -> St {
EK::Paladin => s("unit.paladin", 700.0, 26.0, 1.0, 1.7, 1.2, 220, 1000),
EK::Jailer => s("unit.jailer", 260.0, 16.0, 1.0, 1.6, 1.3, 140, 0),
EK::Priestess => s("unit.priestess", 70.0, 14.0, 5.0, 2.0, 1.6, 120, 0),
EK::Chainbreaker => s("unit.chainbreaker", 600.0, 36.0, 1.1, 2.3, 1.1, 400, 0),
}
}
@@ -720,6 +722,13 @@ impl Game {
if self.counts[6] >= 6 {
opts.push((EK::Priestess, 2));
}
// the Chainbreaker only visits big dungeons with experienced creatures and a rich treasury
let room_tiles: usize = self.counts.iter().sum();
let veteran = self.ents.iter().any(|e| e.alive && e.creature() && e.level >= 5);
let has_one = self.ents.iter().any(|e| e.alive && e.kind == EK::Chainbreaker && !e.hero);
if room_tiles >= 40 && veteran && self.gold >= 4000.0 && !has_one {
opts.push((EK::Chainbreaker, 1));
}
let total: u32 = opts.iter().map(|o| o.1).sum();
let mut r = self.rng.next() % total;
let mut kind = EK::Kobold;
@@ -1160,6 +1169,25 @@ impl Game {
self.sfx(Sfx::MeleeHit, tx, ty);
}
e.xp += dmg * 0.6;
if e.kind == EK::Chainbreaker {
// twin axes: a second enemy next to the Chainbreaker takes half damage
let side = !e.hero;
let second = self
.ents
.iter()
.enumerate()
.find(|(o, x)| *o != j && x.alive && !x.held && x.hero == side && x.captive == 0 && (x.x - e.x).powi(2) + (x.y - e.y).powi(2) < 2.6)
.map(|(o, _)| o);
if let Some(o) = second {
let (ox, oy) = (self.ents[o].x, self.ents[o].y);
self.fx(2, ox, oy - 0.2, 0.0, 0.0, 0.25, 0xff40a0ff);
self.ents[o].hp -= dmg * 0.5;
self.ents[o].flash = 0.15;
if self.ents[o].hp <= 0.0 {
self.kill(o);
}
}
}
if e.kind == EK::Priestess {
// fireball: half damage to enemies around the target
self.fx(3, tx, ty, 0.0, 0.0, 0.5, 0xff3080ff);
@@ -1617,6 +1645,15 @@ impl Game {
}
}
}
if e.kind == EK::Chainbreaker {
// restless: without a fight for two minutes its mood sinks (skill_cd counts down as idle time)
if matches!(e.job, Job::Attack(_)) {
e.skill_cd = 0.0;
} else if e.skill_cd < -120.0 {
e.happy -= 0.005 * dt;
self.hint(20, "hint.chainbreaker_restless");
}
}
if let Job::Attack(id) = e.job {
if !self.attack_job(e, id, dt) {
e.job = Job::None;
@@ -3270,5 +3307,44 @@ mod tests {
assert!(g.pickup(Some(imp)));
assert!(!g.drop(40.5, 40.5) || g.map.t[idx(40, 40)].owner == Owner::Player);
}
#[test]
fn chainbreaker_visits_only_great_dungeons() {
let mut g = Game::new(8, 64, 64);
g.gold = 9000.0;
// conditions not met: no Chainbreaker among many portal visits
let (px, py) = g.map.portals[0];
for y in 0..3 {
for x in 0..3 {
g.map.t[idx(px + x, py + y)].owner = Owner::Player;
}
}
let (cx, cy) = g.map.core;
g.build_rect(1, cx + 4, cy - 3, cx + 5, cy + 5); // lair 18
g.build_rect(2, cx - 3, cy - 3, cx - 2, cy + 5); // farm 18
g.recount();
for _ in 0..60 {
g.try_portal();
}
assert!(!g.ents.iter().any(|e| e.kind == EK::Chainbreaker));
// a veteran creature makes it possible (rooms 16 ring + 36 = 52 tiles, gold 9000)
if let Some(e) = g.ents.iter_mut().find(|e| e.creature()) {
e.level = 5;
}
for e in g.ents.iter_mut().filter(|e| e.creature()) {
e.alive = false; // free beds
}
g.ents.retain(|e| e.alive);
g.spawn(EK::Kobold, false, cx as f32, cy as f32, 6);
let mut seen = false;
for _ in 0..400 {
g.gold = 9000.0;
g.try_portal();
seen |= g.ents.iter().any(|e| e.kind == EK::Chainbreaker);
g.ents.retain(|e| !e.creature() || e.kind == EK::Chainbreaker || e.level >= 5);
}
assert!(seen);
assert!(g.ents.iter().filter(|e| e.kind == EK::Chainbreaker).count() <= 1);
}
}