diff --git a/src/render3d.rs b/src/render3d.rs index a8e1dd0..adf2af5 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -1142,11 +1142,42 @@ impl R3d { 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); } - for tw in &g.towers { - if tw.kind == TowerKind::Magic { - let (x, z) = tw.pos(); - 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); + // towers: the dungeon's glow red from their joints, the knights' carry a cold lantern; + // magic and rune towers shine in their colour; every shot flares up + for tw in g.towers.iter().filter(|tw| g.map.shown(tw.tile)) { + 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 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)); continue; } - if tr.stage != ARMED { + if tr.stage != ARMED || !g.trap_glows(tr) { continue; } if tr.revealed && !tr.kind.door() { diff --git a/src/traps.rs b/src/traps.rs index a04c9ae..ef460e2 100644 --- a/src/traps.rs +++ b/src/traps.rs @@ -100,6 +100,8 @@ pub struct Trap { pub aim: f32, // direction of the last shot (fire jet, roller) #[serde(default)] 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 @@ -178,7 +180,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, 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.sfx_ui(Sfx::FlagSet); } @@ -227,6 +229,12 @@ impl Game { 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)? pub fn forge_orders(&self) -> bool { 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); self.traps[k].hp -= dmg; self.traps[k].fired = 0.0; + self.traps[k].sprung = true; self.fx(2, cx, cy, 0.0, 0.0, 0.25, 0xff4080c0); self.sfx(Sfx::Dig, cx, cy); if self.traps[k].kind == TrapKind::MagicDoor && self.traps[k].cd <= 0.0 { @@ -441,6 +450,7 @@ impl Game { if damage > 0.0 { t.hp -= damage * dt; t.fired = 0.0; + t.sprung = true; if t.hp <= 0.0 { wrecked.push(k); } @@ -482,6 +492,7 @@ impl Game { if !hit.is_empty() { self.traps[k].cd = 2.5; self.traps[k].fired = 0.0; + self.traps[k].sprung = true; for j in hit { self.damage_unit(j, 45.0); } @@ -493,6 +504,7 @@ impl Game { if !near(0.9, &self.ents).is_empty() { self.traps[k].cd = 12.0; self.traps[k].fired = 0.0; + self.traps[k].sprung = true; self.clouds.push(Cloud { x: cx, y: cy, t: 0.0 }); self.sfx(Sfx::Magic, cx, cy); } @@ -505,6 +517,7 @@ impl Game { let a = (ey - cy).atan2(ex - cx); self.traps[k].cd = 4.0; self.traps[k].fired = 0.0; + self.traps[k].sprung = true; self.traps[k].aim = a; let (dx, dy) = (a.cos(), a.sin()); 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()) }; self.traps[k].cd = 18.0; self.traps[k].fired = 0.0; + self.traps[k].sprung = true; self.traps[k].aim = dy.atan2(dx); self.rollers.push(Roller { x: cx, y: cy, dx, dy, t: 0.0, hit: Vec::new() }); self.sfx(Sfx::DigDone, cx, cy); @@ -541,6 +555,7 @@ impl Game { if let Some(&j) = seen.first() { self.traps[k].cd = 20.0; self.traps[k].fired = 0.0; + self.traps[k].sprung = true; let target = self.ents[j].id; 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 { @@ -628,7 +643,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, 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; assert!(g.map.walk_cost()(i).is_some()); g.place_trap(TrapKind::WoodDoor, cx + 10, cy); @@ -653,7 +668,7 @@ mod tests { 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.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::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().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"); + } }