Skip to main content

proof_engine/scene/
field_manager.rs

1//! Force field lifecycle manager — creation, TTL expiry, spatial queries, composition.
2//!
3//! The FieldManager owns all active force fields in a scene. Fields can be permanent
4//! or time-limited (TTL). Every frame, expired fields are pruned and the manager
5//! provides efficient queries for field force at a given world position.
6
7use glam::Vec3;
8use crate::math::ForceField;
9use crate::scene::FieldId;
10
11// ── Managed field ─────────────────────────────────────────────────────────────
12
13/// A force field with lifecycle metadata.
14pub struct ManagedField {
15    pub id: FieldId,
16    pub field: ForceField,
17    /// None = permanent, Some(seconds) = expires after this duration.
18    pub ttl: Option<f32>,
19    /// Seconds this field has been alive.
20    pub age: f32,
21    /// Tags for grouping/query filtering.
22    pub tags: Vec<String>,
23    /// Strength multiplier — can be faded over time.
24    pub strength_scale: f32,
25    /// Fade-in duration (ramps strength from 0 to 1 over this time).
26    pub fade_in: f32,
27    /// Fade-out duration (ramps strength from 1 to 0 before expiry).
28    pub fade_out: f32,
29}
30
31impl ManagedField {
32    /// Whether this field has exceeded its TTL.
33    pub fn is_expired(&self) -> bool {
34        self.ttl.map(|ttl| self.age >= ttl).unwrap_or(false)
35    }
36
37    /// Effective strength multiplier accounting for fade-in and fade-out.
38    pub fn effective_scale(&self) -> f32 {
39        let base = self.strength_scale;
40        // Fade in
41        let fade_in_factor = if self.fade_in > 0.0 {
42            (self.age / self.fade_in).min(1.0)
43        } else {
44            1.0
45        };
46        // Fade out
47        let fade_out_factor = if let Some(ttl) = self.ttl {
48            if self.fade_out > 0.0 {
49                let remaining = ttl - self.age;
50                (remaining / self.fade_out).clamp(0.0, 1.0)
51            } else {
52                1.0
53            }
54        } else {
55            1.0
56        };
57        base * fade_in_factor * fade_out_factor
58    }
59
60    /// Returns whether this field has a specific tag.
61    pub fn has_tag(&self, tag: &str) -> bool {
62        self.tags.iter().any(|t| t == tag)
63    }
64}
65
66// ── Field query result ────────────────────────────────────────────────────────
67
68/// The aggregated effect of all fields at a position.
69#[derive(Debug, Clone)]
70pub struct FieldSample {
71    /// Total force vector to apply.
72    pub force: Vec3,
73    /// Total temperature contribution (from HeatSources).
74    pub temperature: f32,
75    /// Total entropy contribution.
76    pub entropy: f32,
77    /// Number of fields that contributed.
78    pub field_count: usize,
79}
80
81impl Default for FieldSample {
82    fn default() -> Self {
83        Self { force: Vec3::ZERO, temperature: 0.0, entropy: 0.0, field_count: 0 }
84    }
85}
86
87// ── Field composition ─────────────────────────────────────────────────────────
88
89/// How multiple fields combine their forces.
90#[derive(Debug, Clone, Copy, PartialEq)]
91pub enum FieldBlend {
92    /// Sum all field forces (default).
93    Additive,
94    /// Average field forces.
95    Average,
96    /// Only the strongest field contributes.
97    Dominant,
98    /// Max force per component.
99    ComponentMax,
100}
101
102// ── Spatial cell for broad-phase culling ─────────────────────────────────────
103
104const CELL_SIZE: f32 = 10.0;
105
106fn world_to_cell(pos: Vec3) -> (i32, i32, i32) {
107    (
108        (pos.x / CELL_SIZE).floor() as i32,
109        (pos.y / CELL_SIZE).floor() as i32,
110        (pos.z / CELL_SIZE).floor() as i32,
111    )
112}
113
114// ── Field manager ─────────────────────────────────────────────────────────────
115
116/// Manages all active force fields in a scene.
117pub struct FieldManager {
118    fields: Vec<ManagedField>,
119    next_id: u32,
120    pub blend_mode: FieldBlend,
121    /// Global force multiplier applied to all fields.
122    pub global_scale: f32,
123}
124
125impl FieldManager {
126    pub fn new() -> Self {
127        Self {
128            fields: Vec::new(),
129            next_id: 1,
130            blend_mode: FieldBlend::Additive,
131            global_scale: 1.0,
132        }
133    }
134
135    // ── Field CRUD ────────────────────────────────────────────────────────────
136
137    /// Add a permanent force field. Returns its ID.
138    pub fn add(&mut self, field: ForceField) -> FieldId {
139        self.add_full(field, None, vec![], 1.0, 0.0, 0.0)
140    }
141
142    /// Add a field with a time-to-live in seconds. Returns its ID.
143    pub fn add_timed(&mut self, field: ForceField, ttl: f32) -> FieldId {
144        self.add_full(field, Some(ttl), vec![], 1.0, 0.0, 0.0)
145    }
146
147    /// Add a field with a TTL and optional fade in/out durations.
148    pub fn add_faded(&mut self, field: ForceField, ttl: f32, fade_in: f32, fade_out: f32) -> FieldId {
149        self.add_full(field, Some(ttl), vec![], 1.0, fade_in, fade_out)
150    }
151
152    /// Add a tagged field.
153    pub fn add_tagged(&mut self, field: ForceField, ttl: Option<f32>, tags: Vec<String>) -> FieldId {
154        self.add_full(field, ttl, tags, 1.0, 0.0, 0.0)
155    }
156
157    fn add_full(
158        &mut self,
159        field: ForceField,
160        ttl: Option<f32>,
161        tags: Vec<String>,
162        strength_scale: f32,
163        fade_in: f32,
164        fade_out: f32,
165    ) -> FieldId {
166        let id = FieldId(self.next_id);
167        self.next_id += 1;
168        self.fields.push(ManagedField {
169            id,
170            field,
171            ttl,
172            age: 0.0,
173            tags,
174            strength_scale,
175            fade_in,
176            fade_out,
177        });
178        id
179    }
180
181    /// Remove a field by ID. Returns true if removed.
182    pub fn remove(&mut self, id: FieldId) -> bool {
183        let before = self.fields.len();
184        self.fields.retain(|f| f.id != id);
185        self.fields.len() < before
186    }
187
188    /// Remove all fields with a specific tag.
189    pub fn remove_tagged(&mut self, tag: &str) {
190        self.fields.retain(|f| !f.has_tag(tag));
191    }
192
193    /// Remove all fields.
194    pub fn clear(&mut self) {
195        self.fields.clear();
196    }
197
198    /// Get a reference to a field by ID.
199    pub fn get(&self, id: FieldId) -> Option<&ManagedField> {
200        self.fields.iter().find(|f| f.id == id)
201    }
202
203    /// Get a mutable reference to a field by ID.
204    pub fn get_mut(&mut self, id: FieldId) -> Option<&mut ManagedField> {
205        self.fields.iter_mut().find(|f| f.id == id)
206    }
207
208    // ── Tick & expiry ─────────────────────────────────────────────────────────
209
210    /// Advance all fields by dt seconds and prune expired ones.
211    pub fn tick(&mut self, dt: f32) {
212        for f in &mut self.fields {
213            f.age += dt;
214        }
215        self.fields.retain(|f| !f.is_expired());
216    }
217
218    // ── Spatial queries ───────────────────────────────────────────────────────
219
220    /// Sample all field effects at a world position.
221    pub fn sample(&self, pos: Vec3, mass: f32, charge: f32, t: f32) -> FieldSample {
222        let mut sample = FieldSample::default();
223        let mut forces: Vec<Vec3> = Vec::new();
224
225        for mf in &self.fields {
226            let scale = mf.effective_scale() * self.global_scale;
227            if scale == 0.0 { continue; }
228
229            let force = mf.field.force_at(pos, mass, charge, t) * scale;
230            let temp  = mf.field.temperature_at(pos) * scale;
231            let entr  = mf.field.entropy_at(pos) * scale;
232
233            sample.temperature += temp;
234            sample.entropy += entr;
235            sample.field_count += 1;
236            forces.push(force);
237        }
238
239        sample.force = combine_forces(&forces, self.blend_mode);
240        sample
241    }
242
243    /// Sample only fields with a specific tag.
244    pub fn sample_tagged(&self, pos: Vec3, tag: &str, mass: f32, charge: f32, t: f32) -> FieldSample {
245        let mut sample = FieldSample::default();
246        let mut forces: Vec<Vec3> = Vec::new();
247
248        for mf in self.fields.iter().filter(|f| f.has_tag(tag)) {
249            let scale = mf.effective_scale() * self.global_scale;
250            if scale == 0.0 { continue; }
251
252            let force = mf.field.force_at(pos, mass, charge, t) * scale;
253            sample.temperature += mf.field.temperature_at(pos) * scale;
254            sample.entropy += mf.field.entropy_at(pos) * scale;
255            sample.field_count += 1;
256            forces.push(force);
257        }
258
259        sample.force = combine_forces(&forces, self.blend_mode);
260        sample
261    }
262
263    /// Returns the pure force vector at a position (no temperature/entropy).
264    pub fn force_at(&self, pos: Vec3, mass: f32, charge: f32, t: f32) -> Vec3 {
265        self.sample(pos, mass, charge, t).force
266    }
267
268    /// Returns all field IDs whose bounding region overlaps `pos` within `radius`.
269    pub fn fields_near(&self, pos: Vec3, radius: f32) -> Vec<FieldId> {
270        self.fields.iter()
271            .filter(|mf| field_center(&mf.field)
272                .map(|c| (c - pos).length() < radius)
273                .unwrap_or(true))
274            .map(|mf| mf.id)
275            .collect()
276    }
277
278    // ── Queries ───────────────────────────────────────────────────────────────
279
280    /// Number of active fields.
281    pub fn len(&self) -> usize { self.fields.len() }
282
283    /// True if no fields are active.
284    pub fn is_empty(&self) -> bool { self.fields.is_empty() }
285
286    /// Iterator over all active managed fields.
287    pub fn iter(&self) -> impl Iterator<Item = &ManagedField> {
288        self.fields.iter()
289    }
290
291    // ── Field interference ────────────────────────────────────────────────────
292
293    /// Compute field interference pattern at pos — how fields amplify/cancel each other.
294    /// Returns a scalar in [-1, 1] representing constructive (+) or destructive (-) interference.
295    pub fn interference_at(&self, pos: Vec3, t: f32) -> f32 {
296        if self.fields.len() < 2 { return 0.0; }
297        let forces: Vec<Vec3> = self.fields.iter()
298            .map(|mf| mf.field.force_at(pos, 1.0, 0.0, t) * mf.effective_scale())
299            .collect();
300        if forces.is_empty() { return 0.0; }
301        let sum: Vec3 = forces.iter().copied().sum();
302        let mag_sum: f32 = forces.iter().map(|f| f.length()).sum();
303        if mag_sum < 0.001 { return 0.0; }
304        // Constructive if aligned (magnitude of sum ≈ sum of magnitudes)
305        // Destructive if opposing (magnitude of sum ≈ 0)
306        (sum.length() / mag_sum) * 2.0 - 1.0
307    }
308
309    /// Resonance score at pos — how much fields oscillate in phase.
310    /// Uses the variance of field force magnitudes.
311    pub fn resonance_at(&self, pos: Vec3, t: f32) -> f32 {
312        let mags: Vec<f32> = self.fields.iter()
313            .map(|mf| mf.field.force_at(pos, 1.0, 0.0, t).length() * mf.effective_scale())
314            .collect();
315        if mags.is_empty() { return 0.0; }
316        let mean = mags.iter().sum::<f32>() / mags.len() as f32;
317        let var  = mags.iter().map(|m| (m - mean).powi(2)).sum::<f32>() / mags.len() as f32;
318        1.0 / (1.0 + var)  // high resonance = low variance
319    }
320}
321
322impl Default for FieldManager {
323    fn default() -> Self { Self::new() }
324}
325
326// ── Helpers ───────────────────────────────────────────────────────────────────
327
328fn combine_forces(forces: &[Vec3], blend: FieldBlend) -> Vec3 {
329    if forces.is_empty() { return Vec3::ZERO; }
330    match blend {
331        FieldBlend::Additive => forces.iter().copied().sum(),
332        FieldBlend::Average  => forces.iter().copied().sum::<Vec3>() / forces.len() as f32,
333        FieldBlend::Dominant => forces.iter().copied()
334            .max_by(|a, b| a.length_squared().partial_cmp(&b.length_squared()).unwrap())
335            .unwrap_or(Vec3::ZERO),
336        FieldBlend::ComponentMax => forces.iter().copied().fold(Vec3::ZERO, |acc, f| {
337            Vec3::new(
338                if f.x.abs() > acc.x.abs() { f.x } else { acc.x },
339                if f.y.abs() > acc.y.abs() { f.y } else { acc.y },
340                if f.z.abs() > acc.z.abs() { f.z } else { acc.z },
341            )
342        }),
343    }
344}
345
346/// Extract the logical center of a field (if it has one).
347fn field_center(field: &ForceField) -> Option<Vec3> {
348    use crate::math::ForceField as FF;
349    match field {
350        FF::Gravity { center, .. }        => Some(*center),
351        FF::Vortex  { center, .. }        => Some(*center),
352        FF::Repulsion { center, .. }      => Some(*center),
353        FF::Electromagnetic { center, .. }=> Some(*center),
354        FF::HeatSource { center, .. }     => Some(*center),
355        FF::MathField { center, .. }      => Some(*center),
356        FF::StrangeAttractor { center, .. }=> Some(*center),
357        FF::EntropyField { center, .. }   => Some(*center),
358        FF::Damping { center, .. }        => Some(*center),
359        FF::Flow { .. }          => None,
360        FF::Pulsing { center, .. }       => Some(*center),
361        FF::Shockwave { center, .. }     => Some(*center),
362        FF::Warp { center, .. }          => Some(*center),
363        FF::Tidal { center, .. }         => Some(*center),
364        FF::MagneticDipole { center, .. }=> Some(*center),
365        FF::Saddle { center, .. }        => Some(*center),
366        FF::Wind { .. }                  => None,
367    }
368}
369
370// ── Tests ─────────────────────────────────────────────────────────────────────
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use crate::math::fields::Falloff;
376
377    #[test]
378    fn add_and_remove() {
379        let mut mgr = FieldManager::new();
380        let id = mgr.add(ForceField::Gravity {
381            center: Vec3::ZERO, strength: 1.0, falloff: Falloff::InverseSquare,
382        });
383        assert_eq!(mgr.len(), 1);
384        assert!(mgr.remove(id));
385        assert_eq!(mgr.len(), 0);
386    }
387
388    #[test]
389    fn timed_field_expires() {
390        let mut mgr = FieldManager::new();
391        mgr.add_timed(ForceField::Flow {
392            direction: Vec3::X, strength: 1.0, turbulence: 0.0,
393        }, 0.5);
394        mgr.tick(0.3);
395        assert_eq!(mgr.len(), 1);
396        mgr.tick(0.3);
397        assert_eq!(mgr.len(), 0);
398    }
399
400    #[test]
401    fn sample_returns_force() {
402        let mut mgr = FieldManager::new();
403        mgr.add(ForceField::Flow { direction: Vec3::X, strength: 2.0, turbulence: 0.0 });
404        let sample = mgr.sample(Vec3::ZERO, 1.0, 0.0, 0.0);
405        assert!(sample.force.x > 0.0);
406    }
407
408    #[test]
409    fn fade_in_scales_force() {
410        let mut mgr = FieldManager::new();
411        mgr.add_faded(
412            ForceField::Flow { direction: Vec3::X, strength: 2.0, turbulence: 0.0 },
413            2.0, 1.0, 0.0,
414        );
415        // At t=0 the field just spawned; effective scale should be near 0
416        let sample_early = mgr.sample(Vec3::ZERO, 1.0, 0.0, 0.0);
417        mgr.tick(1.0);
418        let sample_late = mgr.sample(Vec3::ZERO, 1.0, 0.0, 1.0);
419        assert!(sample_late.force.x > sample_early.force.x);
420    }
421}