diff --git a/airdef-mod/src/replay.rs b/airdef-mod/src/replay.rs index 5737f5b..9cebbd6 100644 --- a/airdef-mod/src/replay.rs +++ b/airdef-mod/src/replay.rs @@ -535,10 +535,15 @@ impl Player { } } for tr in rec.tracks.iter().filter(|tr| (self.heard..self.t).contains(&(tr.first as f64))) { - if let What::Missile(kind, _) = tr.what + let event = match tr.what { + What::Missile(kind, _) => Some(kind.launch_sound()), + What::Strike => Some(DroneKind::Kalibr.launch_sound()), + What::Drone(_) => None, + }; + if let Some(event) = event && let Some(p) = tr.at(rec.origin, tr.first as f64) { - out.sound(kind.launch_sound(), heard(p), 1.0, 1.0); + out.sound(event, heard(p), 1.0, 1.0); } } self.heard = self.t; @@ -1102,6 +1107,27 @@ mod engine_tests { let _ = rec; p } + #[test] + fn counter_strike_launch_is_heard_once_and_can_be_replayed_after_seek() { + let mut rec = Recording::blank(100); + rec.straight(1, What::Strike, 10, 70, DVec3::ZERO, DVec3::new(300.0, 70.0, 0.0)); + let mut p = player(&rec); + p.heard = 9.0; + p.t = 11.0; + let mut out = Out::default(); + p.sounds(&rec, DVec3::ZERO, &mut out); + assert_eq!(out.sounds.len(), 1); + assert_eq!(out.sounds[0].0, DroneKind::Kalibr.launch_sound()); + out.sounds.clear(); + p.t = 12.0; + p.sounds(&rec, DVec3::ZERO, &mut out); + assert!(out.sounds.is_empty(), "the loop engine must not repeat the launch"); + p.heard = 9.0; // seek/cut resets the one-shot cursor + p.t = 11.0; + p.sounds(&rec, DVec3::ZERO, &mut out); + assert_eq!(out.sounds.len(), 1); + } + /// A host that remembers which sources run. #[derive(Default)] struct Host(std::collections::BTreeMap); diff --git a/airdef-mod/src/smoke.rs b/airdef-mod/src/smoke.rs index 6591a39..bf76772 100644 --- a/airdef-mod/src/smoke.rs +++ b/airdef-mod/src/smoke.rs @@ -247,6 +247,9 @@ impl Smoke { self.trail((1, m.id), m.pos, tick, look); } } + for f in &sim.strikes.flights { + self.trail((2, f.id), f.pos, tick, drone_look(Class::Cruise, crate::sim::DroneKind::Kalibr.spec().trail_scale())); + } // Forget what is gone. if tick % 100 == 0 { let alive: std::collections::HashSet<(u8, u32)> = sim @@ -254,6 +257,7 @@ impl Smoke { .iter() .map(|d| (0, d.id)) .chain(sim.missiles.iter().map(|m| (1, m.id))) + .chain(sim.strikes.flights.iter().map(|f| (2, f.id))) .collect(); self.last.retain(|k, _| alive.contains(k)); } @@ -286,6 +290,44 @@ mod tests { use super::*; use crate::sim::{DroneKind, MissileKind}; + #[test] + fn live_counter_strike_leaves_a_trail_and_releases_its_source() { + use crate::sim::sites::{LaunchSite, SiteClass}; + use crate::sim::strike::{Estimate, Known, SiteState}; + use glam::DVec2; + let mut sim = Sim::new(11); + let id = 1; + sim.sites.list.push(LaunchSite { + id, side: 0, sector: 0, bearing: 0.0, class: SiteClass::Ramp, + pos: DVec3::new(0.0, 64.0, 300.0), range: 300.0, water: false, + stock: 0, planned: 0, alive: true, next_free: 0, offmap: false, + }); + sim.strikes.known.push(Known { + site: id, class: SiteClass::Ramp, obs: Vec::new(), + est: Some(Estimate { pos: DVec2::new(0.0, 300.0), radius: 5.0, fix: true, + bearing: 0.0, bearing_err: 0.01, origin: DVec2::ZERO }), + launches: 1, last: 0, state: SiteState::Active, cover: None, aimed: false, + }); + sim.order_strike(id, DVec3::new(0.0, 64.0, 0.0), &mut crate::sim::Out::default()).unwrap(); + let mut smoke = Smoke::default(); + sim.tick = 1; + smoke.update(&sim); + let from = sim.strikes.flights[0].pos; + sim.tick = 2; + sim.strikes.flights[0].pos += DVec3::Z * 8.0; + smoke.update(&sim); + let segments = smoke.segments(&sim); + assert_eq!(segments.len(), 1, "a live strike must emit the cruise trail also seen in replay"); + assert_eq!(segments[0].az, from.z); + assert_eq!(segments[0].bz, from.z + 8.0); + let expected = drone_look(Class::Cruise, DroneKind::Kalibr.spec().trail_scale()); + assert_eq!(segments[0].density, expected.density); + sim.strikes.flights.clear(); + sim.tick = 100; + smoke.update(&sim); + assert!(smoke.last.is_empty(), "finished strikes must not leave source bookkeeping behind"); + } + #[test] fn trails_follow_one_rule() { assert!(missile_look(MissileKind::P1sun).is_none());