Skip to main content

proof_engine/entity/
layered_entity.rs

1//! Layered entity rendering — four visual layers peeling away as HP drops.
2//!
3//! Layer 4 (outermost): Particle density aura
4//! Layer 3: Metaball/isosurface primary form
5//! Layer 2: Mathematical curve skeleton
6//! Layer 1 (innermost): SDF glyph identity markers
7//!
8//! At full HP, only the outer layers are visible (solid form with aura).
9//! As damage accumulates, outer layers thin and inner layers emerge.
10//! On death, layers dissolve in sequence from outside in.
11
12use glam::{Vec2, Vec3, Vec4};
13use crate::glyph::{Glyph, GlyphId, RenderLayer, BlendMode};
14use crate::particle::density_entity::{DensityEntity, DensityParticle, ShapeField, ShapeBone};
15use crate::curves::entity_curves::{CurveEntity, EntityCurve, CurveType};
16use crate::curves::tessellate::tessellate_curve;
17use crate::curves::curve_renderer::render_curve_entity;
18use crate::math::{MathFunction, ForceField};
19use std::f32::consts::TAU;
20
21// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22// Layer visibility
23// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
24
25/// Controls opacity of each rendering layer based on HP.
26#[derive(Debug, Clone, Copy)]
27pub struct LayerVisibility {
28    /// Particle density cloud opacity (outermost).
29    pub density_opacity: f32,
30    /// Metaball/isosurface opacity.
31    pub metaball_opacity: f32,
32    /// Mathematical curve skeleton opacity.
33    pub curve_opacity: f32,
34    /// SDF glyph identity markers opacity (innermost).
35    pub glyph_opacity: f32,
36}
37
38impl LayerVisibility {
39    /// Compute layer visibility from HP ratio (0.0-1.0).
40    pub fn from_hp(hp_ratio: f32) -> Self {
41        let hp = hp_ratio.clamp(0.0, 1.0);
42
43        if hp > 0.75 {
44            // Full HP: solid form, no inner layers visible
45            let t = (hp - 0.75) / 0.25; // 0 at 75%, 1 at 100%
46            Self {
47                density_opacity: 0.8 + t * 0.2,
48                metaball_opacity: 0.8 + t * 0.2,
49                curve_opacity: 0.0,
50                glyph_opacity: 0.0,
51            }
52        } else if hp > 0.5 {
53            // Moderate damage: surface thinning, curves emerging
54            let t = (hp - 0.5) / 0.25; // 0 at 50%, 1 at 75%
55            Self {
56                density_opacity: 0.5 + t * 0.3,
57                metaball_opacity: 0.5 + t * 0.3,
58                curve_opacity: (1.0 - t) * 0.3,
59                glyph_opacity: 0.0,
60            }
61        } else if hp > 0.25 {
62            // Heavy damage: holes in surface, curves visible, glyphs starting
63            let t = (hp - 0.25) / 0.25; // 0 at 25%, 1 at 50%
64            Self {
65                density_opacity: 0.2 + t * 0.3,
66                metaball_opacity: 0.2 + t * 0.3,
67                curve_opacity: 0.4 + (1.0 - t) * 0.3,
68                glyph_opacity: (1.0 - t) * 0.3,
69            }
70        } else {
71            // Critical: surface barely exists, curves fraying, glyphs exposed
72            let t = hp / 0.25; // 0 at 0%, 1 at 25%
73            Self {
74                density_opacity: t * 0.2,
75                metaball_opacity: t * 0.2,
76                curve_opacity: 0.5 + (1.0 - t) * 0.5,
77                glyph_opacity: 0.5 + (1.0 - t) * 0.3,
78            }
79        }
80    }
81
82    /// All layers at zero (fully dissolved).
83    pub fn zero() -> Self {
84        Self { density_opacity: 0.0, metaball_opacity: 0.0, curve_opacity: 0.0, glyph_opacity: 0.0 }
85    }
86
87    /// All layers at full (for debug/editor).
88    pub fn full() -> Self {
89        Self { density_opacity: 1.0, metaball_opacity: 1.0, curve_opacity: 1.0, glyph_opacity: 1.0 }
90    }
91}
92
93// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
94// Glyph layer (innermost identity markers)
95// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
96
97/// A 3D-positioned glyph instance for the identity marker layer.
98#[derive(Debug, Clone)]
99pub struct Glyph3DInstance {
100    pub character: char,
101    pub offset: Vec2,
102    pub base_offset: Vec2,
103    pub color: Vec4,
104    pub emission: f32,
105    pub scale: f32,
106    pub velocity: Vec2,
107    pub rotation: f32,
108    pub angular_velocity: f32,
109    pub alive: bool,
110}
111
112impl Glyph3DInstance {
113    pub fn new(character: char, offset: Vec2, color: Vec4) -> Self {
114        Self {
115            character, offset, base_offset: offset,
116            color, emission: 1.5, scale: 0.4,
117            velocity: Vec2::ZERO, rotation: 0.0, angular_velocity: 0.0,
118            alive: true,
119        }
120    }
121}
122
123// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
124// Dissolution state
125// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
126
127/// Tracks the sequenced death dissolution across all layers.
128#[derive(Debug, Clone)]
129pub struct DissolutionState {
130    pub active: bool,
131    pub frame: u32,
132    pub elapsed: f32,
133    /// Which layers have started dissolving.
134    pub density_started: bool,
135    pub metaball_started: bool,
136    pub curves_started: bool,
137    pub glyphs_started: bool,
138    /// Which layers have finished dissolving.
139    pub density_done: bool,
140    pub metaball_done: bool,
141    pub curves_done: bool,
142    pub glyphs_done: bool,
143}
144
145impl DissolutionState {
146    pub fn inactive() -> Self {
147        Self {
148            active: false, frame: 0, elapsed: 0.0,
149            density_started: false, metaball_started: false,
150            curves_started: false, glyphs_started: false,
151            density_done: false, metaball_done: false,
152            curves_done: false, glyphs_done: false,
153        }
154    }
155
156    pub fn is_complete(&self) -> bool {
157        self.density_done && self.metaball_done && self.curves_done && self.glyphs_done
158    }
159}
160
161// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
162// Layered Entity
163// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
164
165/// A multi-layered entity with four visual systems that peel away as HP drops.
166pub struct LayeredEntity {
167    /// Layer 4 (outermost): particle density aura.
168    pub density_layer: Option<DensityEntity>,
169    /// Layer 3: metaball/isosurface primary form.
170    /// (Stored as opacity + emission parameters; actual metaball rendering
171    /// would use the metaball module when the GPU pipeline supports it.)
172    pub metaball_opacity: f32,
173    pub metaball_emission: f32,
174    /// Layer 2: mathematical curve skeleton.
175    pub curve_layer: Option<CurveEntity>,
176    /// Layer 1 (innermost): identity glyph markers.
177    pub glyph_layer: Vec<Glyph3DInstance>,
178
179    /// World position.
180    pub position: Vec3,
181    /// Current HP.
182    pub hp: f32,
183    /// Maximum HP.
184    pub max_hp: f32,
185    /// Computed layer visibility.
186    pub layer_visibility: LayerVisibility,
187    /// Death dissolution state.
188    pub dissolution: DissolutionState,
189    /// Whether the entity is alive.
190    pub alive: bool,
191    /// Entity name.
192    pub name: String,
193    /// Unique ID.
194    pub id: u32,
195    /// Accumulated time.
196    pub time: f32,
197    /// Base color (propagated to all layers).
198    pub base_color: Vec4,
199}
200
201impl LayeredEntity {
202    // ════════════════════════════════════════════════════════════════════════
203    // Construction
204    // ════════════════════════════════════════════════════════════════════════
205
206    pub fn new(name: &str, position: Vec3, max_hp: f32) -> Self {
207        Self {
208            density_layer: None,
209            metaball_opacity: 1.0,
210            metaball_emission: 1.0,
211            curve_layer: None,
212            glyph_layer: Vec::new(),
213            position,
214            hp: max_hp,
215            max_hp,
216            layer_visibility: LayerVisibility::from_hp(1.0),
217            dissolution: DissolutionState::inactive(),
218            alive: true,
219            name: name.to_string(),
220            id: 0,
221            time: 0.0,
222            base_color: Vec4::new(0.5, 0.7, 1.0, 1.0),
223        }
224    }
225
226    /// Set the density (particle cloud) layer.
227    pub fn with_density(mut self, density: DensityEntity) -> Self {
228        self.density_layer = Some(density);
229        self
230    }
231
232    /// Set the curve (mathematical skeleton) layer.
233    pub fn with_curves(mut self, curves: CurveEntity) -> Self {
234        self.curve_layer = Some(curves);
235        self
236    }
237
238    /// Add a glyph identity marker.
239    pub fn with_glyph(mut self, glyph: Glyph3DInstance) -> Self {
240        self.glyph_layer.push(glyph);
241        self
242    }
243
244    /// Add multiple glyph markers at once.
245    pub fn with_glyphs(mut self, glyphs: Vec<Glyph3DInstance>) -> Self {
246        self.glyph_layer = glyphs;
247        self
248    }
249
250    pub fn with_color(mut self, color: Vec4) -> Self {
251        self.base_color = color;
252        self
253    }
254
255    // ════════════════════════════════════════════════════════════════════════
256    // Update
257    // ════════════════════════════════════════════════════════════════════════
258
259    /// Main update: advance all layers, compute visibility, handle dissolution.
260    pub fn update(&mut self, dt: f32) {
261        self.time += dt;
262
263        if self.dissolution.active {
264            self.dissolve_update(dt);
265            return;
266        }
267
268        // Compute layer visibility from HP
269        let hp_ratio = (self.hp / self.max_hp).clamp(0.0, 1.0);
270        self.layer_visibility = LayerVisibility::from_hp(hp_ratio);
271
272        // Update density layer
273        if let Some(ref mut density) = self.density_layer {
274            density.hp_ratio = hp_ratio;
275            density.tick(dt);
276            // Modulate density particle emission by visibility
277            let density_em = self.layer_visibility.density_opacity;
278            for p in &mut density.particles {
279                p.emission = p.emission * density_em;
280            }
281        }
282
283        // Update curve layer
284        if let Some(ref mut curves) = self.curve_layer {
285            curves.hp_ratio = hp_ratio;
286            curves.emission_mult = self.layer_visibility.curve_opacity;
287            curves.tick(dt);
288        }
289
290        // Update metaball parameters
291        self.metaball_opacity = self.layer_visibility.metaball_opacity;
292        self.metaball_emission = self.layer_visibility.metaball_opacity * 1.5;
293
294        // Update glyph layer (spring toward base positions with jitter from HP)
295        let glyph_vis = self.layer_visibility.glyph_opacity;
296        let jitter = (1.0 - hp_ratio) * 0.1;
297        for glyph in &mut self.glyph_layer {
298            if !glyph.alive { continue; }
299            // Spring toward base
300            let to_base = glyph.base_offset - glyph.offset;
301            glyph.velocity += to_base * 5.0 * dt;
302            glyph.velocity *= 0.9; // damping
303            glyph.offset += glyph.velocity * dt;
304            // Jitter
305            glyph.offset.x += hash_noise(self.time + glyph.base_offset.x * 10.0) * jitter;
306            glyph.offset.y += hash_noise(self.time + glyph.base_offset.y * 10.0 + 50.0) * jitter;
307            // Rotation from angular velocity
308            glyph.rotation += glyph.angular_velocity * dt;
309        }
310
311        // Check for death
312        if self.hp <= 0.0 && self.alive {
313            self.begin_dissolution();
314        }
315    }
316
317    // ════════════════════════════════════════════════════════════════════════
318    // Damage
319    // ════════════════════════════════════════════════════════════════════════
320
321    /// Take damage at an impact point.
322    pub fn take_damage(&mut self, amount: f32, impact_point: Vec2, impact_direction: Vec2) {
323        self.hp = (self.hp - amount).max(0.0);
324
325        // Distribute damage to layers
326        // Density: particles near impact scatter
327        if let Some(ref mut density) = self.density_layer {
328            density.apply_hit(impact_point, amount, 2.0);
329        }
330
331        // Curves: recoil from impact
332        if let Some(ref mut curves) = self.curve_layer {
333            for curve in &mut curves.curves {
334                curve.apply_hit_recoil(impact_direction, amount * 0.3);
335            }
336        }
337
338        // Glyphs: impulse away from impact
339        for glyph in &mut self.glyph_layer {
340            let to_glyph = glyph.offset - impact_point;
341            let dist = to_glyph.length();
342            if dist < 2.0 {
343                let impulse = to_glyph.normalize_or_zero() * amount * 0.02 / (dist + 0.1);
344                glyph.velocity += impulse;
345                glyph.angular_velocity += (hash_noise(glyph.offset.x * 7.0) - 0.5) * amount * 0.05;
346            }
347        }
348
349        if self.hp <= 0.0 && self.alive {
350            self.begin_dissolution();
351        }
352    }
353
354    /// Apply a critical hit: permanent particle loss + curve break.
355    pub fn take_crit(&mut self, amount: f32, impact_point: Vec2, impact_direction: Vec2) {
356        self.take_damage(amount, impact_point, impact_direction);
357
358        // Density: permanently kill particles
359        if let Some(ref mut density) = self.density_layer {
360            density.apply_crit(impact_point, amount);
361        }
362
363        // Curves: break a random curve
364        if let Some(ref mut curves) = self.curve_layer {
365            curves.break_random_curve(self.time as u32);
366        }
367    }
368
369    // ════════════════════════════════════════════════════════════════════════
370    // Force field response
371    // ════════════════════════════════════════════════════════════════════════
372
373    /// Apply an external force to all layers.
374    pub fn apply_force(&mut self, force: Vec2) {
375        if let Some(ref mut density) = self.density_layer {
376            for p in &mut density.particles {
377                if p.alive { p.velocity += force * 0.1; }
378            }
379        }
380        if let Some(ref mut curves) = self.curve_layer {
381            for curve in &mut curves.curves {
382                curve.apply_force(force);
383            }
384        }
385        for glyph in &mut self.glyph_layer {
386            glyph.velocity += force * 0.05;
387        }
388    }
389
390    // ════════════════════════════════════════════════════════════════════════
391    // Death dissolution (sequenced across layers)
392    // ════════════════════════════════════════════════════════════════════════
393
394    fn begin_dissolution(&mut self) {
395        self.alive = false;
396        self.dissolution.active = true;
397        self.dissolution.frame = 0;
398        self.dissolution.elapsed = 0.0;
399    }
400
401    fn dissolve_update(&mut self, dt: f32) {
402        self.dissolution.elapsed += dt;
403        self.dissolution.frame += 1;
404        let frame = self.dissolution.frame;
405
406        // Frame 0-30: density explodes outward
407        if frame <= 30 && !self.dissolution.density_started {
408            self.dissolution.density_started = true;
409            if let Some(ref mut density) = self.density_layer {
410                density.die();
411            }
412        }
413        if frame > 30 {
414            if let Some(ref mut density) = self.density_layer {
415                density.tick(dt);
416                if density.is_dissolved() { self.dissolution.density_done = true; }
417            } else {
418                self.dissolution.density_done = true;
419            }
420        }
421
422        // Frame 15-60: metaball sources decay
423        if frame >= 15 && !self.dissolution.metaball_started {
424            self.dissolution.metaball_started = true;
425        }
426        if frame >= 15 {
427            let t = ((frame - 15) as f32 / 45.0).min(1.0);
428            self.metaball_opacity = (1.0 - t).max(0.0);
429            self.metaball_emission = (1.0 - t) * 2.0; // flash then fade
430            if frame >= 60 { self.dissolution.metaball_done = true; }
431        }
432
433        // Frame 30-90: curves lose stiffness, scatter
434        if frame >= 30 && !self.dissolution.curves_started {
435            self.dissolution.curves_started = true;
436            if let Some(ref mut curves) = self.curve_layer {
437                curves.die();
438            }
439        }
440        if frame >= 30 {
441            if let Some(ref mut curves) = self.curve_layer {
442                curves.tick(dt);
443                if curves.is_dissolved() { self.dissolution.curves_done = true; }
444            } else {
445                self.dissolution.curves_done = true;
446            }
447        }
448
449        // Frame 45-120: glyphs get physics, tumble and scatter
450        if frame >= 45 && !self.dissolution.glyphs_started {
451            self.dissolution.glyphs_started = true;
452            for glyph in &mut self.glyph_layer {
453                let angle = hash_noise(glyph.offset.x * 3.0 + glyph.offset.y * 7.0) * TAU;
454                glyph.velocity += Vec2::new(angle.cos(), angle.sin()) * 2.0;
455                glyph.angular_velocity = (hash_noise(glyph.offset.x * 5.0) - 0.5) * 10.0;
456            }
457        }
458        if frame >= 45 {
459            let glyph_t = ((frame - 45) as f32 / 75.0).min(1.0);
460            for glyph in &mut self.glyph_layer {
461                glyph.velocity *= 0.98;
462                glyph.offset += glyph.velocity * dt;
463                glyph.rotation += glyph.angular_velocity * dt;
464                // Fade
465                glyph.color.w = (1.0 - glyph_t).max(0.0);
466                glyph.emission = (1.0 - glyph_t) * 2.0;
467            }
468            if frame >= 120 { self.dissolution.glyphs_done = true; }
469        }
470
471        // All done after frame 180
472        if frame >= 180 {
473            self.dissolution.density_done = true;
474            self.dissolution.metaball_done = true;
475            self.dissolution.curves_done = true;
476            self.dissolution.glyphs_done = true;
477        }
478
479        // Update layer visibility during dissolution
480        self.layer_visibility = LayerVisibility {
481            density_opacity: if self.dissolution.density_done { 0.0 } else { (1.0 - self.dissolution.elapsed / 1.0).max(0.0) },
482            metaball_opacity: self.metaball_opacity,
483            curve_opacity: if self.dissolution.curves_done { 0.0 } else { (1.0 - (self.dissolution.elapsed - 0.5).max(0.0) / 1.5).max(0.0) },
484            glyph_opacity: if self.dissolution.glyphs_done { 0.0 } else { (1.0 - (self.dissolution.elapsed - 0.75).max(0.0) / 1.5).max(0.0) },
485        };
486    }
487
488    // ════════════════════════════════════════════════════════════════════════
489    // Rendering
490    // ════════════════════════════════════════════════════════════════════════
491
492    /// Render all visible layers by spawning glyphs. Returns glyph count.
493    pub fn render(&self, spawn_fn: &mut dyn FnMut(Glyph) -> GlyphId, dt: f32) -> usize {
494        let mut count = 0;
495        let vis = &self.layer_visibility;
496
497        // Layer 4 (back): Density cloud
498        if vis.density_opacity > 0.01 {
499            if let Some(ref density) = self.density_layer {
500                for p in &density.particles {
501                    if !p.alive { continue; }
502                    let alpha = p.color.w * vis.density_opacity;
503                    if alpha < 0.005 { continue; }
504                    spawn_fn(Glyph {
505                        character: '.', scale: Vec2::splat(p.size * 5.0),
506                        position: Vec3::new(self.position.x + p.position.x, self.position.y + p.position.y, self.position.z - 0.2),
507                        color: Vec4::new(p.color.x, p.color.y, p.color.z, alpha),
508                        emission: p.emission * vis.density_opacity,
509                        glow_color: Vec3::new(p.color.x, p.color.y, p.color.z),
510                        glow_radius: p.emission * 0.3 * vis.density_opacity,
511                        mass: 0.0, lifetime: dt * 1.5,
512                        layer: RenderLayer::Particle, blend_mode: BlendMode::Additive,
513                        ..Default::default()
514                    });
515                    count += 1;
516                }
517            }
518        }
519
520        // Layer 3: Metaball (rendered as a filled shape approximation using dense glyphs)
521        if vis.metaball_opacity > 0.01 {
522            // Approximate metaball as a solid core of overlapping '#' glyphs
523            let mb_count = 20;
524            for i in 0..mb_count {
525                let angle = (i as f32 / mb_count as f32) * TAU;
526                let r = 0.3 * vis.metaball_opacity;
527                let x = r * angle.cos();
528                let y = r * angle.sin() + 0.3;
529                spawn_fn(Glyph {
530                    character: '#', scale: Vec2::splat(0.35 * vis.metaball_opacity),
531                    position: Vec3::new(self.position.x + x, self.position.y + y, self.position.z - 0.1),
532                    color: Vec4::new(self.base_color.x, self.base_color.y, self.base_color.z, vis.metaball_opacity * 0.4),
533                    emission: self.metaball_emission * 0.5,
534                    mass: 0.0, lifetime: dt * 1.5,
535                    layer: RenderLayer::Entity, blend_mode: BlendMode::Additive,
536                    ..Default::default()
537                });
538                count += 1;
539            }
540        }
541
542        // Layer 2: Curves
543        if vis.curve_opacity > 0.01 {
544            if let Some(ref curves) = self.curve_layer {
545                for curve in &curves.curves {
546                    if !curve.alive && curve.kinetic_energy() < 0.001 { continue; }
547                    let polyline = tessellate_curve(curve);
548                    for (i, pt) in polyline.iter().enumerate() {
549                        let t = i as f32 / polyline.len().max(1) as f32;
550                        let alpha = curve.color.w * vis.curve_opacity;
551                        if alpha < 0.005 { continue; }
552                        let ch = if curve.thickness > 0.03 { '*' } else { '.' };
553                        spawn_fn(Glyph {
554                            character: ch, scale: Vec2::splat(curve.thickness * 4.0),
555                            position: Vec3::new(
556                                self.position.x + curves.position.x + pt.x,
557                                self.position.y + curves.position.y + pt.y,
558                                self.position.z,
559                            ),
560                            color: Vec4::new(curve.color.x, curve.color.y, curve.color.z, alpha),
561                            emission: curve.emission * vis.curve_opacity * curves.emission_mult,
562                            mass: 0.0, lifetime: dt * 1.5,
563                            layer: RenderLayer::Entity, blend_mode: BlendMode::Additive,
564                            ..Default::default()
565                        });
566                        count += 1;
567                    }
568                }
569            }
570        }
571
572        // Layer 1 (front): Glyphs
573        if vis.glyph_opacity > 0.01 {
574            for glyph in &self.glyph_layer {
575                if !glyph.alive { continue; }
576                let alpha = glyph.color.w * vis.glyph_opacity;
577                if alpha < 0.005 { continue; }
578                spawn_fn(Glyph {
579                    character: glyph.character, scale: Vec2::splat(glyph.scale),
580                    position: Vec3::new(
581                        self.position.x + glyph.offset.x,
582                        self.position.y + glyph.offset.y,
583                        self.position.z + 0.1,
584                    ),
585                    rotation: glyph.rotation,
586                    color: Vec4::new(glyph.color.x, glyph.color.y, glyph.color.z, alpha),
587                    emission: glyph.emission * vis.glyph_opacity,
588                    glow_color: Vec3::new(glyph.color.x, glyph.color.y, glyph.color.z),
589                    glow_radius: glyph.emission * 0.5 * vis.glyph_opacity,
590                    mass: 0.0, lifetime: dt * 1.5,
591                    layer: RenderLayer::Entity, blend_mode: BlendMode::Additive,
592                    ..Default::default()
593                });
594                count += 1;
595            }
596        }
597
598        count
599    }
600
601    // ════════════════════════════════════════════════════════════════════════
602    // Queries
603    // ════════════════════════════════════════════════════════════════════════
604
605    pub fn hp_ratio(&self) -> f32 { (self.hp / self.max_hp).clamp(0.0, 1.0) }
606
607    pub fn is_dissolved(&self) -> bool {
608        !self.alive && self.dissolution.is_complete()
609    }
610
611    /// Combined bounding box from all layers.
612    pub fn get_bounds(&self) -> (Vec2, Vec2) {
613        let mut min = Vec2::splat(f32::MAX);
614        let mut max = Vec2::splat(f32::MIN);
615
616        if let Some(ref density) = self.density_layer {
617            let (dmin, dmax) = density.bounds();
618            min = min.min(dmin); max = max.max(dmax);
619        }
620        if let Some(ref curves) = self.curve_layer {
621            let (cmin, cmax) = curves.bounding_box();
622            min = min.min(cmin); max = max.max(cmax);
623        }
624        for glyph in &self.glyph_layer {
625            min = min.min(glyph.offset - Vec2::splat(glyph.scale));
626            max = max.max(glyph.offset + Vec2::splat(glyph.scale));
627        }
628
629        (min, max)
630    }
631
632    /// Total alive particle count across all layers.
633    pub fn total_particle_count(&self) -> u32 {
634        let density_count = self.density_layer.as_ref().map(|d| d.alive_count()).unwrap_or(0);
635        let curve_count = self.curve_layer.as_ref().map(|c| c.alive_curve_count() as u32).unwrap_or(0);
636        let glyph_count = self.glyph_layer.iter().filter(|g| g.alive).count() as u32;
637        density_count + curve_count + glyph_count
638    }
639}
640
641// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
642// Builder for common entity configurations
643// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
644
645/// Convenience builders for layered entities.
646pub struct LayeredEntityBuilder;
647
648impl LayeredEntityBuilder {
649    /// Create a fully layered mage entity.
650    pub fn mage(position: Vec3) -> LayeredEntity {
651        use crate::particle::density_entity::*;
652        use crate::particle::shape_templates::DensityTemplates;
653        use crate::curves::templates::CurveTemplates;
654
655        let density = DensityTemplates::mage(position);
656        let curves = CurveTemplates::mage(position);
657        let glyphs = vec![
658            Glyph3DInstance::new('@', Vec2::new(0.0, 0.9), Vec4::new(0.5, 0.7, 1.0, 0.9)),
659            Glyph3DInstance::new('#', Vec2::new(0.0, 0.4), Vec4::new(0.4, 0.6, 0.9, 0.8)),
660            Glyph3DInstance::new('*', Vec2::new(0.7, 1.0), Vec4::new(0.8, 0.8, 1.0, 0.95)),
661            Glyph3DInstance::new('<', Vec2::new(-0.6, 0.4), Vec4::new(0.35, 0.55, 1.0, 0.7)),
662            Glyph3DInstance::new('>', Vec2::new(0.6, 0.4), Vec4::new(0.35, 0.55, 1.0, 0.7)),
663        ];
664
665        LayeredEntity::new("Mage", position, 100.0)
666            .with_density(density)
667            .with_curves(curves)
668            .with_glyphs(glyphs)
669            .with_color(Vec4::new(0.3, 0.5, 1.0, 1.0))
670    }
671
672    /// Create a fully layered boss entity.
673    pub fn boss(position: Vec3) -> LayeredEntity {
674        use crate::particle::density_entity::*;
675        use crate::particle::shape_templates::DensityTemplates;
676        use crate::curves::templates::CurveTemplates;
677
678        let density = DensityTemplates::boss("Chaos Lord", position, Vec4::new(0.9, 0.15, 0.3, 0.9), 3000);
679        let curves = CurveTemplates::boss(position);
680        let glyphs = vec![
681            Glyph3DInstance::new('X', Vec2::new(-0.25, 0.9), Vec4::new(1.0, 0.9, 0.1, 1.0)),
682            Glyph3DInstance::new('X', Vec2::new(0.25, 0.9), Vec4::new(1.0, 0.9, 0.1, 1.0)),
683            Glyph3DInstance::new('H', Vec2::new(0.0, 0.0), Vec4::new(0.8, 0.1, 0.2, 0.9)),
684            Glyph3DInstance::new('^', Vec2::new(-0.4, 1.3), Vec4::new(0.9, 0.3, 0.6, 0.8)),
685            Glyph3DInstance::new('^', Vec2::new(0.4, 1.3), Vec4::new(0.9, 0.3, 0.6, 0.8)),
686            Glyph3DInstance::new('v', Vec2::new(0.0, -0.7), Vec4::new(0.7, 0.15, 0.2, 0.7)),
687        ];
688
689        LayeredEntity::new("Chaos Lord", position, 500.0)
690            .with_density(density)
691            .with_curves(curves)
692            .with_glyphs(glyphs)
693            .with_color(Vec4::new(0.9, 0.15, 0.3, 1.0))
694    }
695}
696
697// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
698// Noise helper
699// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
700
701fn hash_noise(x: f32) -> f32 {
702    let n = (x * 374761.393) as i32;
703    let n = (n as u32) ^ ((n as u32) >> 13);
704    let n = n.wrapping_mul(0x5851F42D);
705    (n & 0x00FF_FFFF) as f32 / 0x0080_0000 as f32 - 1.0
706}
707
708// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
709// Tests
710// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    #[test]
717    fn test_layer_visibility_full_hp() {
718        let vis = LayerVisibility::from_hp(1.0);
719        assert!(vis.density_opacity > 0.9);
720        assert!(vis.metaball_opacity > 0.9);
721        assert!(vis.curve_opacity < 0.01);
722        assert!(vis.glyph_opacity < 0.01);
723    }
724
725    #[test]
726    fn test_layer_visibility_half_hp() {
727        let vis = LayerVisibility::from_hp(0.5);
728        assert!(vis.density_opacity > 0.3);
729        assert!(vis.curve_opacity > 0.1);
730    }
731
732    #[test]
733    fn test_layer_visibility_critical() {
734        let vis = LayerVisibility::from_hp(0.1);
735        assert!(vis.density_opacity < 0.15);
736        assert!(vis.curve_opacity > 0.7);
737        assert!(vis.glyph_opacity > 0.5);
738    }
739
740    #[test]
741    fn test_take_damage() {
742        let mut ent = LayeredEntity::new("test", Vec3::ZERO, 100.0);
743        ent.take_damage(30.0, Vec2::ZERO, Vec2::X);
744        assert!((ent.hp - 70.0).abs() < 0.01);
745    }
746
747    #[test]
748    fn test_death_triggers_dissolution() {
749        let mut ent = LayeredEntity::new("test", Vec3::ZERO, 100.0);
750        ent.take_damage(100.0, Vec2::ZERO, Vec2::X);
751        assert!(!ent.alive);
752        assert!(ent.dissolution.active);
753    }
754
755    #[test]
756    fn test_dissolution_completes() {
757        let mut ent = LayeredEntity::new("test", Vec3::ZERO, 100.0);
758        ent.take_damage(100.0, Vec2::ZERO, Vec2::X);
759        for _ in 0..300 { ent.update(1.0 / 60.0); }
760        assert!(ent.is_dissolved());
761    }
762
763    #[test]
764    fn test_builder_mage() {
765        let mage = LayeredEntityBuilder::mage(Vec3::ZERO);
766        assert!(mage.density_layer.is_some());
767        assert!(mage.curve_layer.is_some());
768        assert!(!mage.glyph_layer.is_empty());
769        assert_eq!(mage.hp, 100.0);
770    }
771
772    #[test]
773    fn test_builder_boss() {
774        let boss = LayeredEntityBuilder::boss(Vec3::ZERO);
775        assert!(boss.density_layer.is_some());
776        assert!(boss.curve_layer.is_some());
777        assert!(boss.glyph_layer.len() >= 5);
778        assert_eq!(boss.hp, 500.0);
779    }
780}