Three new heroes: giant, fairy spy, battle mage

- giant (from wave 6): slow and enormous, smashes doors three times as hard and wrecks traps by standing next to them
- fairy spy (from wave 3): flies, finds the dungeon's traps (marked with a glint) so the heroes wreck them, and heals heroes around it
- battle mage (from wave 4): throws fireballs with splash and blasts reinforced walls (steel resists him better)
- sprites, bestiary texts EN/DE, test: fairies find traps, giants wreck them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-04 07:40:20 +01:00
1 parent a58497e3a9
commit 4ce2c2919e
7 files changed
+186 -15

No files matched your search

+72 -4
View File
@@ -98,6 +98,8 @@ pub struct Trap {
pub carrier: u32, // imp carrying or installing it (entity id + 1)
pub fired: f32, // time since it last went off (for drawing)
pub aim: f32, // direction of the last shot (fire jet, roller)
#[serde(default)]
pub revealed: bool, // a fairy has found it: heroes wreck it
}
// a boulder rolling down a corridor
@@ -122,8 +124,12 @@ pub struct Cloud {
const CLOUD_TIME: f32 = 6.0;
// how hard a unit hits doors
pub fn door_breaker(_k: EK) -> f32 {
1.0
pub fn door_breaker(k: EK) -> f32 {
if k == EK::Giant {
3.0
} else {
1.0
}
}
fn enemy(e: &Ent) -> bool {
@@ -172,7 +178,7 @@ impl Game {
}
let id = self.next_id;
self.next_id += 1;
self.traps.push(Trap { id, kind, tile: i, stage: ORDERED, crate_tile: 0, progress: 0.0, cd: 0.0, hp: kind.hp(), locked: false, carrier: 0, fired: 99.0, aim: 0.0 });
self.traps.push(Trap { id, kind, tile: i, stage: ORDERED, crate_tile: 0, progress: 0.0, cd: 0.0, hp: kind.hp(), locked: false, carrier: 0, fired: 99.0, aim: 0.0, revealed: false });
self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.4, 0xff40d0ff);
self.sfx_ui(Sfx::FlagSet);
}
@@ -408,6 +414,45 @@ impl Game {
}
rollers.retain(|r| r.t < 50.0);
self.rollers = rollers;
// fairies find traps, giants and heroes wreck the traps they know about
let mut wrecked = Vec::new();
for k in 0..self.traps.len() {
let t = &self.traps[k];
if t.stage != ARMED || t.kind.door() {
continue;
}
let (tx, ty) = xy(t.tile);
let (cx, cy) = (tx as f32 + 0.5, ty as f32 + 0.5);
let mut damage = 0.0;
let mut found = t.revealed;
for e in self.ents.iter().filter(|e| e.alive && !e.held && e.active_hero()) {
let d2 = (e.x - cx).powi(2) + (e.y - cy).powi(2);
if e.kind == EK::Fairy && d2 < 9.0 {
found = true;
}
if d2 < 1.6 && (found || e.kind == EK::Giant) {
damage += if e.kind == EK::Giant { 40.0 } else { 15.0 };
}
}
let t = &mut self.traps[k];
if found && !t.revealed {
t.revealed = true;
}
if damage > 0.0 {
t.hp -= damage * dt;
t.fired = 0.0;
if t.hp <= 0.0 {
wrecked.push(k);
}
}
}
for &k in wrecked.iter().rev() {
let t = self.traps.remove(k);
let (tx, ty) = xy(t.tile);
self.fx(3, tx as f32 + 0.5, ty as f32 + 0.5, 0.0, 0.0, 0.8, 0xff505070);
self.fx(6, tx as f32 + 0.5, ty as f32 + 0.5, 0.0, 0.0, 0.5, 0xff3a5a7a);
self.msg("msg.trap_wrecked", vec![format!("@trap.{}", t.kind.id())]);
}
for k in 0..self.traps.len() {
if k >= self.traps.len() {
break;
@@ -583,7 +628,7 @@ mod tests {
fn doors_stop_enemies_until_smashed_and_lock() {
let (mut g, cx, cy) = forge_game();
let i = idx(cx + 10, cy);
g.traps.push(Trap { id: 999, kind: TrapKind::WoodDoor, tile: i, stage: ARMED, crate_tile: 0, progress: 0.0, cd: 0.0, hp: 400.0, locked: false, carrier: 0, fired: 99.0, aim: 0.0 });
g.traps.push(Trap { id: 999, kind: TrapKind::WoodDoor, tile: i, stage: ARMED, crate_tile: 0, progress: 0.0, cd: 0.0, hp: 400.0, locked: false, carrier: 0, fired: 99.0, aim: 0.0, revealed: false });
g.map.t[i].kind = Kind::Door;
assert!(g.map.walk_cost()(i).is_some());
g.place_trap(TrapKind::WoodDoor, cx + 10, cy);
@@ -601,4 +646,27 @@ mod tests {
assert!(g.trap_at(i).is_none(), "door smashed");
assert_eq!(g.map.t[i].kind, Kind::Floor);
}
#[test]
fn fairies_find_traps_and_giants_wreck_them() {
let (mut g, cx, cy) = forge_game();
let a = idx(cx + 10, cy);
let b = idx(cx + 10, cy + 4);
for (id, i, kind) in [(1001, a, TrapKind::Frost), (1002, b, TrapKind::Alarm)] {
g.traps.push(Trap { id, kind, tile: i, stage: ARMED, crate_tile: 0, progress: 0.0, cd: 99.0, hp: kind.hp(), locked: false, carrier: 0, fired: 99.0, aim: 0.0, revealed: false });
}
g.spawn(EK::Fairy, true, cx as f32 + 11.5, cy as f32 + 0.5, 1);
g.spawn(EK::Giant, true, cx as f32 + 10.5, cy as f32 + 5.5, 1);
for j in 0..g.ents.len() {
g.ents[j].stun = 999.0; // stand still
}
for _ in 0..(20 * 15) {
g.update(0.05);
for j in 0..g.ents.len() {
g.ents[j].stun = 999.0;
}
}
assert!(g.traps.iter().all(|t| t.id != 1002), "the giant wrecked the alarm");
assert!(g.traps.iter().find(|t| t.id == 1001).map_or(true, |t| t.revealed), "the fairy found the frost glow");
}
}