Light for towers and traps; other masters' traps stay dark until sprung

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 16:08:04 +01:00
1 parent a13abf63f3
commit 6c16bc7b18
2 files changed
+70 -8

No files matched your search

+36 -5
View File
@@ -1142,11 +1142,42 @@ impl R3d {
for &(ex, ey) in &g.planned_entries { for &(ex, ey) in &g.planned_entries {
push(vec3(ex as f32 + 0.5, 1.2, ey as f32 + 0.5), 5.0, vec3(1.0, 0.12, 0.08), 0.6 + 1.0 * pulse, 30.0); push(vec3(ex as f32 + 0.5, 1.2, ey as f32 + 0.5), 5.0, vec3(1.0, 0.12, 0.08), 0.6 + 1.0 * pulse, 30.0);
} }
for tw in &g.towers { // towers: the dungeon's glow red from their joints, the knights' carry a cold lantern;
if tw.kind == TowerKind::Magic { // magic and rune towers shine in their colour; every shot flares up
let (x, z) = tw.pos(); for tw in g.towers.iter().filter(|tw| g.map.shown(tw.tile)) {
push(vec3(x, tower_height(tw) + 0.6, z), 4.5, vec3(0.75, 0.4, 1.0), 0.8 + tw.flash * 3.0, 10.0); let (x, z) = tw.pos();
let top = tower_height(tw);
let (c, i, r) = match tw.kind {
TowerKind::Magic => (vec3(0.75, 0.4, 1.0), 0.9, 4.5),
TowerKind::Rune => (vec3(0.4, 0.8, 1.0), 1.0, 5.0),
_ if tw.hero => (vec3(0.85, 0.9, 1.0), 0.8, 4.0),
_ => (vec3(1.0, 0.3, 0.12), 0.85, 4.0),
};
let pulse = 0.9 + 0.1 * (t * 2.0 + tw.id as f32).sin();
push(vec3(x, top + 0.4, z), r, c, i * pulse + tw.flash * 3.0, 10.0);
}
// traps: each kind in its colour, brighter while it goes off. Another master's traps
// stay dark until they have gone off once (so the light does not betray them)
for tr in g.traps.iter().filter(|tr| tr.stage == crate::traps::ARMED && g.map.shown(tr.tile)) {
use crate::traps::TrapKind as K;
if !g.trap_glows(tr) {
continue;
} }
let fresh = (1.0 - tr.fired / 0.8).clamp(0.0, 1.0); // just went off
let (c, i, r) = match tr.kind {
K::Poison => (vec3(0.35, 1.0, 0.3), 0.55, 2.5),
K::Fire => (vec3(1.0, 0.5, 0.12), 0.6, 3.0),
K::Alarm => (vec3(1.0, 0.15, 0.1), 0.35 + 0.35 * (0.5 + 0.5 * (t * 4.0).sin()), 3.0),
K::Mine => (vec3(0.7, 0.4, 1.0), 0.5, 2.5),
K::Frost => (vec3(0.5, 0.85, 1.0), 0.65, 3.0),
K::Spikes | K::Roller => (vec3(1.0, 0.85, 0.6), 0.0, 2.5),
_ => continue, // doors
};
if i + fresh <= 0.0 {
continue;
}
let (x, z) = xy(tr.tile);
push(vec3(x as f32 + 0.5, 0.35, z as f32 + 0.5), r + fresh, c, i + 2.5 * fresh, 5.0);
} }
// effects light up their surroundings: lightning, explosions, glowing projectiles, spells // effects light up their surroundings: lightning, explosions, glowing projectiles, spells
let tint = |c: u32| { let tint = |c: u32| {
@@ -2602,7 +2633,7 @@ fn draw_traps_glow(g: &Game, t: f32) {
line3(c + vec3(-0.35, y, 0.0), c + vec3(-0.35 + 0.7 * f, y, 0.0), 0.08, Color::new(1.0, 0.7, 0.3, 0.8)); line3(c + vec3(-0.35, y, 0.0), c + vec3(-0.35 + 0.7 * f, y, 0.0), 0.08, Color::new(1.0, 0.7, 0.3, 0.8));
continue; continue;
} }
if tr.stage != ARMED { if tr.stage != ARMED || !g.trap_glows(tr) {
continue; continue;
} }
if tr.revealed && !tr.kind.door() { if tr.revealed && !tr.kind.door() {
+34 -3
View File
@@ -100,6 +100,8 @@ pub struct Trap {
pub aim: f32, // direction of the last shot (fire jet, roller) pub aim: f32, // direction of the last shot (fire jet, roller)
#[serde(default)] #[serde(default)]
pub revealed: bool, // a fairy has found it: heroes wreck it pub revealed: bool, // a fairy has found it: heroes wreck it
#[serde(default)]
pub sprung: bool, // has gone off at least once (its light shows to the other masters from then on)
} }
// a boulder rolling down a corridor // a boulder rolling down a corridor
@@ -178,7 +180,7 @@ impl Game {
} }
let id = self.next_id; let id = self.next_id;
self.next_id += 1; 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, revealed: false }); 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, sprung: false });
self.fx(8, x as f32 + 0.5, y as f32 + 0.5, 0.0, 0.0, 0.4, 0xff40d0ff); 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); self.sfx_ui(Sfx::FlagSet);
} }
@@ -227,6 +229,12 @@ impl Game {
self.update_traps(dt); self.update_traps(dt);
} }
// may the local master see this trap's light and glow? His own always; another master's
// only once it has gone off (in a match the light must not betray a trap)
pub fn trap_glows(&self, t: &Trap) -> bool {
t.kind.door() || self.map.t[t.tile].owner == self.me || t.sprung
}
// is there forge work to do (an order waiting)? // is there forge work to do (an order waiting)?
pub fn forge_orders(&self) -> bool { pub fn forge_orders(&self) -> bool {
self.traps.iter().any(|t| t.stage == ORDERED) self.traps.iter().any(|t| t.stage == ORDERED)
@@ -334,6 +342,7 @@ impl Game {
let (cx, cy) = (tx as f32 + 0.5, ty as f32 + 0.5); let (cx, cy) = (tx as f32 + 0.5, ty as f32 + 0.5);
self.traps[k].hp -= dmg; self.traps[k].hp -= dmg;
self.traps[k].fired = 0.0; self.traps[k].fired = 0.0;
self.traps[k].sprung = true;
self.fx(2, cx, cy, 0.0, 0.0, 0.25, 0xff4080c0); self.fx(2, cx, cy, 0.0, 0.0, 0.25, 0xff4080c0);
self.sfx(Sfx::Dig, cx, cy); self.sfx(Sfx::Dig, cx, cy);
if self.traps[k].kind == TrapKind::MagicDoor && self.traps[k].cd <= 0.0 { if self.traps[k].kind == TrapKind::MagicDoor && self.traps[k].cd <= 0.0 {
@@ -441,6 +450,7 @@ impl Game {
if damage > 0.0 { if damage > 0.0 {
t.hp -= damage * dt; t.hp -= damage * dt;
t.fired = 0.0; t.fired = 0.0;
t.sprung = true;
if t.hp <= 0.0 { if t.hp <= 0.0 {
wrecked.push(k); wrecked.push(k);
} }
@@ -482,6 +492,7 @@ impl Game {
if !hit.is_empty() { if !hit.is_empty() {
self.traps[k].cd = 2.5; self.traps[k].cd = 2.5;
self.traps[k].fired = 0.0; self.traps[k].fired = 0.0;
self.traps[k].sprung = true;
for j in hit { for j in hit {
self.damage_unit(j, 45.0); self.damage_unit(j, 45.0);
} }
@@ -493,6 +504,7 @@ impl Game {
if !near(0.9, &self.ents).is_empty() { if !near(0.9, &self.ents).is_empty() {
self.traps[k].cd = 12.0; self.traps[k].cd = 12.0;
self.traps[k].fired = 0.0; self.traps[k].fired = 0.0;
self.traps[k].sprung = true;
self.clouds.push(Cloud { x: cx, y: cy, t: 0.0 }); self.clouds.push(Cloud { x: cx, y: cy, t: 0.0 });
self.sfx(Sfx::Magic, cx, cy); self.sfx(Sfx::Magic, cx, cy);
} }
@@ -505,6 +517,7 @@ impl Game {
let a = (ey - cy).atan2(ex - cx); let a = (ey - cy).atan2(ex - cx);
self.traps[k].cd = 4.0; self.traps[k].cd = 4.0;
self.traps[k].fired = 0.0; self.traps[k].fired = 0.0;
self.traps[k].sprung = true;
self.traps[k].aim = a; self.traps[k].aim = a;
let (dx, dy) = (a.cos(), a.sin()); let (dx, dy) = (a.cos(), a.sin());
for o in near(3.4, &self.ents) { for o in near(3.4, &self.ents) {
@@ -531,6 +544,7 @@ impl Game {
let (dx, dy) = if ex.abs() > ey.abs() { (ex.signum(), 0.0) } else { (0.0, ey.signum()) }; let (dx, dy) = if ex.abs() > ey.abs() { (ex.signum(), 0.0) } else { (0.0, ey.signum()) };
self.traps[k].cd = 18.0; self.traps[k].cd = 18.0;
self.traps[k].fired = 0.0; self.traps[k].fired = 0.0;
self.traps[k].sprung = true;
self.traps[k].aim = dy.atan2(dx); self.traps[k].aim = dy.atan2(dx);
self.rollers.push(Roller { x: cx, y: cy, dx, dy, t: 0.0, hit: Vec::new() }); self.rollers.push(Roller { x: cx, y: cy, dx, dy, t: 0.0, hit: Vec::new() });
self.sfx(Sfx::DigDone, cx, cy); self.sfx(Sfx::DigDone, cx, cy);
@@ -541,6 +555,7 @@ impl Game {
if let Some(&j) = seen.first() { if let Some(&j) = seen.first() {
self.traps[k].cd = 20.0; self.traps[k].cd = 20.0;
self.traps[k].fired = 0.0; self.traps[k].fired = 0.0;
self.traps[k].sprung = true;
let target = self.ents[j].id; let target = self.ents[j].id;
for o in self.ents.iter_mut() { for o in self.ents.iter_mut() {
if o.alive && o.creature() && !matches!(o.job, Job::Attack(_)) && (o.x - cx).powi(2) + (o.y - cy).powi(2) < 18.0 * 18.0 { if o.alive && o.creature() && !matches!(o.job, Job::Attack(_)) && (o.x - cx).powi(2) + (o.y - cy).powi(2) < 18.0 * 18.0 {
@@ -628,7 +643,7 @@ mod tests {
fn doors_stop_enemies_until_smashed_and_lock() { fn doors_stop_enemies_until_smashed_and_lock() {
let (mut g, cx, cy) = forge_game(); let (mut g, cx, cy) = forge_game();
let i = idx(cx + 10, cy); 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, revealed: false }); 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, sprung: false });
g.map.t[i].kind = Kind::Door; g.map.t[i].kind = Kind::Door;
assert!(g.map.walk_cost()(i).is_some()); assert!(g.map.walk_cost()(i).is_some());
g.place_trap(TrapKind::WoodDoor, cx + 10, cy); g.place_trap(TrapKind::WoodDoor, cx + 10, cy);
@@ -653,7 +668,7 @@ mod tests {
let a = idx(cx + 10, cy); let a = idx(cx + 10, cy);
let b = idx(cx + 10, cy + 4); let b = idx(cx + 10, cy + 4);
for (id, i, kind) in [(1001, a, TrapKind::Frost), (1002, b, TrapKind::Alarm)] { 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.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, sprung: false });
} }
g.spawn(EK::Fairy, true, cx as f32 + 11.5, cy as f32 + 0.5, 1); 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); g.spawn(EK::Giant, true, cx as f32 + 10.5, cy as f32 + 5.5, 1);
@@ -669,4 +684,20 @@ mod tests {
assert!(g.traps.iter().all(|t| t.id != 1002), "the giant wrecked the alarm"); 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"); assert!(g.traps.iter().find(|t| t.id == 1001).map_or(true, |t| t.revealed), "the fairy found the frost glow");
} }
#[test]
fn foreign_traps_glow_only_after_going_off() {
let mut g = Game::new(3, 64, 64);
let (cx, cy) = g.map.core;
let mine = idx(cx + 2, cy - 2);
let theirs = idx(cx + 3, cy - 2);
g.map.t[mine].owner = g.me;
g.map.t[theirs].owner = Owner::Rival1;
let trap = |tile| Trap { id: 1, kind: TrapKind::Fire, tile, stage: ARMED, crate_tile: 0, progress: 0.0, cd: 0.0, hp: 10.0, locked: false, carrier: 0, fired: 99.0, aim: 0.0, revealed: false, sprung: false };
assert!(g.trap_glows(&trap(mine)), "my own trap");
let mut t = trap(theirs);
assert!(!g.trap_glows(&t), "a rival's trap stays dark");
t.sprung = true;
assert!(g.trap_glows(&t), "until it has gone off");
}
} }