embedded_3dgfx/
sector_lights.rs1#[allow(unused_imports)]
4use micromath::F32Ext;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum LightEffectKind {
9 Glow,
10 Random,
11 Alternate,
12}
13
14#[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 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
56pub 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
88pub 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}