Release interceptor engine source at burnout

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
2026-11-02 23:55:29 +03:00
parent 44a023006d
commit e3ca3fbd8c
5 changed files with 34 additions and 5 deletions
+15
View File
@@ -82,6 +82,12 @@ impl Engines {
self.live.is_empty()
}
/// An explicitly unpowered entity releases its source without waiting for the missing-frame grace.
pub fn stop(&mut self, key: u32) -> Option<Op> {
self.dead.remove(&key);
self.live.remove(&key).map(|live| Op::Stop(live.id))
}
/// The game released every source: forget them (nothing to stop).
pub fn reset(&mut self) {
self.live.clear();
@@ -197,6 +203,15 @@ mod tests {
damaged: false,
}
}
#[test]
fn explicit_burnout_releases_the_source_immediately() {
let mut engines = Engines::default();
let ops = engines.step(vec![d(42, 0.0)], false, DVec3::ZERO);
assert!(matches!(ops.as_slice(), [Op::Start(_, _)]));
assert!(matches!(engines.stop(42), Some(Op::Stop(_))));
assert!(engines.is_empty());
assert!(engines.stop(42).is_none());
}
fn starts(ops: &[Op]) -> usize {
ops.iter().filter(|o| matches!(o, Op::Start(..))).count()
}
+8 -5
View File
@@ -715,13 +715,14 @@ impl AirDef {
self.engines.reset();
self.engine_gen = generation;
}
if self.engines.is_empty() && out.engines.is_empty() && !cut {
if self.engines.is_empty() && out.engines.is_empty() && out.engine_stops.is_empty() && !cut {
return;
}
let ops: Vec<render::SourceOp> = self
.engines
.step(std::mem::take(&mut out.engines), cut, ctx.listener)
.into_iter()
let mut changes = self.engines.step(std::mem::take(&mut out.engines), cut, ctx.listener);
for key in std::mem::take(&mut out.engine_stops) {
if let Some(op) = self.engines.stop(key) { changes.push(op); }
}
let ops: Vec<render::SourceOp> = changes.into_iter()
.map(|op| match op {
engines::Op::Start(id, v) => source_op(render::SourceOpKind::Start, id, &v),
engines::Op::Update(id, v) => source_op(render::SourceOpKind::Update, id, &v),
@@ -1949,6 +1950,7 @@ impl Mod for AirDef {
out.particles.clear();
out.sounds.clear();
out.engines.clear();
out.engine_stops.clear();
out.shake = None;
}
playback.tick(&self.sim, &mut self.ground, tick_context.listener, &mut out);
@@ -1968,6 +1970,7 @@ impl Mod for AirDef {
out.particles.clear();
out.sounds.clear();
out.engines.clear();
out.engine_stops.clear();
out.shake = None;
}
playback.tick(&self.sim, &mut self.ground, tick_context.listener, &mut out);
+7
View File
@@ -500,6 +500,8 @@ impl Player {
if rec.missile_is_lit(tr, self.t, kind.lit_after()) {
let (event, gain, pitch) = kind.engine_voice();
out.engine(key, event, at, vel, gain, pitch, false);
} else {
out.engine_stops.push(key);
}
}
What::StrikeCarrier(_) => {
@@ -1146,6 +1148,11 @@ mod engine_tests {
assert!(!rec.missile_is_lit(&rec.tracks[0], 50.0, 5));
assert!(rec.missile_is_lit(&rec.tracks[0], 49.0, 5), "seek back restores the recorded state");
let mut p = player(&rec);
p.rate = 1.0;
p.t = 50.0;
let mut burnout = Out::default();
p.engines(&rec, DVec3::ZERO, false, &mut burnout);
assert!(burnout.engine_stops.contains(&0x8000_0000));
let mut out = Out::default();
p.heard = 18.0;
p.t = 21.0;
+3
View File
@@ -113,6 +113,8 @@ pub struct Out {
/// The engines sounding this tick (persistent sources, `crate::engines`), and whether the
/// picture cut (a replay seek or a new shot): every engine then starts afresh.
pub engines: Vec<EngineDecl>,
/// Sources explicitly stopped this tick (for example, motor burnout).
pub engine_stops: Vec<u32>,
pub engine_cut: bool,
pub commands: Vec<String>,
/// Camera jolt (strength, seconds), the strongest this tick.
@@ -2983,6 +2985,7 @@ impl Sim {
let spec = m.kind.spec();
let target = self.drones.iter().find(|d| d.id == m.target && d.alive());
let lit = m.engine_lit();
if !lit { out.engine_stops.push(0x4000_0000 | m.id); }
let coast = m.age > spec.cold + m.kind.burn_ticks();
if m.age == spec.cold + m.kind.burn_ticks() + 1 {
burnouts.push((m.pos, m.id, m.kind));
+1
View File
@@ -1877,6 +1877,7 @@ fn motor_burnout_reduces_speed_and_stops_powered_effects() {
assert!(powered <= last && sim.missiles[0].vel.length() < last);
assert!(!sim.missiles[0].engine_lit());
assert!(!out.engines.iter().any(|e| e.key == 0x4000_0000 | id));
assert!(out.engine_stops.contains(&(0x4000_0000 | id)));
}
#[test]