90379f6e17
build / mod (push) Successful in 2m24s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
277 lines
11 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|
|
}
|