Skip to main content

embedded_3dgfx/
sector_lights.rs

1//! Doom-style sector light animation descriptors and evaluators.
2
3#[allow(unused_imports)]
4use micromath::F32Ext;
5
6/// Animated light waveform kind.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum LightEffectKind {
9    Glow,
10    Random,
11    Alternate,
12}
13
14/// Compact per-sector light descriptor.
15///
16/// `base`/`alt` are 0..=255 brightness values.
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct SectorLight {
19    pub base: u8,
20    pub alt: u8,
21    pub speed: f32,
22    pub duration: f32,
23    pub sync: f32,
24    pub effect: Option<LightEffectKind>,
25}
26
27impl SectorLight {
28    /// Constant full-bright light.
29    pub const fn fullbright() -> Self {
30        Self {
31            base: 255,
32            alt: 255,
33            speed: 0.0,
34            duration: 0.0,
35            sync: 0.0,
36            effect: None,
37        }
38    }
39}
40
41#[inline]
42fn fract(x: f32) -> f32 {
43    x - x.floor()
44}
45
46#[inline]
47fn clamp01(x: f32) -> f32 {
48    x.clamp(0.0, 1.0)
49}
50
51#[inline]
52fn noise(sync: f32, time: f32) -> f32 {
53    fract(1.0 + ((sync + time / 1000.0) * 12.9898 + sync * 78.233).sin() * 43758.547)
54}
55
56/// Evaluate a normalized brightness value in `[0.0, 1.0]`.
57pub fn light_level_at(light: &SectorLight, time: f32) -> f32 {
58    let base = light.base as f32 / 255.0;
59    let alt = light.alt as f32 / 255.0;
60    let effect = if let Some(e) = light.effect {
61        e
62    } else {
63        return base;
64    };
65    match effect {
66        LightEffectKind::Glow => {
67            let scale = (base - alt).abs().max(1e-6);
68            let phase = time * light.speed / scale;
69            clamp01((0.5 - fract(phase)).abs() * 2.0 * scale + alt.min(base))
70        }
71        LightEffectKind::Random => {
72            if noise(light.sync, (time * light.speed).floor()) < light.duration {
73                alt
74            } else {
75                base
76            }
77        }
78        LightEffectKind::Alternate => {
79            if fract(time * light.speed + light.sync * 3.5435) < light.duration {
80                alt
81            } else {
82                base
83            }
84        }
85    }
86}
87
88/// Evaluate a byte brightness in `0..=255`.
89pub fn light_level_u8_at(light: &SectorLight, time: f32) -> u8 {
90    (light_level_at(light, time) * 255.0).clamp(0.0, 255.0) as u8
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn static_light_returns_base() {
99        let light = SectorLight {
100            base: 192,
101            alt: 64,
102            speed: 3.0,
103            duration: 0.5,
104            sync: 0.25,
105            effect: None,
106        };
107        assert_eq!(light_level_u8_at(&light, 0.0), 192);
108        assert_eq!(light_level_u8_at(&light, 8.0), 192);
109    }
110
111    #[test]
112    fn random_light_is_deterministic_for_same_input() {
113        let light = SectorLight {
114            base: 200,
115            alt: 80,
116            speed: 20.0,
117            duration: 0.3,
118            sync: 0.11,
119            effect: Some(LightEffectKind::Random),
120        };
121        let a = light_level_u8_at(&light, 1.25);
122        let b = light_level_u8_at(&light, 1.25);
123        assert_eq!(a, b);
124    }
125
126    #[test]
127    fn glow_and_alternate_remain_in_range() {
128        let glow = SectorLight {
129            base: 255,
130            alt: 48,
131            speed: 0.5,
132            duration: 0.0,
133            sync: 0.0,
134            effect: Some(LightEffectKind::Glow),
135        };
136        let alt = SectorLight {
137            base: 255,
138            alt: 96,
139            speed: 2.0,
140            duration: 0.5,
141            sync: 0.1,
142            effect: Some(LightEffectKind::Alternate),
143        };
144        for t in [0.0, 0.1, 0.5, 1.0, 2.0, 4.0] {
145            let g = light_level_u8_at(&glow, t);
146            let a = light_level_u8_at(&alt, t);
147            assert!(g <= 255);
148            assert!(a <= 255);
149        }
150    }
151}