Skip to main content

pfx_core/sim/shots/
emit.rs

1use super::super::{Rng, atan2, cos, sin, sqrt};
2
3pub const FULL_TURN: i32 = 360_000;
4
5const PI: f64 = f64::from_bits(0x4009_21FB_5444_2D18);
6const RADIANS: f64 = PI / 180_000.0;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub enum Pattern {
11    Single,
12    Fan { count: u32, arc: i32 },
13    Ring { count: u32 },
14    Spiral { arms: u32, step: i32 },
15    Crown { count: u32, tilt: i32 },
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct Rate {
21    pub charge: u32,
22    pub cost: u32,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct Burst {
28    pub count: u32,
29    pub gap: u32,
30    pub cooldown: u32,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35pub struct Fire {
36    pub pattern: Pattern,
37    pub rate: Rate,
38    pub burst: Option<Burst>,
39    pub windup: u32,
40    pub speed: i32,
41    pub spread: i32,
42}
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct Telegraph {
47    left: u32,
48    total: u32,
49}
50
51impl Telegraph {
52    pub const fn new() -> Self {
53        Self { left: 0, total: 0 }
54    }
55
56    pub fn start(&mut self, ticks: u32) {
57        self.total = ticks.max(1);
58        self.left = self.total;
59    }
60
61    pub fn cancel(&mut self) -> bool {
62        let was = self.is_winding();
63        self.left = 0;
64        self.total = 0;
65        was
66    }
67
68    #[must_use]
69    pub fn tick(&mut self) -> bool {
70        if self.left == 0 {
71            return false;
72        }
73        self.left -= 1;
74        if self.left == 0 {
75            self.total = 0;
76            return true;
77        }
78        false
79    }
80
81    pub const fn is_winding(&self) -> bool {
82        self.left > 0
83    }
84
85    pub const fn left(&self) -> u32 {
86        self.left
87    }
88
89    pub const fn total(&self) -> u32 {
90        self.total
91    }
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96pub enum Phase {
97    Ready,
98    Winding { left: u32, total: u32 },
99    Burst { left: u32, wait: u32 },
100    Cooling { left: u32 },
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105pub enum Aim2 {
106    Angle(i32),
107    At { target: [i32; 2], vel: [i32; 2] },
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub enum Aim3 {
113    Basis {
114        forward: [i32; 3],
115        up: [i32; 3],
116    },
117    At {
118        target: [i32; 3],
119        vel: [i32; 3],
120        up: [i32; 3],
121    },
122}
123
124#[derive(Clone, Debug)]
125#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
126pub struct Emitter {
127    pub fire: Fire,
128    charge: u32,
129    phase: Phase,
130    telegraph: Telegraph,
131    spin: i32,
132    #[cfg_attr(feature = "serde", serde(skip))]
133    locals: Vec<(f64, f64)>,
134}
135
136impl Emitter {
137    pub fn new(fire: Fire) -> Self {
138        Self::with_charge(fire, 0)
139    }
140
141    pub fn with_charge(fire: Fire, charge: u32) -> Self {
142        Self {
143            fire,
144            charge,
145            phase: Phase::Ready,
146            telegraph: Telegraph::new(),
147            spin: 0,
148            locals: Vec::new(),
149        }
150    }
151
152    pub fn phase(&self) -> Phase {
153        match self.phase {
154            Phase::Winding { .. } => Phase::Winding {
155                left: self.telegraph.left(),
156                total: self.telegraph.total(),
157            },
158            phase => phase,
159        }
160    }
161
162    pub const fn charge(&self) -> u32 {
163        self.charge
164    }
165
166    pub const fn spin(&self) -> i32 {
167        self.spin
168    }
169
170    pub fn cancel(&mut self) -> bool {
171        if self.telegraph.cancel() {
172            self.phase = Phase::Ready;
173            return true;
174        }
175        false
176    }
177
178    pub fn reset(&mut self) {
179        self.charge = 0;
180        self.phase = Phase::Ready;
181        self.telegraph.cancel();
182        self.spin = 0;
183    }
184
185    #[must_use]
186    pub fn tick(&mut self, trigger: bool) -> u32 {
187        let cost = self.fire.rate.cost.max(1);
188        match self.phase {
189            Phase::Cooling { left } => {
190                self.phase = if left <= 1 {
191                    Phase::Ready
192                } else {
193                    Phase::Cooling { left: left - 1 }
194                };
195                0
196            }
197            Phase::Winding { .. } => {
198                if self.telegraph.tick() {
199                    self.release()
200                } else {
201                    0
202                }
203            }
204            Phase::Burst { left, wait } => {
205                if wait > 1 {
206                    self.phase = Phase::Burst {
207                        left,
208                        wait: wait - 1,
209                    };
210                    return 0;
211                }
212                self.after_volley(left - 1);
213                1
214            }
215            Phase::Ready => {
216                if !trigger {
217                    if self.charge < cost {
218                        self.charge = self.charge.saturating_add(self.fire.rate.charge).min(cost);
219                    }
220                    return 0;
221                }
222                self.charge = self.charge.saturating_add(self.fire.rate.charge);
223                if self.charge < cost {
224                    return 0;
225                }
226                if self.fire.burst.is_none() && self.fire.windup == 0 {
227                    let volleys = self.charge / cost;
228                    self.charge %= cost;
229                    return volleys;
230                }
231                self.charge -= cost;
232                if self.fire.windup > 0 {
233                    self.telegraph.start(self.fire.windup);
234                    self.phase = Phase::Winding {
235                        left: self.fire.windup,
236                        total: self.fire.windup,
237                    };
238                    return 0;
239                }
240                self.release()
241            }
242        }
243    }
244
245    pub fn tick2(
246        &mut self,
247        origin: [i32; 2],
248        aim: Aim2,
249        trigger: bool,
250        rng: &mut Rng,
251        out: &mut Vec<[i32; 2]>,
252    ) -> u32 {
253        let volleys = self.tick(trigger);
254        for _ in 0..volleys {
255            self.volley2(origin, aim, rng, out);
256        }
257        volleys
258    }
259
260    pub fn tick3(
261        &mut self,
262        origin: [i32; 3],
263        aim: Aim3,
264        trigger: bool,
265        rng: &mut Rng,
266        out: &mut Vec<[i32; 3]>,
267    ) -> u32 {
268        let volleys = self.tick(trigger);
269        for _ in 0..volleys {
270            self.volley3(origin, aim, rng, out);
271        }
272        volleys
273    }
274
275    pub fn volley2(&mut self, origin: [i32; 2], aim: Aim2, rng: &mut Rng, out: &mut Vec<[i32; 2]>) {
276        let base = match aim {
277            Aim2::Angle(angle) => f64::from(angle) * RADIANS,
278            Aim2::At { target, vel } => {
279                let dir = aim_point(origin, target, vel, self.fire.speed);
280                if dir == [0.0; 2] {
281                    0.0
282                } else {
283                    atan2(dir[1], dir[0])
284                }
285            }
286        };
287        self.fill_locals(true);
288        let speed = f64::from(self.fire.speed);
289        for &(tilt, _) in &self.locals {
290            let mut angle = base + tilt * RADIANS;
291            if self.fire.spread > 0 {
292                angle +=
293                    f64::from(rng.range_i32(-self.fire.spread, self.fire.spread + 1)) * RADIANS;
294            }
295            out.push([round(cos(angle) * speed), round(sin(angle) * speed)]);
296        }
297    }
298
299    pub fn volley3(&mut self, origin: [i32; 3], aim: Aim3, rng: &mut Rng, out: &mut Vec<[i32; 3]>) {
300        let (forward, up) = match aim {
301            Aim3::Basis { forward, up } => (forward.map(f64::from), up),
302            Aim3::At { target, vel, up } => (aim_point(origin, target, vel, self.fire.speed), up),
303        };
304        let (f, s, u) = basis(forward, up.map(f64::from));
305        self.fill_locals(false);
306        let speed = f64::from(self.fire.speed);
307        let cone = f64::from(self.fire.spread) * RADIANS;
308        for &(tilt, roll) in &self.locals {
309            let (t, r) = (tilt * RADIANS, roll * RADIANS);
310            let (ct, st, cr, sr) = (cos(t), sin(t), cos(r), sin(r));
311            let mut d: [f64; 3] = std::array::from_fn(|i| ct * f[i] + st * (cr * s[i] + sr * u[i]));
312            if self.fire.spread > 0 {
313                let ca = 1.0 - rng.next_f64() * (1.0 - cos(cone));
314                let sa = sqrt((1.0 - ca * ca).max(0.0));
315                let b = rng.next_f64() * (2.0 * PI);
316                let (_, p, q) = basis(d, u);
317                let (cb, sb) = (cos(b), sin(b));
318                d = std::array::from_fn(|i| ca * d[i] + sa * (cb * p[i] + sb * q[i]));
319            }
320            out.push(d.map(|c| round(c * speed)));
321        }
322    }
323
324    fn release(&mut self) -> u32 {
325        match self.fire.burst {
326            Some(burst) if burst.count > 1 && burst.gap == 0 => {
327                self.after_volley(0);
328                burst.count
329            }
330            Some(burst) if burst.count > 1 => {
331                self.after_volley(burst.count - 1);
332                1
333            }
334            _ => {
335                self.after_volley(0);
336                1
337            }
338        }
339    }
340
341    fn after_volley(&mut self, left: u32) {
342        let burst = self.fire.burst.unwrap_or(Burst {
343            count: 1,
344            gap: 0,
345            cooldown: 0,
346        });
347        self.phase = if left > 0 {
348            Phase::Burst {
349                left,
350                wait: burst.gap,
351            }
352        } else if burst.cooldown > 0 {
353            Phase::Cooling {
354                left: burst.cooldown,
355            }
356        } else {
357            Phase::Ready
358        };
359    }
360
361    fn fill_locals(&mut self, flat: bool) {
362        self.locals.clear();
363        let even = |i: u32, count: u32| f64::from(i) * f64::from(FULL_TURN) / f64::from(count);
364        match self.fire.pattern {
365            Pattern::Single => self.locals.push((0.0, 0.0)),
366            Pattern::Fan { count, arc } => {
367                if count == 1 {
368                    self.locals.push((0.0, 0.0));
369                }
370                if count > 1 {
371                    let arc = f64::from(arc);
372                    let gaps = f64::from(count - 1);
373                    self.locals
374                        .extend((0..count).map(|i| (f64::from(i) * arc / gaps - arc / 2.0, 0.0)));
375                }
376            }
377            Pattern::Ring { count } => self
378                .locals
379                .extend((0..count).map(|i| (even(i, count), 0.0))),
380            Pattern::Spiral { arms, step } => {
381                let spin = f64::from(self.spin);
382                self.locals
383                    .extend((0..arms).map(|i| (spin + even(i, arms), 0.0)));
384                self.spin = wrap(i64::from(self.spin) + i64::from(step));
385            }
386            Pattern::Crown { count, tilt } => {
387                let tilt = f64::from(tilt);
388                if flat {
389                    self.locals.extend(
390                        [(tilt, 0.0), (-tilt, 0.0)]
391                            .into_iter()
392                            .take(count.min(2) as usize),
393                    );
394                } else {
395                    self.locals
396                        .extend((0..count).map(|i| (tilt, even(i, count))));
397                }
398            }
399        }
400    }
401}
402
403pub fn intercept<const N: usize>(
404    from: [i32; N],
405    target: [i32; N],
406    vel: [i32; N],
407    speed: i32,
408) -> Option<f64> {
409    let d: [f64; N] = std::array::from_fn(|i| f64::from(target[i]) - f64::from(from[i]));
410    let v = vel.map(f64::from);
411    let dot = |x: &[f64; N], y: &[f64; N]| (0..N).fold(0.0, |sum, i| sum + x[i] * y[i]);
412    let s = f64::from(speed);
413    let a = dot(&v, &v) - s * s;
414    let b = 2.0 * dot(&d, &v);
415    let c = dot(&d, &d);
416    if c == 0.0 || speed <= 0 {
417        return None;
418    }
419    if a == 0.0 {
420        return (b < 0.0).then(|| -c / b);
421    }
422    let disc = b * b - 4.0 * a * c;
423    if disc < 0.0 {
424        return None;
425    }
426    let root = sqrt(disc);
427    let t1 = (-b - root) / (2.0 * a);
428    let t2 = (-b + root) / (2.0 * a);
429    let (low, high) = if t1 < t2 { (t1, t2) } else { (t2, t1) };
430    if low > 0.0 {
431        Some(low)
432    } else if high > 0.0 {
433        Some(high)
434    } else {
435        None
436    }
437}
438
439fn aim_point<const N: usize>(
440    from: [i32; N],
441    target: [i32; N],
442    vel: [i32; N],
443    speed: i32,
444) -> [f64; N] {
445    let t = intercept(from, target, vel, speed).unwrap_or(0.0);
446    std::array::from_fn(|i| f64::from(target[i]) - f64::from(from[i]) + f64::from(vel[i]) * t)
447}
448
449fn basis(forward: [f64; 3], up: [f64; 3]) -> ([f64; 3], [f64; 3], [f64; 3]) {
450    let f = normalize(forward).unwrap_or([1.0, 0.0, 0.0]);
451    let side = [up, [0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]]
452        .into_iter()
453        .find_map(|hint| normalize(cross(f, hint)))
454        .unwrap_or([0.0, 1.0, 0.0]);
455    (f, side, cross(side, f))
456}
457
458fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
459    [
460        a[1] * b[2] - a[2] * b[1],
461        a[2] * b[0] - a[0] * b[2],
462        a[0] * b[1] - a[1] * b[0],
463    ]
464}
465
466fn normalize(v: [f64; 3]) -> Option<[f64; 3]> {
467    let len = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
468    (len > 0.0 && len.is_finite()).then(|| v.map(|c| c / len))
469}
470
471fn wrap(angle: i64) -> i32 {
472    let turn = i64::from(FULL_TURN);
473    let mut angle = angle % turn;
474    if angle < 0 {
475        angle += turn;
476    }
477    angle as i32
478}
479
480pub(super) fn round(x: f64) -> i32 {
481    x.round() as i32
482}