Skip to main content

proof_engine/curves/
entity_curves.rs

1//! Core data structures for curve-based entities.
2//!
3//! A CurveEntity is a collection of mathematical curves that form a visible
4//! entity in the scene. Each curve responds to force fields, breathing,
5//! damage, and death the same way glyph clusters do.
6
7use glam::{Vec2, Vec3, Vec4};
8use crate::math::MathFunction;
9
10// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11// Curve types
12// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13
14/// The mathematical definition of a curve.
15#[derive(Debug, Clone)]
16pub enum CurveType {
17    /// Bezier curve of arbitrary degree (control points define shape).
18    Bezier { degree: u32 },
19    /// Lissajous figure: x = A*sin(a*t + delta), y = B*sin(b*t).
20    Lissajous { a: f32, b: f32, delta: f32 },
21    /// Parametric curve defined by two MathFunctions: x(t), y(t).
22    Parametric { x_fn: MathFunction, y_fn: MathFunction },
23    /// Circle with optional distortion function applied to radius.
24    Circle { radius: f32, distortion: Option<MathFunction> },
25    /// Spiral: r = rate * theta, with optional decay.
26    Spiral { rate: f32, decay: f32 },
27    /// Rose curve: r = amplitude * cos(k * theta).
28    Rose { k: f32, amplitude: f32 },
29    /// Hypotrochoid: the curve traced by a point on a circle rolling inside another.
30    Hypotrochoid { big_r: f32, small_r: f32, d: f32 },
31    /// Superellipse (Lame curve): |x/a|^n + |y/b|^n = 1.
32    Superellipse { a: f32, b: f32, n: f32 },
33    /// Catenary: y = a * cosh(x/a).
34    Catenary { a: f32, span: f32 },
35}
36
37// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
38// Single curve
39// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
40
41/// A single mathematical curve within an entity.
42#[derive(Debug, Clone)]
43pub struct EntityCurve {
44    /// The mathematical definition.
45    pub curve_type: CurveType,
46    /// Control points (interpretation depends on curve_type).
47    /// For Bezier: these ARE the control points.
48    /// For others: these define offset/scale/anchor positions.
49    pub control_points: Vec<Vec2>,
50    /// Base (unmodified) control points for restoring after deformation.
51    pub base_points: Vec<Vec2>,
52    /// RGBA color of this curve.
53    pub color: Vec4,
54    /// Emission intensity (for glow/bloom).
55    pub emission: f32,
56    /// Line thickness in world units.
57    pub thickness: f32,
58    /// Stiffness: how much this curve resists external forces (higher = more rigid).
59    pub stiffness: f32,
60    /// Number of line segments to tessellate into.
61    pub segment_count: u32,
62    /// Whether this curve is still intact (false = broken by crit/death).
63    pub alive: bool,
64    /// Per-point velocity (for physics response and dissolution).
65    pub point_velocities: Vec<Vec2>,
66    /// Closed curve (connect last point back to first).
67    pub closed: bool,
68    /// Dash pattern: None = solid, Some(on, off) = dashed.
69    pub dash_pattern: Option<(f32, f32)>,
70    /// Layer priority for rendering order.
71    pub layer: u32,
72}
73
74impl EntityCurve {
75    /// Create a new curve with the given type and initial control points.
76    pub fn new(curve_type: CurveType, control_points: Vec<Vec2>) -> Self {
77        let n = control_points.len();
78        Self {
79            base_points: control_points.clone(),
80            control_points,
81            curve_type,
82            color: Vec4::new(0.5, 0.7, 1.0, 0.9),
83            emission: 1.0,
84            thickness: 0.03,
85            stiffness: 1.0,
86            segment_count: 64,
87            alive: true,
88            point_velocities: vec![Vec2::ZERO; n],
89            closed: false,
90            dash_pattern: None,
91            layer: 0,
92        }
93    }
94
95    pub fn with_color(mut self, color: Vec4) -> Self { self.color = color; self }
96    pub fn with_emission(mut self, e: f32) -> Self { self.emission = e; self }
97    pub fn with_thickness(mut self, t: f32) -> Self { self.thickness = t; self }
98    pub fn with_stiffness(mut self, s: f32) -> Self { self.stiffness = s; self }
99    pub fn with_segments(mut self, n: u32) -> Self { self.segment_count = n; self }
100    pub fn with_closed(mut self, c: bool) -> Self { self.closed = c; self }
101    pub fn with_dash(mut self, on: f32, off: f32) -> Self { self.dash_pattern = Some((on, off)); self }
102    pub fn with_layer(mut self, l: u32) -> Self { self.layer = l; self }
103
104    /// Apply an external force impulse to all control points.
105    /// Points with lower stiffness move more.
106    pub fn apply_force(&mut self, force: Vec2) {
107        let inv_stiff = 1.0 / self.stiffness.max(0.01);
108        for vel in &mut self.point_velocities {
109            *vel += force * inv_stiff;
110        }
111    }
112
113    /// Apply a directional impulse from a hit (recoil).
114    /// Points closer to the impact side are affected more.
115    pub fn apply_hit_recoil(&mut self, direction: Vec2, magnitude: f32) {
116        let dir = direction.normalize_or_zero();
117        let inv_stiff = 1.0 / self.stiffness.max(0.01);
118        for (i, pt) in self.control_points.iter().enumerate() {
119            let facing = (pt.normalize_or_zero()).dot(dir);
120            let response = if facing > 0.0 {
121                dir * magnitude * (0.5 + facing * 0.5) * inv_stiff
122            } else {
123                dir * magnitude * 0.2 * inv_stiff
124            };
125            self.point_velocities[i] += response;
126        }
127    }
128
129    /// Step physics: apply velocity, spring-back toward base positions, damping.
130    pub fn step_physics(&mut self, dt: f32, damping: f32) {
131        for i in 0..self.control_points.len() {
132            // Spring toward base position
133            let to_base = self.base_points[i] - self.control_points[i];
134            let spring_force = to_base * self.stiffness * 5.0;
135            self.point_velocities[i] += spring_force * dt;
136
137            // Damping
138            self.point_velocities[i] *= damping;
139
140            // Integrate
141            self.control_points[i] += self.point_velocities[i] * dt;
142        }
143    }
144
145    /// Break this curve (from crit hit or death).
146    pub fn break_curve(&mut self) {
147        self.alive = false;
148        self.stiffness = 0.0;
149    }
150
151    /// Total energy (kinetic) in this curve's control points.
152    pub fn kinetic_energy(&self) -> f32 {
153        self.point_velocities.iter().map(|v| v.length_squared()).sum::<f32>() * 0.5
154    }
155}
156
157// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
158// Curve entity (collection of curves forming one entity)
159// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
160
161/// A complete entity composed of mathematical curves.
162#[derive(Debug, Clone)]
163pub struct CurveEntity {
164    /// All curves composing this entity.
165    pub curves: Vec<EntityCurve>,
166    /// World-space position of this entity.
167    pub position: Vec3,
168    /// Center of mass (computed from control points).
169    pub center_of_mass: Vec2,
170    /// HP ratio (1.0 = full, 0.0 = dead).
171    pub hp_ratio: f32,
172    /// Breathing oscillation phase.
173    pub breath_phase: f32,
174    /// Breathing rate (Hz).
175    pub breath_rate: f32,
176    /// Breathing amplitude (how much curves expand/contract).
177    pub breath_amplitude: f32,
178    /// Accumulated force response (for hit recoil).
179    pub force_response: Vec2,
180    /// Entity name/type.
181    pub name: String,
182    /// Global emission multiplier.
183    pub emission_mult: f32,
184    /// Global damping for all curve physics.
185    pub damping: f32,
186    /// Whether this entity is alive.
187    pub alive: bool,
188    /// Time since death (for dissolution).
189    pub death_time: f32,
190    /// Total accumulated time.
191    pub time: f32,
192    /// Unique ID.
193    pub id: u32,
194}
195
196impl CurveEntity {
197    pub fn new(name: &str, position: Vec3) -> Self {
198        Self {
199            curves: Vec::new(),
200            position,
201            center_of_mass: Vec2::ZERO,
202            hp_ratio: 1.0,
203            breath_phase: 0.0,
204            breath_rate: 0.5,
205            breath_amplitude: 0.03,
206            force_response: Vec2::ZERO,
207            name: name.to_string(),
208            emission_mult: 1.0,
209            damping: 0.92,
210            alive: true,
211            death_time: 0.0,
212            time: 0.0,
213            id: 0,
214        }
215    }
216
217    /// Add a curve to this entity.
218    pub fn add_curve(&mut self, curve: EntityCurve) {
219        self.curves.push(curve);
220    }
221
222    /// Update the entity for one frame.
223    pub fn tick(&mut self, dt: f32) {
224        self.time += dt;
225        self.breath_phase += dt * self.breath_rate * std::f32::consts::TAU;
226
227        // Apply breathing to all curves
228        let breath_scale = 1.0 + self.breath_phase.sin() * self.breath_amplitude;
229        for curve in &mut self.curves {
230            for (i, pt) in curve.control_points.iter_mut().enumerate() {
231                let base = curve.base_points[i];
232                let breathed = base * breath_scale;
233                // Blend toward breathed position (don't override physics)
234                *pt += (breathed - *pt) * 0.1;
235            }
236        }
237
238        // HP degradation: add noise to control points
239        if self.hp_ratio < 1.0 {
240            let noise_amp = (1.0 - self.hp_ratio) * 0.15;
241            for curve in &mut self.curves {
242                for (i, pt) in curve.control_points.iter_mut().enumerate() {
243                    let noise_x = simple_noise(self.time * 3.0 + i as f32 * 1.618) * noise_amp;
244                    let noise_y = simple_noise(self.time * 2.7 + i as f32 * 2.718) * noise_amp;
245                    pt.x += noise_x;
246                    pt.y += noise_y;
247                }
248            }
249        }
250
251        // Step physics for all curves
252        for curve in &mut self.curves {
253            curve.step_physics(dt, self.damping);
254        }
255
256        // Update center of mass
257        self.update_center_of_mass();
258
259        // Decay force response
260        self.force_response *= 0.9;
261
262        // Death timer
263        if !self.alive {
264            self.death_time += dt;
265        }
266    }
267
268    /// Compute center of mass from all control points.
269    pub fn update_center_of_mass(&mut self) {
270        let mut sum = Vec2::ZERO;
271        let mut count = 0u32;
272        for curve in &self.curves {
273            for pt in &curve.control_points {
274                sum += *pt;
275                count += 1;
276            }
277        }
278        if count > 0 {
279            self.center_of_mass = sum / count as f32;
280        }
281    }
282
283    /// Apply a hit from a direction with given damage.
284    pub fn apply_hit(&mut self, direction: Vec2, damage: f32) {
285        self.hp_ratio = (self.hp_ratio - damage / 100.0).max(0.0);
286        self.force_response += direction * damage * 0.01;
287        for curve in &mut self.curves {
288            curve.apply_hit_recoil(direction, damage * 0.5);
289        }
290        if self.hp_ratio <= 0.0 {
291            self.alive = false;
292        }
293    }
294
295    /// Set HP ratio and update emission accordingly.
296    pub fn set_hp(&mut self, hp_ratio: f32) {
297        self.hp_ratio = hp_ratio.clamp(0.0, 1.0);
298        self.emission_mult = 0.3 + hp_ratio * 0.7;
299        if hp_ratio <= 0.0 { self.alive = false; }
300    }
301
302    /// Increase stiffness temporarily (defend).
303    pub fn brace(&mut self, multiplier: f32) {
304        for curve in &mut self.curves {
305            curve.stiffness *= multiplier;
306        }
307    }
308
309    /// Restore original stiffness (end defend).
310    pub fn unbrace(&mut self, multiplier: f32) {
311        for curve in &mut self.curves {
312            curve.stiffness /= multiplier;
313        }
314    }
315
316    /// Break a random curve (crit hit).
317    pub fn break_random_curve(&mut self, rng_seed: u32) {
318        let alive_curves: Vec<usize> = self.curves.iter().enumerate()
319            .filter(|(_, c)| c.alive)
320            .map(|(i, _)| i)
321            .collect();
322        if let Some(&idx) = alive_curves.get(rng_seed as usize % alive_curves.len().max(1)) {
323            self.curves[idx].break_curve();
324        }
325    }
326
327    /// Trigger death dissolution.
328    pub fn die(&mut self) {
329        self.alive = false;
330        self.death_time = 0.0;
331        for curve in &mut self.curves {
332            curve.stiffness = 0.0;
333            // Add random outward velocity to each control point
334            for (i, vel) in curve.point_velocities.iter_mut().enumerate() {
335                let angle = (i as f32 * 2.399) + self.time; // golden angle spread
336                *vel += Vec2::new(angle.cos(), angle.sin()) * 2.0;
337            }
338        }
339    }
340
341    /// Whether the entity has fully dissolved.
342    pub fn is_dissolved(&self) -> bool {
343        !self.alive && self.death_time > 3.0
344    }
345
346    /// Bounding box of all control points.
347    pub fn bounding_box(&self) -> (Vec2, Vec2) {
348        let mut min = Vec2::splat(f32::MAX);
349        let mut max = Vec2::splat(f32::MIN);
350        for curve in &self.curves {
351            for pt in &curve.control_points {
352                min = min.min(*pt);
353                max = max.max(*pt);
354            }
355        }
356        (min, max)
357    }
358
359    /// Number of alive curves.
360    pub fn alive_curve_count(&self) -> usize {
361        self.curves.iter().filter(|c| c.alive).count()
362    }
363
364    /// Total number of control points across all curves.
365    pub fn total_control_points(&self) -> usize {
366        self.curves.iter().map(|c| c.control_points.len()).sum()
367    }
368}
369
370// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
371// Simple noise (deterministic, no dependency)
372// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
373
374fn simple_noise(x: f32) -> f32 {
375    let xi = x.floor() as i32;
376    let xf = x - x.floor();
377    let t = xf * xf * (3.0 - 2.0 * xf);
378    let a = hash_f(xi);
379    let b = hash_f(xi + 1);
380    a + (b - a) * t
381}
382
383fn hash_f(n: i32) -> f32 {
384    let n = (n as u32).wrapping_mul(0x9E3779B9);
385    let n = n ^ (n >> 16);
386    let n = n.wrapping_mul(0x85EBCA6B);
387    (n & 0x00FF_FFFF) as f32 / 0x0080_0000 as f32 - 1.0
388}
389
390// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
391// Tests
392// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[test]
399    fn test_curve_entity_creation() {
400        let mut ent = CurveEntity::new("test", Vec3::ZERO);
401        let curve = EntityCurve::new(
402            CurveType::Circle { radius: 1.0, distortion: None },
403            vec![Vec2::ZERO],
404        );
405        ent.add_curve(curve);
406        assert_eq!(ent.curves.len(), 1);
407        assert!(ent.alive);
408    }
409
410    #[test]
411    fn test_hit_recoil() {
412        let mut ent = CurveEntity::new("test", Vec3::ZERO);
413        let curve = EntityCurve::new(
414            CurveType::Bezier { degree: 3 },
415            vec![Vec2::ZERO, Vec2::new(1.0, 0.0), Vec2::new(1.0, 1.0), Vec2::new(0.0, 1.0)],
416        );
417        ent.add_curve(curve);
418        ent.apply_hit(Vec2::new(1.0, 0.0), 30.0);
419        assert!(ent.hp_ratio < 1.0);
420        // Some velocity should be applied
421        assert!(ent.curves[0].point_velocities.iter().any(|v| v.length() > 0.0));
422    }
423
424    #[test]
425    fn test_death_dissolution() {
426        let mut ent = CurveEntity::new("test", Vec3::ZERO);
427        ent.add_curve(EntityCurve::new(CurveType::Circle { radius: 1.0, distortion: None }, vec![Vec2::ZERO]));
428        ent.die();
429        assert!(!ent.alive);
430        for _ in 0..200 { ent.tick(1.0 / 60.0); }
431        assert!(ent.is_dissolved());
432    }
433
434    #[test]
435    fn test_bounding_box() {
436        let mut ent = CurveEntity::new("test", Vec3::ZERO);
437        ent.add_curve(EntityCurve::new(
438            CurveType::Bezier { degree: 2 },
439            vec![Vec2::new(-1.0, -1.0), Vec2::new(1.0, 1.0)],
440        ));
441        let (min, max) = ent.bounding_box();
442        assert!(min.x <= -1.0 && min.y <= -1.0);
443        assert!(max.x >= 1.0 && max.y >= 1.0);
444    }
445}