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:
1 parent
05ba88359e
commit
c5b7896609
7 files changed
+111
-7
No files matched your search
+76
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-2
@@ -675,6 +675,19 @@ fn sprite_rows(k: EK) -> [&'static str; 10] {
|
||||
".kRRRRRRk.",
|
||||
"..kkkkkk..",
|
||||
],
|
||||
// obsidian demon with glowing cracks, broken chains and twin axes
|
||||
EK::Chainbreaker => [
|
||||
"s.kkkkkk.s",
|
||||
"sskdoodkss",
|
||||
".SkeddekS.",
|
||||
".SkdoodkS.",
|
||||
"kdddoodddk",
|
||||
"kdoddddodk",
|
||||
"SkddooddkS",
|
||||
".kddddddk.",
|
||||
".kdok.odk.",
|
||||
".kkk..kkk.",
|
||||
],
|
||||
EK::Paladin => [
|
||||
"...kkkk..w",
|
||||
"..kyyyyk.w",
|
||||
@@ -714,13 +727,14 @@ fn pal(ch: char) -> Option<Color> {
|
||||
'C' => c(96, 98, 92),
|
||||
'o' => c(226, 124, 48),
|
||||
'O' => c(160, 72, 24),
|
||||
'd' => c(44, 36, 48),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub const SPRITE_KINDS: [EK; 12] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Paladin, EK::Jailer, EK::Priestess];
|
||||
pub const SPRITE_KINDS: [EK; 13] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Paladin, EK::Jailer, EK::Priestess, EK::Chainbreaker];
|
||||
// width of the sprite texture in pixels
|
||||
pub const SPRITE_TEX_W: f32 = (12 * 12) as f32;
|
||||
pub const SPRITE_TEX_W: f32 = (13 * 12) as f32;
|
||||
pub const SPRITE_TEX_H: f32 = 24.0;
|
||||
|
||||
pub fn sprite_index(k: EK) -> u32 {
|
||||
|
||||
+4
-4
@@ -976,8 +976,8 @@ impl R3d {
|
||||
if !e.alive || e.held {
|
||||
continue;
|
||||
}
|
||||
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer);
|
||||
let w = if big { 0.8 } else { 0.62 };
|
||||
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker);
|
||||
let w = if e.kind == EK::Chainbreaker { 1.0 } else if big { 0.8 } else { 0.62 };
|
||||
let h = w;
|
||||
let sleeping = matches!(e.job, Job::Sleep(_)) && !e.moving;
|
||||
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
|
||||
@@ -1045,7 +1045,7 @@ impl R3d {
|
||||
if !e.alive || e.held {
|
||||
continue;
|
||||
}
|
||||
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer);
|
||||
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker);
|
||||
let head = proj(vec3(e.x, if big { 0.9 } else { 0.72 }, e.y));
|
||||
// bar width in block characters, depending on the zoom
|
||||
let cols = ((px_per_tile * 0.6 / cw) as usize).clamp(3, 8);
|
||||
@@ -1265,7 +1265,7 @@ fn draw_shadows(g: &Game) {
|
||||
if !e.alive || e.held {
|
||||
continue;
|
||||
}
|
||||
let r = if matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer) { 0.32 } else { 0.24 };
|
||||
let r = if matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer | EK::Chainbreaker) { 0.32 } else { 0.24 };
|
||||
disc(vec3(e.x, 0.012, e.y), r, Color::new(0.0, 0.0, 0.0, 0.4), 12);
|
||||
let rc = if e.captive != 0 {
|
||||
Color::new(0.6, 0.6, 0.6, 0.6)
|
||||
|
||||
+4
-1
@@ -125,7 +125,10 @@ mod tests {
|
||||
g.update(0.05); // the scouts warn 30 s before the wave
|
||||
}
|
||||
assert!(!g.planned_entries.is_empty());
|
||||
let (ex, ey) = g.planned_entries[0];
|
||||
let (cx, cy) = g.map.core;
|
||||
g.spawn(crate::game::EK::Chainbreaker, false, (cx - 1) as f32 + 0.5, (cy - 1) as f32 + 0.5, 1);
|
||||
g.spawn(crate::game::EK::Ogre, false, (cx + 4) as f32 + 0.5, (cy - 1) as f32 + 0.5, 1);
|
||||
let (ex, ey) = (cx + 1, cy + 1);
|
||||
let mut cam = Cam::new(ex as f32 + 0.5, ey as f32 + 0.5);
|
||||
cam.zoom = 12.0;
|
||||
std::fs::write(path, to_json(&g, &cam, &Settings { language: "en".into(), ..Settings::default() })).unwrap();
|
||||
|
||||
Reference in new issue
Block a user