1use super::behaviors::BehaviorBlock;
2use super::classes::SimulationMode;
3use super::collision::CollisionPolicy;
4use super::force_fields::ForceBinding;
5use super::promotion::PromotionPolicy;
6use super::render_policy::RenderPolicy;
7use super::semantic_binding::SemanticBinding;
8use serde::{Deserialize, Serialize};
9
10pub type EmitterId = u64;
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub enum SpawnShape {
15 #[default]
16 Point,
17 Sphere {
18 radius: f32,
19 },
20 Box {
21 extents: [f32; 3],
22 },
23 Cone {
24 radius: f32,
25 angle_radians: f32,
26 },
27 Ring {
28 radius: f32,
29 thickness: f32,
30 },
31 Line {
32 length: f32,
33 },
34 MeshSurface {
35 mesh_asset: String,
36 },
37 VolumeTexture {
38 texture_asset: String,
39 },
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub enum SpawnMode {
45 Continuous,
46 Burst,
47 EventDriven,
48 Chained,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct SpawnPolicy {
54 pub mode: SpawnMode,
55 pub rate_per_sec: f32,
56 pub burst_count: u32,
57 pub max_live: u32,
58 pub warmup_seconds: f32,
59 pub looped: bool,
60 pub seed: u64,
61 pub density_scale_lod: bool,
62}
63
64impl Default for SpawnPolicy {
65 fn default() -> Self {
66 Self {
67 mode: SpawnMode::Continuous,
68 rate_per_sec: 50.0,
69 burst_count: 0,
70 max_live: 1000,
71 warmup_seconds: 0.0,
72 looped: true,
73 seed: 0,
74 density_scale_lod: false,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum VelocityInit {
82 Constant([f32; 3]),
83 RandomRange {
84 min: [f32; 3],
85 max: [f32; 3],
86 },
87 ImpulseFromEventNormal {
88 scale: f32,
89 },
90 ConeDirection {
91 direction: [f32; 3],
92 spread_angle: f32,
93 speed: f32,
94 },
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct InitPolicy {
100 pub velocity: VelocityInit,
101 pub angular_velocity_range: Option<([f32; 3], [f32; 3])>,
102 pub color: [f32; 4],
103 pub color_random: bool,
104 pub scale_range: ([f32; 2], [f32; 2]),
105 pub mass_range: (f32, f32),
106}
107
108impl Default for InitPolicy {
109 fn default() -> Self {
110 Self {
111 velocity: VelocityInit::RandomRange {
112 min: [-1.0, 1.0, -1.0],
113 max: [1.0, 3.0, 1.0],
114 },
115 angular_velocity_range: None,
116 color: [1.0, 1.0, 1.0, 1.0],
117 color_random: false,
118 scale_range: ([0.05, 0.05], [0.15, 0.15]),
119 mass_range: (1.0, 1.0),
120 }
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct LifetimePolicy {
127 pub seconds: f32,
128 pub random_range: Option<(f32, f32)>,
129}
130
131impl Default for LifetimePolicy {
132 fn default() -> Self {
133 Self {
134 seconds: 5.0,
135 random_range: None,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct NoisePolicy {
143 pub frequency: f32,
144 pub amplitude: f32,
145 pub octaves: u32,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct SimPolicy {
151 pub mode: SimulationMode,
152 pub gravity: [f32; 3],
153 pub drag: f32,
154 pub damping: f32,
155 pub noise: Option<NoisePolicy>,
156 pub forces: Vec<ForceBinding>,
157 pub lifetime: LifetimePolicy,
158 pub behaviors: Vec<BehaviorBlock>,
159}
160
161impl Default for SimPolicy {
162 fn default() -> Self {
163 Self {
164 mode: SimulationMode::Full,
165 gravity: [0.0, -9.81, 0.0],
166 drag: 0.05,
167 damping: 0.01,
168 noise: None,
169 forces: Vec::new(),
170 lifetime: LifetimePolicy::default(),
171 behaviors: Vec::new(),
172 }
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct BudgetPolicy {
179 pub max_gpu_particles: u32,
180 pub importance: String,
181 pub offscreen_mode: SimulationMode,
182}
183
184impl Default for BudgetPolicy {
185 fn default() -> Self {
186 Self {
187 max_gpu_particles: 1000,
188 importance: "Normal".into(),
189 offscreen_mode: SimulationMode::Simplified,
190 }
191 }
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ReplicationPolicy {
197 pub class: super::classes::ReplicationClass,
198}
199
200impl Default for ReplicationPolicy {
201 fn default() -> Self {
202 Self {
203 class: super::classes::ReplicationClass::None,
204 }
205 }
206}
207
208#[derive(Debug, Clone, Default, Serialize, Deserialize)]
210pub struct EmitterDescriptor {
211 pub id: String,
212 pub label: String,
213 pub shape: SpawnShape,
214 pub spawn: SpawnPolicy,
215 pub initialize: InitPolicy,
216 pub simulate: SimPolicy,
217 pub collision: CollisionPolicy,
218 pub render: RenderPolicy,
219 pub promotion: PromotionPolicy,
220 pub semantics: SemanticBinding,
221 pub budgets: BudgetPolicy,
222 pub replication: ReplicationPolicy,
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn emitter_descriptor_default() {
231 let e = EmitterDescriptor::default();
232 assert!(e.id.is_empty());
233 assert_eq!(e.spawn.rate_per_sec, 50.0);
234 }
235
236 #[test]
237 fn emitter_descriptor_serde_roundtrip() {
238 let mut e = EmitterDescriptor::default();
239 e.id = "test_emitter".into();
240 e.label = "Test Emitter".into();
241 e.shape = SpawnShape::Sphere { radius: 2.0 };
242 let json = serde_json::to_string(&e).unwrap();
243 let restored: EmitterDescriptor = serde_json::from_str(&json).unwrap();
244 assert_eq!(restored.id, "test_emitter");
245 }
246
247 #[test]
248 fn spawn_shape_variants() {
249 let shapes = vec![
250 SpawnShape::Point,
251 SpawnShape::Sphere { radius: 1.0 },
252 SpawnShape::Box {
253 extents: [1.0, 2.0, 3.0],
254 },
255 SpawnShape::Cone {
256 radius: 1.0,
257 angle_radians: 0.5,
258 },
259 SpawnShape::Ring {
260 radius: 2.0,
261 thickness: 0.1,
262 },
263 SpawnShape::Line { length: 5.0 },
264 ];
265 for shape in shapes {
266 let json = serde_json::to_string(&shape).unwrap();
267 let _: SpawnShape = serde_json::from_str(&json).unwrap();
268 }
269 }
270}