Files
airdef/airdef-mod/src/sim/record.rs
T

277 lines
11 KiB
Rust

//! Raids are recorded for their replays: every drone's and interceptor's
//! track (a point every other tick, falling wrecks included) and what
//! happened (shot down, by what; hits; systems lost; the Oreshnik's split).
//! A recording starts with the first drone in the sky and ends when the sky
//! has been clear a while; the last few are kept.
use super::*;
/// Ticks between a track's points.
pub const STEP: u32 = 2;
/// The longest recording (10 minutes) and how many are kept.
const LONGEST: u32 = 12_000;
const KEPT: usize = 3;
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum What {
Drone(DroneKind),
/// An interceptor and the system that fired it.
Missile(MissileKind, u32),
}
pub struct Track {
pub id: u32,
pub what: What,
/// The recording's tick of its first point.
pub first: u32,
/// Its points, `STEP` ticks apart, relative to the recording's origin.
points: Vec<[f32; 3]>,
/// When it was shot down (a drone falling from then on).
pub fell: Option<u32>,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum EventKind {
/// A drone shot down: by which system (its index then), with a missile or the guns.
Kill { drone: u32, kind: DroneKind, sam: u32, sam_kind: SamKind, sam_index: usize, missile: Option<u32> },
/// A drone's warhead went off where it aimed: at an object, a system, or the land.
Hit { drone: u32, kind: DroneKind, power: u32, object: Option<usize>, sam: Option<(SamKind, usize)> },
/// A system destroyed.
SamLost { sam: u32, kind: SamKind, index: usize },
/// The Oreshnik's bus or a block splitting.
Split { drone: u32 },
}
#[derive(Clone, Copy, Debug)]
pub struct Event {
pub at: u32,
pub pos: DVec3,
pub kind: EventKind,
}
pub struct Recording {
/// Which raid of the session (1, 2, …).
pub number: u32,
pub origin: DVec3,
/// Its length in ticks.
pub len: u32,
pub tracks: Vec<Track>,
index: HashMap<u32, usize>,
pub events: Vec<Event>,
/// The systems and objects as they stood: (id, kind, where).
pub sams: Vec<(u32, SamKind, DVec3)>,
pub objects: Vec<DVec3>,
/// Ticks the sky has been clear.
quiet: u32,
}
impl Track {
/// Where it was at `t` (the recording's ticks, fractional), if it was.
pub fn at(&self, origin: DVec3, t: f64) -> Option<DVec3> {
let k = (t - self.first as f64) / STEP as f64;
if k < 0.0 || self.points.is_empty() || k > (self.points.len() - 1) as f64 {
return None;
}
let i = (k.floor() as usize).min(self.points.len() - 1);
let j = (i + 1).min(self.points.len() - 1);
let f = k - i as f64;
let p = |q: [f32; 3]| DVec3::new(q[0] as f64, q[1] as f64, q[2] as f64);
Some(origin + p(self.points[i]).lerp(p(self.points[j]), f))
}
/// Which way it was flying at `t`.
pub fn dir(&self, origin: DVec3, t: f64) -> DVec3 {
let a = self.at(origin, t - STEP as f64).or_else(|| self.at(origin, t));
let b = self.at(origin, t + STEP as f64).or_else(|| self.at(origin, t));
match (a, b) {
(Some(a), Some(b)) => (b - a).normalize_or(DVec3::NEG_Z),
_ => DVec3::NEG_Z,
}
}
/// The tick of its last point.
pub fn last(&self) -> u32 {
self.first + (self.points.len().max(1) as u32 - 1) * STEP
}
/// Its points, every `every`-th, up to tick `t`.
pub fn path(&self, origin: DVec3, every: usize, t: f64) -> Vec<DVec3> {
let p = |q: &[f32; 3]| origin + DVec3::new(q[0] as f64, q[1] as f64, q[2] as f64);
let upto = ((t - self.first as f64) / STEP as f64).floor();
if upto < 0.0 {
return Vec::new();
}
let upto = (upto as usize).min(self.points.len().saturating_sub(1));
let mut out: Vec<DVec3> = self.points[..=upto].iter().step_by(every.max(1)).map(p).collect();
if upto % every.max(1) != 0 {
out.push(p(&self.points[upto]));
}
out
}
}
impl Recording {
fn new(number: u32, sim: &Sim) -> Self {
let origin = sim.targets.first().copied().or_else(|| sim.sams.first().map(|s| s.pos)).unwrap_or(DVec3::ZERO);
Recording {
number,
origin: origin.floor(),
len: 0,
tracks: Vec::new(),
index: HashMap::new(),
events: Vec::new(),
sams: sim.sams.iter().map(|s| (s.id, s.kind, s.pos)).collect(),
objects: sim.targets.clone(),
quiet: 0,
}
}
fn point(&mut self, id: u32, what: What, pos: DVec3) {
let rel = pos - self.origin;
let q = [rel.x as f32, rel.y as f32, rel.z as f32];
let now = self.len;
match self.index.get(&id) {
Some(&i) => self.tracks[i].points.push(q),
None => {
self.index.insert(id, self.tracks.len());
self.tracks.push(Track { id, what, first: now, points: vec![q], fell: None });
}
}
}
pub fn track(&self, id: u32) -> Option<&Track> {
self.index.get(&id).map(|&i| &self.tracks[i])
}
/// The system with this id as it stood.
pub fn sam(&self, id: u32) -> Option<(SamKind, DVec3)> {
self.sams.iter().find(|s| s.0 == id).map(|s| (s.1, s.2))
}
}
impl Sim {
/// The recording of raid `number`, while it is kept.
pub fn replay(&self, number: u32) -> Option<&Recording> {
self.replays.iter().find(|r| r.number == number)
}
}
impl Sim {
/// Something worth a replay happened.
pub(super) fn log(&mut self, pos: DVec3, kind: EventKind) {
if let Some(r) = &mut self.rec {
let at = r.len;
if let EventKind::Kill { drone, .. } = kind
&& let Some(&i) = r.index.get(&drone)
{
r.tracks[i].fell = Some(at);
}
r.events.push(Event { at, pos, kind });
}
}
/// Once a tick: starts a recording with the first drone, adds the tracks' points, ends it
/// when the sky has been clear 5 s.
pub(super) fn record(&mut self) {
// Wrecks still falling (far off they fall a long while) don't keep it going.
let busy = self.drones.iter().any(|d| d.alive()) || !self.missiles.is_empty() || !self.queue.is_empty();
if self.rec.is_none() {
// It starts with the first drone in the sky (not with a raid's drones still waiting).
if !self.drones.iter().any(|d| d.alive()) {
return;
}
self.recordings += 1;
self.rec = Some(Recording::new(self.recordings, self));
}
let Some(r) = &mut self.rec else { return };
if r.len % STEP == 0 {
for d in &self.drones {
r.point(d.id, What::Drone(d.kind), d.pos);
}
for m in &self.missiles {
r.point(m.id, What::Missile(m.kind, m.sam), m.pos);
}
}
r.len += 1;
r.quiet = if busy { 0 } else { r.quiet + 1 };
if r.quiet >= 100 || r.len >= LONGEST {
let mut done = self.rec.take().unwrap();
done.len = done.len.saturating_sub(done.quiet.saturating_sub(40));
if !done.events.is_empty() {
self.replays.push(done);
if self.replays.len() > KEPT {
self.replays.remove(0);
}
}
}
}
/// Draws the recording's drones and interceptors as they were at `t`, as ghosts: ids from `base`.
pub fn draw_ghosts(&self, rec: &Recording, t: f64, base: u32, out: &mut Out) {
for (k, tr) in rec.tracks.iter().enumerate() {
let Some(pos) = tr.at(rec.origin, t) else { continue };
let dir = tr.dir(rec.origin, t);
let id = base + k as u32 * PARTS;
match tr.what {
What::Drone(kind) => {
let spec = kind.spec();
let s = spec.scale;
let falling = tr.fell.is_some_and(|f| t >= f as f64);
let mut rot = look(dir, 0.0);
if falling {
let spin = (t - tr.fell.unwrap_or(0) as f64) * 0.35;
rot = rot * DQuat::from_rotation_z(spin) * DQuat::from_rotation_x(-0.6);
out.model(id, spec.model, pos, rot, s, false);
out.model(id + 1, "airdef:flame", pos, rot, 0.6 + 0.2 * ((t * 1.7).sin() + 1.0), true);
continue;
}
if !matches!(kind, DroneKind::OreshnikBlock | DroneKind::OreshnikSub) {
out.model(id, spec.model, pos, rot, s, false);
}
match spec.engine {
Engine::Prop(back, size) => {
let at = pos + rot * DVec3::new(0.0, 0.0, back * s);
out.model(id + 1, "airdef:shahed_prop", at, rot * DQuat::from_rotation_z(t * 1.1), s * size, false);
}
Engine::Jet(up, back, size) => {
out.model(id + 1, "airdef:geran3_flame", pos + rot * DVec3::new(0.0, up * s, back * s), rot, s * size, true);
}
Engine::Rocket(back, size) => {
if matches!(kind, DroneKind::OreshnikBlock | DroneKind::OreshnikSub) {
out.model(id + 1, "airdef:reentry", pos, rot, s, true);
} else if spec.ballistic.is_none() || t - (tr.first as f64) < 60.0 {
out.model(id + 1, "airdef:flame", pos + rot * DVec3::new(0.0, 0.0, back * s + size), rot, size, true);
} else {
out.model(id + 1, "airdef:reentry", pos, rot, s * 0.6, true);
}
}
}
}
What::Missile(kind, _) => {
let spec = kind.spec();
let rot = look(dir, 0.0) * DQuat::from_rotation_z(t * 0.3);
out.model(id, spec.model, pos, rot, spec.scale, false);
if t - tr.first as f64 > spec.cold as f64 && spec.flame > 0.0 {
let back = (spec.rear - 8.0) / 16.0 * spec.scale + spec.flame;
out.model(id + 1, "airdef:flame", pos + rot * DVec3::new(0.0, 0.0, back), rot, spec.flame, true);
}
}
}
}
// Blasts: a fireball growing and fading for 1.5 s after each.
for (k, e) in rec.events.iter().enumerate() {
let age = t - e.at as f64;
if !(0.0..30.0).contains(&age) {
continue;
}
let size = match e.kind {
EventKind::Kill { .. } => 3.5,
EventKind::Hit { power, .. } => [2.5, 4.5, 7.0, 10.0, 13.0][(power as usize).min(4)],
EventKind::SamLost { .. } => 8.0,
EventKind::Split { .. } => 2.0,
};
let f = age / 30.0;
let s = size * (f * 4.0).min(1.0) * (1.0 - f * f);
let id = base + 0x0080_0000 + k as u32 * PARTS;
let spin = DQuat::from_rotation_y(age * 0.05);
out.model(id, "airdef:fireball", e.pos + DVec3::Y * age * 0.04, spin, s.max(0.01), true);
if age < 4.0 {
out.model(id + 1, "airdef:flash", e.pos, spin, size * 0.6, true);
}
}
}
}