Skip to main content

proof_engine/game/
cloth_rope.rs

1//! Cloth, rope, and soft body game integration for Chaos RPG.
2//!
3//! Provides Verlet-based cloth simulation (`ClothStrip`), chain-based rope
4//! physics (`RopeChain`), spring-mass soft body blobs (`SoftBodyBlob`), and
5//! concrete game-entity wrappers (boss capes, tendrils, robes, soul chains,
6//! slime enemies, quantum blobs, weapon trails, pendulum traps, chest lids).
7//!
8//! All types use `glam::Vec3` for 3D positions and reuse constraint-solving
9//! patterns from `crate::physics::soft_body`.
10
11use glam::Vec3;
12
13// ─────────────────────────────────────────────────────────────────────────────
14// Verlet Point
15// ─────────────────────────────────────────────────────────────────────────────
16
17/// A single Verlet-integrated point mass used by cloth and rope systems.
18#[derive(Debug, Clone)]
19pub struct VerletPoint {
20    pub position: Vec3,
21    pub old_position: Vec3,
22    pub acceleration: Vec3,
23    pub pinned: bool,
24    pub mass: f32,
25}
26
27impl VerletPoint {
28    pub fn new(position: Vec3, mass: f32) -> Self {
29        Self {
30            position,
31            old_position: position,
32            acceleration: Vec3::ZERO,
33            pinned: false,
34            mass,
35        }
36    }
37
38    /// Verlet integration step.
39    pub fn integrate(&mut self, dt: f32) {
40        if self.pinned {
41            return;
42        }
43        let velocity = self.position - self.old_position;
44        self.old_position = self.position;
45        // Verlet: x_new = x + v + a * dt^2 (this used a * dt, so one 16 ms
46        // step under gravity moved a point 0.157 instead of 0.0025).
47        self.position += velocity * 0.999 + self.acceleration * dt * dt;
48        self.acceleration = Vec3::ZERO;
49    }
50
51    pub fn apply_force(&mut self, force: Vec3) {
52        if !self.pinned && self.mass > 0.0 {
53            self.acceleration += force / self.mass;
54        }
55    }
56}
57
58// ─────────────────────────────────────────────────────────────────────────────
59// Distance Constraint
60// ─────────────────────────────────────────────────────────────────────────────
61
62/// A rigid distance constraint between two point indices.
63#[derive(Debug, Clone)]
64pub struct DistanceConstraint {
65    pub a: usize,
66    pub b: usize,
67    pub rest_length: f32,
68    pub active: bool,
69}
70
71impl DistanceConstraint {
72    pub fn new(a: usize, b: usize, rest_length: f32) -> Self {
73        Self {
74            a,
75            b,
76            rest_length,
77            active: true,
78        }
79    }
80
81    /// Satisfy the constraint by moving both endpoints (weighted by pinned state).
82    pub fn satisfy(&self, points: &mut [VerletPoint]) {
83        if !self.active {
84            return;
85        }
86        let pa = points[self.a].position;
87        let pb = points[self.b].position;
88        let delta = pb - pa;
89        let dist = delta.length();
90        if dist < 1e-8 {
91            return;
92        }
93        let diff = (dist - self.rest_length) / dist;
94
95        let pinned_a = points[self.a].pinned;
96        let pinned_b = points[self.b].pinned;
97
98        if pinned_a && pinned_b {
99            return;
100        } else if pinned_a {
101            points[self.b].position -= delta * diff;
102        } else if pinned_b {
103            points[self.a].position += delta * diff;
104        } else {
105            let half = delta * diff * 0.5;
106            points[self.a].position += half;
107            points[self.b].position -= half;
108        }
109    }
110}
111
112// ─────────────────────────────────────────────────────────────────────────────
113// ClothStrip
114// ─────────────────────────────────────────────────────────────────────────────
115
116/// A rectangular grid of Verlet-integrated point masses connected by distance
117/// and bend constraints, simulating cloth or fabric.
118#[derive(Debug, Clone)]
119pub struct ClothStrip {
120    pub points: Vec<VerletPoint>,
121    pub structural_constraints: Vec<DistanceConstraint>,
122    pub bend_constraints: Vec<DistanceConstraint>,
123    pub width: usize,
124    pub height: usize,
125    pub spacing: f32,
126    /// Maximum simulation lifetime in seconds. 0 = infinite.
127    pub lifetime: f32,
128    pub age: f32,
129}
130
131impl ClothStrip {
132    /// Create a cloth grid of `width_points x height_points`, anchored at
133    /// `anchor_pos` (top-left corner). Points are spaced `spacing` apart.
134    pub fn new(width_points: usize, height_points: usize, spacing: f32, anchor_pos: Vec3) -> Self {
135        let mut points = Vec::with_capacity(width_points * height_points);
136        for row in 0..height_points {
137            for col in 0..width_points {
138                let pos = anchor_pos
139                    + Vec3::new(col as f32 * spacing, -(row as f32) * spacing, 0.0);
140                points.push(VerletPoint::new(pos, 1.0));
141            }
142        }
143
144        let idx = |r: usize, c: usize| r * width_points + c;
145
146        // Structural constraints: horizontal + vertical neighbors
147        let mut structural = Vec::new();
148        for r in 0..height_points {
149            for c in 0..width_points {
150                if c + 1 < width_points {
151                    structural.push(DistanceConstraint::new(idx(r, c), idx(r, c + 1), spacing));
152                }
153                if r + 1 < height_points {
154                    structural.push(DistanceConstraint::new(idx(r, c), idx(r + 1, c), spacing));
155                }
156                // Diagonal shear
157                if c + 1 < width_points && r + 1 < height_points {
158                    let diag = spacing * std::f32::consts::SQRT_2;
159                    structural
160                        .push(DistanceConstraint::new(idx(r, c), idx(r + 1, c + 1), diag));
161                    structural
162                        .push(DistanceConstraint::new(idx(r, c + 1), idx(r + 1, c), diag));
163                }
164            }
165        }
166
167        // Bend constraints: skip-one neighbors for stiffness
168        let mut bend = Vec::new();
169        for r in 0..height_points {
170            for c in 0..width_points {
171                if c + 2 < width_points {
172                    bend.push(DistanceConstraint::new(
173                        idx(r, c),
174                        idx(r, c + 2),
175                        spacing * 2.0,
176                    ));
177                }
178                if r + 2 < height_points {
179                    bend.push(DistanceConstraint::new(
180                        idx(r, c),
181                        idx(r + 2, c),
182                        spacing * 2.0,
183                    ));
184                }
185            }
186        }
187
188        Self {
189            points,
190            structural_constraints: structural,
191            bend_constraints: bend,
192            width: width_points,
193            height: height_points,
194            spacing,
195            lifetime: 0.0,
196            age: 0.0,
197        }
198    }
199
200    /// Advance simulation by `dt` with `iterations` constraint relaxation passes.
201    pub fn step(&mut self, dt: f32, iterations: usize) {
202        self.age += dt;
203        // Integrate all points
204        for p in &mut self.points {
205            p.integrate(dt);
206        }
207        // Constraint relaxation
208        for _ in 0..iterations {
209            for c in &self.structural_constraints {
210                c.satisfy(&mut self.points);
211            }
212            for c in &self.bend_constraints {
213                c.satisfy(&mut self.points);
214            }
215        }
216    }
217
218    /// Apply a uniform force to all unpinned points.
219    pub fn apply_force(&mut self, force: Vec3) {
220        for p in &mut self.points {
221            p.apply_force(force);
222        }
223    }
224
225    /// Apply wind with directional bias plus turbulence noise.
226    pub fn apply_wind(&mut self, direction: Vec3, strength: f32, turbulence: f32) {
227        for (i, p) in self.points.iter_mut().enumerate() {
228            // Simple pseudo-turbulence: use point index as seed variation
229            let t = (i as f32 * 0.37).sin() * turbulence;
230            let wind = direction * (strength + t);
231            p.apply_force(wind);
232        }
233    }
234
235    /// Pin a point so it does not move.
236    pub fn pin_point(&mut self, index: usize) {
237        if let Some(p) = self.points.get_mut(index) {
238            p.pinned = true;
239        }
240    }
241
242    /// Unpin a point so it can move freely.
243    pub fn unpin_point(&mut self, index: usize) {
244        if let Some(p) = self.points.get_mut(index) {
245            p.pinned = false;
246        }
247    }
248
249    /// Tear the cloth at a point by deactivating all constraints referencing it.
250    pub fn tear_at(&mut self, index: usize) {
251        for c in &mut self.structural_constraints {
252            if c.a == index || c.b == index {
253                c.active = false;
254            }
255        }
256        for c in &mut self.bend_constraints {
257            if c.a == index || c.b == index {
258                c.active = false;
259            }
260        }
261    }
262
263    /// Return all point positions as `[f32; 3]` arrays for rendering.
264    pub fn get_render_data(&self) -> Vec<[f32; 3]> {
265        self.points
266            .iter()
267            .map(|p| [p.position.x, p.position.y, p.position.z])
268            .collect()
269    }
270
271    /// Move a pinned point to a new position (useful for anchoring to entities).
272    pub fn set_point_position(&mut self, index: usize, pos: Vec3) {
273        if let Some(p) = self.points.get_mut(index) {
274            p.position = pos;
275            p.old_position = pos;
276        }
277    }
278
279    /// Check if this cloth has expired (lifetime > 0 and age exceeded).
280    pub fn is_expired(&self) -> bool {
281        self.lifetime > 0.0 && self.age >= self.lifetime
282    }
283
284    /// Number of active structural constraints.
285    pub fn active_constraint_count(&self) -> usize {
286        self.structural_constraints.iter().filter(|c| c.active).count()
287            + self.bend_constraints.iter().filter(|c| c.active).count()
288    }
289}
290
291// ─────────────────────────────────────────────────────────────────────────────
292// RopeChain
293// ─────────────────────────────────────────────────────────────────────────────
294
295/// A chain of point masses connected by distance constraints, forming a rope.
296#[derive(Debug, Clone)]
297pub struct RopeChain {
298    pub points: Vec<VerletPoint>,
299    pub constraints: Vec<DistanceConstraint>,
300    /// Maximum simulation lifetime in seconds. 0 = infinite.
301    pub lifetime: f32,
302    pub age: f32,
303}
304
305impl RopeChain {
306    /// Create a rope from `start` to `end` with `segments` links.
307    pub fn new(start: Vec3, end: Vec3, segments: usize) -> Self {
308        let seg_count = segments.max(1);
309        let mut points = Vec::with_capacity(seg_count + 1);
310        for i in 0..=seg_count {
311            let t = i as f32 / seg_count as f32;
312            let pos = start.lerp(end, t);
313            points.push(VerletPoint::new(pos, 1.0));
314        }
315
316        let seg_length = (end - start).length() / seg_count as f32;
317        let mut constraints = Vec::with_capacity(seg_count);
318        for i in 0..seg_count {
319            constraints.push(DistanceConstraint::new(i, i + 1, seg_length));
320        }
321
322        Self {
323            points,
324            constraints,
325            lifetime: 0.0,
326            age: 0.0,
327        }
328    }
329
330    /// Step physics: gravity + Verlet integration + constraint solve.
331    pub fn step(&mut self, dt: f32) {
332        self.age += dt;
333        self.apply_gravity();
334        for p in &mut self.points {
335            p.integrate(dt);
336        }
337        // Multiple iterations for stability
338        for _ in 0..8 {
339            for c in &self.constraints {
340                c.satisfy(&mut self.points);
341            }
342        }
343    }
344
345    /// Fix the start point at a position.
346    pub fn attach_start(&mut self, pos: Vec3) {
347        if let Some(p) = self.points.first_mut() {
348            p.pinned = true;
349            p.position = pos;
350            p.old_position = pos;
351        }
352    }
353
354    /// Fix the end point at a position.
355    pub fn attach_end(&mut self, pos: Vec3) {
356        if let Some(p) = self.points.last_mut() {
357            p.pinned = true;
358            p.position = pos;
359            p.old_position = pos;
360        }
361    }
362
363    /// Apply standard gravity to all points.
364    pub fn apply_gravity(&mut self) {
365        let gravity = Vec3::new(0.0, -9.81, 0.0);
366        for p in &mut self.points {
367            p.apply_force(gravity * p.mass);
368        }
369    }
370
371    /// Sever the rope at a segment index, returning a new `RopeChain` for the
372    /// detached portion (from the cut point to the end). The current rope is
373    /// truncated to end at the cut point.
374    pub fn sever_at(&mut self, segment_index: usize) -> Option<RopeChain> {
375        if segment_index >= self.constraints.len() || self.constraints.is_empty() {
376            return None;
377        }
378        // Deactivate the constraint
379        self.constraints[segment_index].active = false;
380
381        // Build a new rope from the severed portion
382        let split_point = segment_index + 1;
383        if split_point >= self.points.len() {
384            return None;
385        }
386
387        let new_points: Vec<VerletPoint> = self.points[split_point..].to_vec();
388        if new_points.len() < 2 {
389            return None;
390        }
391
392        let mut new_constraints = Vec::new();
393        for i in 0..new_points.len() - 1 {
394            let rest = new_points[i]
395                .position
396                .distance(new_points[i + 1].position)
397                .max(0.01);
398            new_constraints.push(DistanceConstraint::new(i, i + 1, rest));
399        }
400
401        // Truncate current rope
402        self.points.truncate(split_point + 1);
403        self.constraints.truncate(segment_index);
404
405        Some(RopeChain {
406            points: new_points,
407            constraints: new_constraints,
408            lifetime: self.lifetime,
409            age: self.age,
410        })
411    }
412
413    /// Get positions of all points.
414    pub fn get_points(&self) -> Vec<Vec3> {
415        self.points.iter().map(|p| p.position).collect()
416    }
417
418    /// Check if this rope has expired.
419    pub fn is_expired(&self) -> bool {
420        self.lifetime > 0.0 && self.age >= self.lifetime
421    }
422
423    /// Total length of the rope (sum of segment distances).
424    pub fn current_length(&self) -> f32 {
425        let mut total = 0.0;
426        for i in 0..self.points.len().saturating_sub(1) {
427            total += self.points[i].position.distance(self.points[i + 1].position);
428        }
429        total
430    }
431
432    /// Apply a force to all points.
433    pub fn apply_force(&mut self, force: Vec3) {
434        for p in &mut self.points {
435            p.apply_force(force);
436        }
437    }
438}
439
440// ─────────────────────────────────────────────────────────────────────────────
441// SoftBodyBlob
442// ─────────────────────────────────────────────────────────────────────────────
443
444/// A spring-connected mesh of points forming a deformable 2D blob (circle of
445/// perimeter points plus a center point).
446#[derive(Debug, Clone)]
447pub struct SoftBodyBlob {
448    pub points: Vec<VerletPoint>,
449    pub constraints: Vec<DistanceConstraint>,
450    pub center_index: usize,
451    pub perimeter_count: usize,
452    pub spring_stiffness: f32,
453    /// Morph target shapes: each is a list of offsets from center.
454    pub shape_a: Vec<Vec3>,
455    pub shape_b: Vec<Vec3>,
456    pub morph_frequency: f32,
457    pub morph_time: f32,
458    pub morphing: bool,
459    /// Maximum simulation lifetime in seconds. 0 = infinite.
460    pub lifetime: f32,
461    pub age: f32,
462}
463
464impl SoftBodyBlob {
465    /// Create a blob centered at `center` with the given `radius` and
466    /// `resolution` (number of perimeter points).
467    pub fn new(center: Vec3, radius: f32, resolution: usize) -> Self {
468        let n = resolution.max(4);
469        let tau = std::f32::consts::TAU;
470
471        let mut points = Vec::with_capacity(n + 1);
472        // Perimeter points
473        for i in 0..n {
474            let angle = i as f32 / n as f32 * tau;
475            let pos = center + Vec3::new(angle.cos() * radius, angle.sin() * radius, 0.0);
476            points.push(VerletPoint::new(pos, 1.0));
477        }
478        // Center point
479        points.push(VerletPoint::new(center, 2.0));
480        let center_idx = n;
481
482        let mut constraints = Vec::new();
483        let default_stiffness_rest = radius; // rest length for spoke = radius
484
485        // Ring constraints (perimeter)
486        let arc_len = tau * radius / n as f32;
487        for i in 0..n {
488            let j = (i + 1) % n;
489            constraints.push(DistanceConstraint::new(i, j, arc_len));
490        }
491
492        // Spoke constraints (perimeter -> center)
493        for i in 0..n {
494            constraints.push(DistanceConstraint::new(i, center_idx, default_stiffness_rest));
495        }
496
497        // Cross-brace constraints (skip-one on ring for rigidity)
498        for i in 0..n {
499            let j = (i + 2) % n;
500            let dist = points[i].position.distance(points[j].position);
501            constraints.push(DistanceConstraint::new(i, j, dist));
502        }
503
504        // Capture initial shape as shape_a
505        let shape_a: Vec<Vec3> = points.iter().map(|p| p.position - center).collect();
506
507        Self {
508            points,
509            constraints,
510            center_index: center_idx,
511            perimeter_count: n,
512            spring_stiffness: 1.0,
513            shape_a: shape_a.clone(),
514            shape_b: shape_a,
515            morph_frequency: 0.0,
516            morph_time: 0.0,
517            morphing: false,
518            lifetime: 0.0,
519            age: 0.0,
520        }
521    }
522
523    /// Step the soft body simulation.
524    pub fn step(&mut self, dt: f32) {
525        self.age += dt;
526        self.morph_time += dt;
527
528        // Apply gravity
529        let gravity = Vec3::new(0.0, -9.81, 0.0) * 0.5;
530        for p in &mut self.points {
531            p.apply_force(gravity * p.mass);
532        }
533
534        // Apply morphing forces toward target shape
535        if self.morphing && self.morph_frequency > 0.0 {
536            let t = (self.morph_time * self.morph_frequency * std::f32::consts::TAU).sin() * 0.5
537                + 0.5;
538            let center_pos = self.points[self.center_index].position;
539            for i in 0..self.points.len() {
540                if i < self.shape_a.len() && i < self.shape_b.len() {
541                    let target_offset = self.shape_a[i].lerp(self.shape_b[i], t);
542                    let target = center_pos + target_offset;
543                    let diff = target - self.points[i].position;
544                    self.points[i].apply_force(diff * self.spring_stiffness * 50.0);
545                }
546            }
547        }
548
549        for p in &mut self.points {
550            p.integrate(dt);
551        }
552
553        // Constraint relaxation
554        let iters = 6;
555        for _ in 0..iters {
556            for c in &self.constraints {
557                c.satisfy(&mut self.points);
558            }
559        }
560    }
561
562    /// Apply a hit that deforms the blob in a direction.
563    pub fn apply_hit(&mut self, direction: Vec3, force: f32) {
564        let dir = if direction.length_squared() > 1e-8 {
565            direction.normalize()
566        } else {
567            Vec3::X
568        };
569        let center_pos = self.points[self.center_index].position;
570        for p in &mut self.points {
571            if p.pinned {
572                continue;
573            }
574            let to_point = p.position - center_pos;
575            // Points facing the hit direction get pushed more
576            let alignment = to_point.normalize_or_zero().dot(dir).max(0.0);
577            p.apply_force(dir * force * (0.3 + alignment * 0.7));
578        }
579    }
580
581    /// Get the perimeter (hull) points for rendering.
582    pub fn get_hull(&self) -> Vec<Vec3> {
583        self.points[..self.perimeter_count]
584            .iter()
585            .map(|p| p.position)
586            .collect()
587    }
588
589    /// Set up oscillation between two shape configurations.
590    /// `shape_a` and `shape_b` are lists of offsets from center for each point.
591    pub fn oscillate_between(&mut self, shape_a: Vec<Vec3>, shape_b: Vec<Vec3>, frequency: f32) {
592        self.shape_a = shape_a;
593        self.shape_b = shape_b;
594        self.morph_frequency = frequency;
595        self.morph_time = 0.0;
596        self.morphing = true;
597    }
598
599    /// Set spring stiffness (affects constraint solving pressure and morph force).
600    pub fn set_stiffness(&mut self, stiffness: f32) {
601        self.spring_stiffness = stiffness.max(0.01);
602    }
603
604    /// Check if this blob has expired.
605    pub fn is_expired(&self) -> bool {
606        self.lifetime > 0.0 && self.age >= self.lifetime
607    }
608
609    /// Get the center position.
610    pub fn center_position(&self) -> Vec3 {
611        self.points[self.center_index].position
612    }
613}
614
615// ═════════════════════════════════════════════════════════════════════════════
616// GAME-SPECIFIC INTEGRATIONS
617// ═════════════════════════════════════════════════════════════════════════════
618
619// ─────────────────────────────────────────────────────────────────────────────
620// BossCape
621// ─────────────────────────────────────────────────────────────────────────────
622
623/// A cloth strip attached to a boss entity. Anchor points (top row) follow
624/// the boss position. Responds to vortex fields and movement.
625#[derive(Debug, Clone)]
626pub struct BossCape {
627    pub cloth: ClothStrip,
628    /// Number of top-row anchor points.
629    pub anchor_count: usize,
630    /// Local offsets of anchor points relative to boss position.
631    pub anchor_offsets: Vec<Vec3>,
632    /// Current boss position.
633    pub boss_position: Vec3,
634    /// Previous boss position (for velocity-based sway).
635    pub prev_boss_position: Vec3,
636}
637
638impl BossCape {
639    /// Create a cape that is `width` points across and `height` points long,
640    /// attached at the boss position.
641    pub fn new(boss_pos: Vec3, width: usize, height: usize, spacing: f32) -> Self {
642        let mut cloth = ClothStrip::new(width, height, spacing, boss_pos);
643
644        // Pin top row
645        let mut anchor_offsets = Vec::with_capacity(width);
646        for c in 0..width {
647            cloth.pin_point(c);
648            anchor_offsets.push(Vec3::new(c as f32 * spacing, 0.0, 0.0));
649        }
650
651        Self {
652            cloth,
653            anchor_count: width,
654            anchor_offsets,
655            boss_position: boss_pos,
656            prev_boss_position: boss_pos,
657        }
658    }
659
660    /// Update boss position and step the cape simulation.
661    pub fn update(&mut self, new_boss_pos: Vec3, dt: f32) {
662        self.prev_boss_position = self.boss_position;
663        self.boss_position = new_boss_pos;
664
665        // Move anchor points to follow boss
666        for (i, offset) in self.anchor_offsets.iter().enumerate() {
667            self.cloth
668                .set_point_position(i, self.boss_position + *offset);
669        }
670
671        // Movement-induced sway: apply force opposite to movement direction
672        let move_delta = self.boss_position - self.prev_boss_position;
673        if move_delta.length_squared() > 1e-6 {
674            let sway = -move_delta.normalize() * move_delta.length() * 20.0;
675            self.cloth.apply_force(sway);
676        }
677
678        // Gravity
679        self.cloth
680            .apply_force(Vec3::new(0.0, -9.81, 0.0));
681
682        self.cloth.step(dt, 5);
683    }
684
685    /// Apply a vortex field (e.g., from a spell). Points closer to `origin`
686    /// receive a tangential swirling force.
687    pub fn apply_vortex(&mut self, origin: Vec3, strength: f32, radius: f32) {
688        for p in &mut self.cloth.points {
689            if p.pinned {
690                continue;
691            }
692            let to_point = p.position - origin;
693            let dist = to_point.length();
694            if dist < radius && dist > 1e-4 {
695                let falloff = 1.0 - dist / radius;
696                // Tangential direction (perpendicular in XY plane)
697                let tangent = Vec3::new(-to_point.y, to_point.x, 0.0).normalize_or_zero();
698                p.apply_force(tangent * strength * falloff);
699            }
700        }
701    }
702
703    /// Get render data.
704    pub fn get_render_data(&self) -> Vec<[f32; 3]> {
705        self.cloth.get_render_data()
706    }
707}
708
709// ─────────────────────────────────────────────────────────────────────────────
710// HydraTendril
711// ─────────────────────────────────────────────────────────────────────────────
712
713/// A rope chain connecting two hydra split instances. The tendril can be
714/// severed when enough damage is dealt to the connection point.
715#[derive(Debug, Clone)]
716pub struct HydraTendril {
717    pub rope: RopeChain,
718    /// Health of the connection. When zero, the tendril is severed.
719    pub health: f32,
720    pub max_health: f32,
721    /// If true, the tendril has been severed.
722    pub severed: bool,
723    /// Position of hydra instance A.
724    pub endpoint_a: Vec3,
725    /// Position of hydra instance B.
726    pub endpoint_b: Vec3,
727}
728
729impl HydraTendril {
730    pub fn new(pos_a: Vec3, pos_b: Vec3, segments: usize, health: f32) -> Self {
731        let mut rope = RopeChain::new(pos_a, pos_b, segments);
732        rope.attach_start(pos_a);
733        rope.attach_end(pos_b);
734
735        Self {
736            rope,
737            health,
738            max_health: health,
739            severed: false,
740            endpoint_a: pos_a,
741            endpoint_b: pos_b,
742        }
743    }
744
745    /// Update endpoints (follow hydra positions) and step physics.
746    pub fn update(&mut self, pos_a: Vec3, pos_b: Vec3, dt: f32) {
747        if self.severed {
748            self.rope.step(dt);
749            return;
750        }
751
752        self.endpoint_a = pos_a;
753        self.endpoint_b = pos_b;
754        self.rope.attach_start(pos_a);
755        self.rope.attach_end(pos_b);
756        self.rope.step(dt);
757    }
758
759    /// Deal damage to the tendril. Returns true if severed.
760    pub fn damage(&mut self, amount: f32) -> bool {
761        if self.severed {
762            return true;
763        }
764        self.health = (self.health - amount).max(0.0);
765        if self.health <= 0.0 {
766            self.sever();
767            return true;
768        }
769        false
770    }
771
772    /// Sever the tendril at the midpoint.
773    fn sever(&mut self) {
774        self.severed = true;
775        let mid = self.rope.constraints.len() / 2;
776        let _ = self.rope.sever_at(mid);
777        // Unpin both ends so the severed pieces fall
778        if let Some(p) = self.rope.points.first_mut() {
779            p.pinned = false;
780        }
781        if let Some(p) = self.rope.points.last_mut() {
782            p.pinned = false;
783        }
784    }
785
786    pub fn get_points(&self) -> Vec<Vec3> {
787        self.rope.get_points()
788    }
789
790    pub fn is_alive(&self) -> bool {
791        !self.severed
792    }
793}
794
795// ─────────────────────────────────────────────────────────────────────────────
796// PlayerRobe
797// ─────────────────────────────────────────────────────────────────────────────
798
799/// 2-3 short cloth strips for the mage class, swaying with player movement.
800#[derive(Debug, Clone)]
801pub struct PlayerRobe {
802    pub strips: Vec<ClothStrip>,
803    /// Local offsets where each strip attaches to the player.
804    pub attachment_offsets: Vec<Vec3>,
805    pub player_position: Vec3,
806    pub prev_player_position: Vec3,
807}
808
809impl PlayerRobe {
810    /// Create a robe with `strip_count` cloth strips (2 or 3), each
811    /// `strip_width` x `strip_height` points.
812    pub fn new(
813        player_pos: Vec3,
814        strip_count: usize,
815        strip_width: usize,
816        strip_height: usize,
817        spacing: f32,
818    ) -> Self {
819        let count = strip_count.clamp(1, 4);
820        let mut strips = Vec::with_capacity(count);
821        let mut offsets = Vec::with_capacity(count);
822
823        // Distribute strips evenly around the player
824        let spread = spacing * strip_width as f32;
825        for i in 0..count {
826            let x_off = (i as f32 - (count as f32 - 1.0) * 0.5) * spread;
827            let offset = Vec3::new(x_off, 0.0, 0.0);
828            let anchor = player_pos + offset;
829            let mut strip = ClothStrip::new(strip_width, strip_height, spacing, anchor);
830            // Pin top row
831            for c in 0..strip_width {
832                strip.pin_point(c);
833            }
834            strips.push(strip);
835            offsets.push(offset);
836        }
837
838        Self {
839            strips,
840            attachment_offsets: offsets,
841            player_position: player_pos,
842            prev_player_position: player_pos,
843        }
844    }
845
846    /// Update player position and step all strips.
847    pub fn update(&mut self, new_pos: Vec3, dt: f32) {
848        self.prev_player_position = self.player_position;
849        self.player_position = new_pos;
850
851        let move_delta = self.player_position - self.prev_player_position;
852
853        for (idx, strip) in self.strips.iter_mut().enumerate() {
854            // Update anchor points
855            let base = self.player_position + self.attachment_offsets[idx];
856            for c in 0..strip.width {
857                strip.set_point_position(c, base + Vec3::new(c as f32 * strip.spacing, 0.0, 0.0));
858            }
859
860            // Sway opposite to movement
861            if move_delta.length_squared() > 1e-6 {
862                let sway = -move_delta.normalize() * move_delta.length() * 15.0;
863                strip.apply_force(sway);
864            }
865
866            strip.apply_force(Vec3::new(0.0, -5.0, 0.0));
867            strip.step(dt, 4);
868        }
869    }
870
871    /// Collect render data from all strips.
872    pub fn get_render_data(&self) -> Vec<Vec<[f32; 3]>> {
873        self.strips.iter().map(|s| s.get_render_data()).collect()
874    }
875}
876
877// ─────────────────────────────────────────────────────────────────────────────
878// NecroSoulChain
879// ─────────────────────────────────────────────────────────────────────────────
880
881/// A rope connecting a necromancer to a recently killed enemy. Dark energy
882/// particles flow along the chain. The chain breaks when the enemy is too
883/// far or the soul is consumed.
884#[derive(Debug, Clone)]
885pub struct NecroSoulChain {
886    pub rope: RopeChain,
887    /// Progress of the soul drain (0.0 = start, 1.0 = fully consumed).
888    pub drain_progress: f32,
889    /// Speed of the drain per second.
890    pub drain_rate: f32,
891    /// Maximum allowed distance before chain snaps.
892    pub max_distance: f32,
893    /// If true, the chain has broken.
894    pub broken: bool,
895    /// Particle positions along the chain (normalized 0..1 along rope length).
896    pub particle_positions: Vec<f32>,
897    /// Particle speed along the chain.
898    pub particle_speed: f32,
899}
900
901impl NecroSoulChain {
902    pub fn new(
903        necro_pos: Vec3,
904        enemy_pos: Vec3,
905        segments: usize,
906        max_distance: f32,
907        drain_rate: f32,
908    ) -> Self {
909        let mut rope = RopeChain::new(necro_pos, enemy_pos, segments);
910        rope.attach_start(necro_pos);
911
912        // Create several particles flowing along the chain
913        let particle_count = 5;
914        let particle_positions = (0..particle_count)
915            .map(|i| i as f32 / particle_count as f32)
916            .collect();
917
918        Self {
919            rope,
920            drain_progress: 0.0,
921            drain_rate,
922            max_distance,
923            broken: false,
924            particle_positions,
925            particle_speed: 1.5,
926        }
927    }
928
929    /// Update chain: move endpoints, step physics, advance drain, check break.
930    pub fn update(&mut self, necro_pos: Vec3, enemy_pos: Vec3, dt: f32) {
931        if self.broken {
932            return;
933        }
934
935        // Check distance
936        let dist = necro_pos.distance(enemy_pos);
937        if dist > self.max_distance {
938            self.broken = true;
939            return;
940        }
941
942        self.rope.attach_start(necro_pos);
943        // Enemy end is free to dangle but biased toward enemy_pos
944        if let Some(p) = self.rope.points.last_mut() {
945            let diff = enemy_pos - p.position;
946            p.apply_force(diff * 30.0);
947        }
948
949        self.rope.step(dt);
950
951        // Advance drain
952        self.drain_progress = (self.drain_progress + self.drain_rate * dt).min(1.0);
953        if self.drain_progress >= 1.0 {
954            self.broken = true;
955        }
956
957        // Advance particles (flow from enemy toward necromancer)
958        for pp in &mut self.particle_positions {
959            *pp -= self.particle_speed * dt;
960            if *pp < 0.0 {
961                *pp += 1.0; // wrap around
962            }
963        }
964    }
965
966    /// Get world-space positions of the dark energy particles.
967    pub fn get_particle_world_positions(&self) -> Vec<Vec3> {
968        let points = self.rope.get_points();
969        if points.len() < 2 {
970            return Vec::new();
971        }
972        self.particle_positions
973            .iter()
974            .map(|t| {
975                let total = points.len() - 1;
976                let f = t * total as f32;
977                let idx = (f as usize).min(total - 1);
978                let frac = f - idx as f32;
979                points[idx].lerp(points[idx + 1], frac)
980            })
981            .collect()
982    }
983
984    pub fn is_active(&self) -> bool {
985        !self.broken
986    }
987
988    pub fn get_points(&self) -> Vec<Vec3> {
989        self.rope.get_points()
990    }
991}
992
993// ─────────────────────────────────────────────────────────────────────────────
994// SlimeEnemy
995// ─────────────────────────────────────────────────────────────────────────────
996
997/// A soft body blob that deforms on hit, jiggles, and reforms. Lower HP
998/// results in more wobbly behavior (lower spring stiffness).
999#[derive(Debug, Clone)]
1000pub struct SlimeEnemy {
1001    pub blob: SoftBodyBlob,
1002    pub hp: f32,
1003    pub max_hp: f32,
1004    /// Base stiffness at full HP.
1005    pub base_stiffness: f32,
1006    /// Minimum stiffness at zero HP.
1007    pub min_stiffness: f32,
1008}
1009
1010impl SlimeEnemy {
1011    pub fn new(center: Vec3, radius: f32, resolution: usize, max_hp: f32) -> Self {
1012        let blob = SoftBodyBlob::new(center, radius, resolution);
1013        Self {
1014            blob,
1015            hp: max_hp,
1016            max_hp,
1017            base_stiffness: 1.0,
1018            min_stiffness: 0.1,
1019        }
1020    }
1021
1022    /// Update the slime simulation each frame.
1023    pub fn update(&mut self, dt: f32) {
1024        // Stiffness scales with HP fraction
1025        let hp_frac = (self.hp / self.max_hp).clamp(0.0, 1.0);
1026        let stiffness = self.min_stiffness + (self.base_stiffness - self.min_stiffness) * hp_frac;
1027        self.blob.set_stiffness(stiffness);
1028        self.blob.step(dt);
1029    }
1030
1031    /// Take damage from a hit in a given direction.
1032    pub fn take_hit(&mut self, direction: Vec3, force: f32, damage: f32) {
1033        self.hp = (self.hp - damage).max(0.0);
1034        self.blob.apply_hit(direction, force);
1035    }
1036
1037    pub fn is_dead(&self) -> bool {
1038        self.hp <= 0.0
1039    }
1040
1041    pub fn get_hull(&self) -> Vec<Vec3> {
1042        self.blob.get_hull()
1043    }
1044
1045    pub fn center_position(&self) -> Vec3 {
1046        self.blob.center_position()
1047    }
1048}
1049
1050// ─────────────────────────────────────────────────────────────────────────────
1051// QuantumBlob
1052// ─────────────────────────────────────────────────────────────────────────────
1053
1054/// A soft body that oscillates between two shapes (Eigenstate boss mechanic).
1055/// The frequency can be configured and represents the quantum superposition
1056/// oscillation.
1057#[derive(Debug, Clone)]
1058pub struct QuantumBlob {
1059    pub blob: SoftBodyBlob,
1060    /// Current eigenstate (0 or 1), toggled on "collapse".
1061    pub eigenstate: u8,
1062    /// Whether currently oscillating (superposition) or collapsed.
1063    pub superposed: bool,
1064}
1065
1066impl QuantumBlob {
1067    /// Create a quantum blob with two shape configurations.
1068    pub fn new(
1069        center: Vec3,
1070        radius: f32,
1071        resolution: usize,
1072        shape_a: Vec<Vec3>,
1073        shape_b: Vec<Vec3>,
1074        frequency: f32,
1075    ) -> Self {
1076        let mut blob = SoftBodyBlob::new(center, radius, resolution);
1077        blob.oscillate_between(shape_a, shape_b, frequency);
1078        Self {
1079            blob,
1080            eigenstate: 0,
1081            superposed: true,
1082        }
1083    }
1084
1085    /// Create with default shapes: circle (shape A) and elongated ellipse (shape B).
1086    pub fn new_default(center: Vec3, radius: f32, resolution: usize, frequency: f32) -> Self {
1087        let n = resolution.max(4);
1088        let tau = std::f32::consts::TAU;
1089
1090        // Shape A: circle
1091        let mut shape_a = Vec::new();
1092        for i in 0..n {
1093            let angle = i as f32 / n as f32 * tau;
1094            shape_a.push(Vec3::new(angle.cos() * radius, angle.sin() * radius, 0.0));
1095        }
1096        shape_a.push(Vec3::ZERO); // center offset
1097
1098        // Shape B: elongated ellipse
1099        let mut shape_b = Vec::new();
1100        for i in 0..n {
1101            let angle = i as f32 / n as f32 * tau;
1102            shape_b.push(Vec3::new(
1103                angle.cos() * radius * 1.5,
1104                angle.sin() * radius * 0.6,
1105                0.0,
1106            ));
1107        }
1108        shape_b.push(Vec3::ZERO);
1109
1110        Self::new(center, radius, resolution, shape_a, shape_b, frequency)
1111    }
1112
1113    /// Step the blob.
1114    pub fn update(&mut self, dt: f32) {
1115        self.blob.step(dt);
1116    }
1117
1118    /// Collapse the superposition to one eigenstate. Stops oscillation.
1119    pub fn collapse(&mut self, state: u8) {
1120        self.superposed = false;
1121        self.eigenstate = state.min(1);
1122        self.blob.morphing = false;
1123    }
1124
1125    /// Resume superposition oscillation.
1126    pub fn enter_superposition(&mut self, frequency: f32) {
1127        self.superposed = true;
1128        self.blob.morph_frequency = frequency;
1129        self.blob.morphing = true;
1130        self.blob.morph_time = 0.0;
1131    }
1132
1133    pub fn get_hull(&self) -> Vec<Vec3> {
1134        self.blob.get_hull()
1135    }
1136
1137    pub fn center_position(&self) -> Vec3 {
1138        self.blob.center_position()
1139    }
1140}
1141
1142// ─────────────────────────────────────────────────────────────────────────────
1143// WeaponTrail
1144// ─────────────────────────────────────────────────────────────────────────────
1145
1146/// A chain of segments that follows a weapon swing path. On impact, segments
1147/// compress (bunch up) then relax back.
1148#[derive(Debug, Clone)]
1149pub struct WeaponTrail {
1150    pub rope: RopeChain,
1151    /// Whether the trail is currently in compressed (impact) state.
1152    pub compressed: bool,
1153    /// Timer for compression recovery.
1154    pub compress_timer: f32,
1155    /// Duration of compression effect.
1156    pub compress_duration: f32,
1157    /// The tip (leading point) of the trail.
1158    pub tip_position: Vec3,
1159}
1160
1161impl WeaponTrail {
1162    pub fn new(start: Vec3, segments: usize, segment_length: f32) -> Self {
1163        let end = start + Vec3::new(segment_length * segments as f32, 0.0, 0.0);
1164        let rope = RopeChain::new(start, end, segments);
1165        Self {
1166            rope,
1167            compressed: false,
1168            compress_timer: 0.0,
1169            compress_duration: 0.2,
1170            tip_position: end,
1171        }
1172    }
1173
1174    /// Update the trail: move the lead point and step physics.
1175    pub fn update(&mut self, weapon_tip: Vec3, dt: f32) {
1176        self.tip_position = weapon_tip;
1177        // The first point follows the weapon tip
1178        self.rope.attach_start(weapon_tip);
1179
1180        // Handle compression recovery
1181        if self.compressed {
1182            self.compress_timer += dt;
1183            if self.compress_timer >= self.compress_duration {
1184                self.compressed = false;
1185                self.compress_timer = 0.0;
1186            }
1187        }
1188
1189        // During compression, temporarily shorten rest lengths
1190        if self.compressed {
1191            let factor = 1.0 - (self.compress_timer / self.compress_duration) * 0.7;
1192            for c in &mut self.rope.constraints {
1193                // We store original rest in the constraint, temporarily reduce
1194                c.rest_length *= factor;
1195            }
1196            self.rope.step(dt);
1197            // Restore
1198            let inv_factor = 1.0 / factor;
1199            for c in &mut self.rope.constraints {
1200                c.rest_length *= inv_factor;
1201            }
1202        } else {
1203            self.rope.step(dt);
1204        }
1205    }
1206
1207    /// Trigger compression (e.g., weapon hit something).
1208    pub fn on_impact(&mut self) {
1209        self.compressed = true;
1210        self.compress_timer = 0.0;
1211    }
1212
1213    pub fn get_points(&self) -> Vec<Vec3> {
1214        self.rope.get_points()
1215    }
1216}
1217
1218// ─────────────────────────────────────────────────────────────────────────────
1219// PendulumTrap
1220// ─────────────────────────────────────────────────────────────────────────────
1221
1222/// A pendulum trap using a rope chain with a fixed top point. Swings under
1223/// gravity and damages entities on contact with the weighted end.
1224#[derive(Debug, Clone)]
1225pub struct PendulumTrap {
1226    pub rope: RopeChain,
1227    /// Damage dealt on contact.
1228    pub damage: f32,
1229    /// Radius of the weight at the end for collision detection.
1230    pub weight_radius: f32,
1231    /// Mass multiplier for the last point (the weight).
1232    pub weight_mass: f32,
1233}
1234
1235impl PendulumTrap {
1236    /// Create a pendulum hanging from `pivot` with the given `length` and
1237    /// `segments`. The weight hangs at the bottom.
1238    pub fn new(pivot: Vec3, length: f32, segments: usize, damage: f32, weight_radius: f32) -> Self {
1239        let bottom = pivot + Vec3::new(0.0, -length, 0.0);
1240        let mut rope = RopeChain::new(pivot, bottom, segments);
1241        rope.attach_start(pivot);
1242
1243        // Make the last point heavier
1244        let weight_mass = 5.0;
1245        if let Some(p) = rope.points.last_mut() {
1246            p.mass = weight_mass;
1247        }
1248
1249        Self {
1250            rope,
1251            damage,
1252            weight_radius,
1253            weight_mass,
1254        }
1255    }
1256
1257    /// Give the pendulum an initial push.
1258    pub fn push(&mut self, force: Vec3) {
1259        if let Some(p) = self.rope.points.last_mut() {
1260            p.apply_force(force);
1261        }
1262    }
1263
1264    /// Step the pendulum simulation.
1265    pub fn update(&mut self, dt: f32) {
1266        self.rope.step(dt);
1267    }
1268
1269    /// Get the weight (endpoint) position for collision checks.
1270    pub fn weight_position(&self) -> Vec3 {
1271        self.rope
1272            .points
1273            .last()
1274            .map(|p| p.position)
1275            .unwrap_or(Vec3::ZERO)
1276    }
1277
1278    /// Check if a point is within the weight's damage radius.
1279    pub fn check_collision(&self, point: Vec3) -> bool {
1280        self.weight_position().distance(point) < self.weight_radius
1281    }
1282
1283    pub fn get_points(&self) -> Vec<Vec3> {
1284        self.rope.get_points()
1285    }
1286}
1287
1288// ─────────────────────────────────────────────────────────────────────────────
1289// TreasureChestLid
1290// ─────────────────────────────────────────────────────────────────────────────
1291
1292/// A simple hinge implemented as a two-segment rope. The hinge point is
1293/// fixed, and the lid swings open when interacted with.
1294#[derive(Debug, Clone)]
1295pub struct TreasureChestLid {
1296    pub rope: RopeChain,
1297    /// Whether the chest is open.
1298    pub is_open: bool,
1299    /// Angle of the lid in radians (0 = closed, ~PI/2 = open).
1300    pub angle: f32,
1301    /// Target angle for the lid.
1302    pub target_angle: f32,
1303    /// Hinge position.
1304    pub hinge_pos: Vec3,
1305    /// Length of the lid.
1306    pub lid_length: f32,
1307}
1308
1309impl TreasureChestLid {
1310    pub fn new(hinge_pos: Vec3, lid_length: f32) -> Self {
1311        let end = hinge_pos + Vec3::new(lid_length, 0.0, 0.0);
1312        let mut rope = RopeChain::new(hinge_pos, end, 2);
1313        rope.attach_start(hinge_pos);
1314
1315        Self {
1316            rope,
1317            is_open: false,
1318            angle: 0.0,
1319            target_angle: 0.0,
1320            hinge_pos,
1321            lid_length,
1322        }
1323    }
1324
1325    /// Open the chest lid.
1326    pub fn open(&mut self) {
1327        self.is_open = true;
1328        self.target_angle = std::f32::consts::FRAC_PI_2;
1329    }
1330
1331    /// Close the chest lid.
1332    pub fn close(&mut self) {
1333        self.is_open = false;
1334        self.target_angle = 0.0;
1335    }
1336
1337    /// Toggle open/close.
1338    pub fn toggle(&mut self) {
1339        if self.is_open {
1340            self.close();
1341        } else {
1342            self.open();
1343        }
1344    }
1345
1346    /// Step the hinge: smoothly interpolate angle toward target, apply as
1347    /// force to the rope endpoint.
1348    pub fn update(&mut self, dt: f32) {
1349        // Smooth interpolation toward target angle
1350        let diff = self.target_angle - self.angle;
1351        self.angle += diff * dt * 5.0;
1352
1353        // Position the lid endpoint based on angle
1354        let end_pos = self.hinge_pos
1355            + Vec3::new(
1356                self.angle.cos() * self.lid_length,
1357                self.angle.sin() * self.lid_length,
1358                0.0,
1359            );
1360
1361        // Push rope endpoint toward desired position
1362        if let Some(p) = self.rope.points.last_mut() {
1363            let diff_vec = end_pos - p.position;
1364            p.apply_force(diff_vec * 100.0);
1365        }
1366
1367        self.rope.step(dt);
1368    }
1369
1370    /// Get the lid tip position.
1371    pub fn tip_position(&self) -> Vec3 {
1372        self.rope
1373            .points
1374            .last()
1375            .map(|p| p.position)
1376            .unwrap_or(self.hinge_pos)
1377    }
1378
1379    pub fn get_points(&self) -> Vec<Vec3> {
1380        self.rope.get_points()
1381    }
1382}
1383
1384// ═════════════════════════════════════════════════════════════════════════════
1385// ClothRopeManager
1386// ═════════════════════════════════════════════════════════════════════════════
1387
1388/// Maximum number of active instances per type.
1389pub const MAX_CLOTH: usize = 20;
1390pub const MAX_ROPES: usize = 30;
1391pub const MAX_SOFTBODIES: usize = 15;
1392
1393/// Identifies an instance within the manager.
1394#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1395pub struct ClothId(pub u32);
1396
1397#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1398pub struct RopeId(pub u32);
1399
1400#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1401pub struct SoftBodyId(pub u32);
1402
1403/// Central manager owning all active cloth, rope, and soft body instances.
1404/// Steps all each frame, removes expired ones, and provides render data.
1405#[derive(Debug, Clone)]
1406pub struct ClothRopeManager {
1407    pub cloths: Vec<(ClothId, ClothStrip)>,
1408    pub ropes: Vec<(RopeId, RopeChain)>,
1409    pub soft_bodies: Vec<(SoftBodyId, SoftBodyBlob)>,
1410    next_cloth_id: u32,
1411    next_rope_id: u32,
1412    next_soft_body_id: u32,
1413}
1414
1415impl ClothRopeManager {
1416    pub fn new() -> Self {
1417        Self {
1418            cloths: Vec::new(),
1419            ropes: Vec::new(),
1420            soft_bodies: Vec::new(),
1421            next_cloth_id: 0,
1422            next_rope_id: 0,
1423            next_soft_body_id: 0,
1424        }
1425    }
1426
1427    // ── Add instances ──────────────────────────────────────────────────────
1428
1429    /// Add a cloth strip. Returns `None` if the limit is reached.
1430    pub fn add_cloth(&mut self, cloth: ClothStrip) -> Option<ClothId> {
1431        if self.cloths.len() >= MAX_CLOTH {
1432            return None;
1433        }
1434        let id = ClothId(self.next_cloth_id);
1435        self.next_cloth_id += 1;
1436        self.cloths.push((id, cloth));
1437        Some(id)
1438    }
1439
1440    /// Add a rope chain. Returns `None` if the limit is reached.
1441    pub fn add_rope(&mut self, rope: RopeChain) -> Option<RopeId> {
1442        if self.ropes.len() >= MAX_ROPES {
1443            return None;
1444        }
1445        let id = RopeId(self.next_rope_id);
1446        self.next_rope_id += 1;
1447        self.ropes.push((id, rope));
1448        Some(id)
1449    }
1450
1451    /// Add a soft body blob. Returns `None` if the limit is reached.
1452    pub fn add_soft_body(&mut self, blob: SoftBodyBlob) -> Option<SoftBodyId> {
1453        if self.soft_bodies.len() >= MAX_SOFTBODIES {
1454            return None;
1455        }
1456        let id = SoftBodyId(self.next_soft_body_id);
1457        self.next_soft_body_id += 1;
1458        self.soft_bodies.push((id, blob));
1459        Some(id)
1460    }
1461
1462    // ── Remove instances ───────────────────────────────────────────────────
1463
1464    pub fn remove_cloth(&mut self, id: ClothId) {
1465        self.cloths.retain(|(cid, _)| *cid != id);
1466    }
1467
1468    pub fn remove_rope(&mut self, id: RopeId) {
1469        self.ropes.retain(|(rid, _)| *rid != id);
1470    }
1471
1472    pub fn remove_soft_body(&mut self, id: SoftBodyId) {
1473        self.soft_bodies.retain(|(sid, _)| *sid != id);
1474    }
1475
1476    // ── Get mutable references ─────────────────────────────────────────────
1477
1478    pub fn get_cloth_mut(&mut self, id: ClothId) -> Option<&mut ClothStrip> {
1479        self.cloths
1480            .iter_mut()
1481            .find(|(cid, _)| *cid == id)
1482            .map(|(_, c)| c)
1483    }
1484
1485    pub fn get_rope_mut(&mut self, id: RopeId) -> Option<&mut RopeChain> {
1486        self.ropes
1487            .iter_mut()
1488            .find(|(rid, _)| *rid == id)
1489            .map(|(_, r)| r)
1490    }
1491
1492    pub fn get_soft_body_mut(&mut self, id: SoftBodyId) -> Option<&mut SoftBodyBlob> {
1493        self.soft_bodies
1494            .iter_mut()
1495            .find(|(sid, _)| *sid == id)
1496            .map(|(_, s)| s)
1497    }
1498
1499    // ── Step all ───────────────────────────────────────────────────────────
1500
1501    /// Step all active cloth, rope, and soft body instances, then remove any
1502    /// that have expired.
1503    pub fn step_all(&mut self, dt: f32) {
1504        for (_, cloth) in &mut self.cloths {
1505            cloth.apply_force(Vec3::new(0.0, -9.81, 0.0));
1506            cloth.step(dt, 4);
1507        }
1508        for (_, rope) in &mut self.ropes {
1509            rope.step(dt);
1510        }
1511        for (_, blob) in &mut self.soft_bodies {
1512            blob.step(dt);
1513        }
1514        self.remove_expired();
1515    }
1516
1517    /// Remove instances that have exceeded their lifetime.
1518    fn remove_expired(&mut self) {
1519        self.cloths.retain(|(_, c)| !c.is_expired());
1520        self.ropes.retain(|(_, r)| !r.is_expired());
1521        self.soft_bodies.retain(|(_, s)| !s.is_expired());
1522    }
1523
1524    // ── Render data ────────────────────────────────────────────────────────
1525
1526    /// Collect render data for all cloth strips.
1527    pub fn cloth_render_data(&self) -> Vec<(ClothId, Vec<[f32; 3]>)> {
1528        self.cloths
1529            .iter()
1530            .map(|(id, c)| (*id, c.get_render_data()))
1531            .collect()
1532    }
1533
1534    /// Collect render data for all ropes.
1535    pub fn rope_render_data(&self) -> Vec<(RopeId, Vec<Vec3>)> {
1536        self.ropes
1537            .iter()
1538            .map(|(id, r)| (*id, r.get_points()))
1539            .collect()
1540    }
1541
1542    /// Collect render data for all soft bodies.
1543    pub fn soft_body_render_data(&self) -> Vec<(SoftBodyId, Vec<Vec3>)> {
1544        self.soft_bodies
1545            .iter()
1546            .map(|(id, s)| (*id, s.get_hull()))
1547            .collect()
1548    }
1549
1550    // ── Stats ──────────────────────────────────────────────────────────────
1551
1552    pub fn cloth_count(&self) -> usize {
1553        self.cloths.len()
1554    }
1555
1556    pub fn rope_count(&self) -> usize {
1557        self.ropes.len()
1558    }
1559
1560    pub fn soft_body_count(&self) -> usize {
1561        self.soft_bodies.len()
1562    }
1563
1564    pub fn total_count(&self) -> usize {
1565        self.cloth_count() + self.rope_count() + self.soft_body_count()
1566    }
1567
1568    /// Total number of simulated points across all instances.
1569    pub fn total_point_count(&self) -> usize {
1570        let c: usize = self.cloths.iter().map(|(_, cl)| cl.points.len()).sum();
1571        let r: usize = self.ropes.iter().map(|(_, rp)| rp.points.len()).sum();
1572        let s: usize = self
1573            .soft_bodies
1574            .iter()
1575            .map(|(_, sb)| sb.points.len())
1576            .sum();
1577        c + r + s
1578    }
1579}
1580
1581impl Default for ClothRopeManager {
1582    fn default() -> Self {
1583        Self::new()
1584    }
1585}
1586
1587// ─────────────────────────────────────────────────────────────────────────────
1588// Unit tests
1589// ─────────────────────────────────────────────────────────────────────────────
1590
1591#[cfg(test)]
1592mod tests {
1593    use super::*;
1594
1595    #[test]
1596    fn test_verlet_point_integration() {
1597        let mut p = VerletPoint::new(Vec3::ZERO, 1.0);
1598        p.apply_force(Vec3::new(0.0, -9.81, 0.0));
1599        p.integrate(0.016);
1600        // Should have moved downward
1601        assert!(p.position.y < 0.0);
1602    }
1603
1604    #[test]
1605    fn test_verlet_pinned_no_move() {
1606        let mut p = VerletPoint::new(Vec3::new(1.0, 2.0, 3.0), 1.0);
1607        p.pinned = true;
1608        p.apply_force(Vec3::new(100.0, 0.0, 0.0));
1609        p.integrate(0.016);
1610        assert!((p.position.x - 1.0).abs() < 1e-6);
1611    }
1612
1613    #[test]
1614    fn test_distance_constraint() {
1615        let mut points = vec![
1616            VerletPoint::new(Vec3::ZERO, 1.0),
1617            VerletPoint::new(Vec3::new(3.0, 0.0, 0.0), 1.0),
1618        ];
1619        let c = DistanceConstraint::new(0, 1, 1.0);
1620        for _ in 0..50 {
1621            c.satisfy(&mut points);
1622        }
1623        let dist = points[0].position.distance(points[1].position);
1624        assert!((dist - 1.0).abs() < 0.01);
1625    }
1626
1627    #[test]
1628    fn test_cloth_creation() {
1629        let cloth = ClothStrip::new(4, 4, 0.5, Vec3::ZERO);
1630        assert_eq!(cloth.points.len(), 16);
1631        assert!(!cloth.structural_constraints.is_empty());
1632        assert!(!cloth.bend_constraints.is_empty());
1633    }
1634
1635    #[test]
1636    fn test_cloth_pin_unpin() {
1637        let mut cloth = ClothStrip::new(4, 4, 0.5, Vec3::ZERO);
1638        cloth.pin_point(0);
1639        assert!(cloth.points[0].pinned);
1640        cloth.unpin_point(0);
1641        assert!(!cloth.points[0].pinned);
1642    }
1643
1644    #[test]
1645    fn test_cloth_tear() {
1646        let mut cloth = ClothStrip::new(4, 4, 0.5, Vec3::ZERO);
1647        let before = cloth.active_constraint_count();
1648        cloth.tear_at(5); // tear at a center point
1649        let after = cloth.active_constraint_count();
1650        assert!(after < before);
1651    }
1652
1653    #[test]
1654    fn test_cloth_step() {
1655        // The strip already hangs at its rest length below the pinned top
1656        // row, so "the bottom row moves below its rest position" (the old
1657        // assertion) is not something an inextensible cloth does. Check the
1658        // integrator instead: a free strip falls g * dt^2 in its first step,
1659        // and a pinned one keeps its pinned row fixed.
1660        let dt = 0.016;
1661        let g = Vec3::new(0.0, -9.81, 0.0);
1662        let mut free = ClothStrip::new(4, 4, 0.5, Vec3::ZERO);
1663        let y0 = free.points[12].position.y;
1664        free.apply_force(g);
1665        free.step(dt, 4);
1666        let fall = y0 - free.points[12].position.y;
1667        assert!((fall - 9.81 * dt * dt).abs() < 1e-4, "fell {fall}");
1668
1669        let mut cloth = ClothStrip::new(4, 4, 0.5, Vec3::ZERO);
1670        for i in 0..4 {
1671            cloth.pin_point(i);
1672        }
1673        for _ in 0..60 {
1674            cloth.apply_force(g);
1675            cloth.step(dt, 4);
1676        }
1677        assert_eq!(cloth.points[0].position, Vec3::ZERO);
1678        // The bottom row stays close to its rest depth (-1.5), not above it.
1679        assert!(cloth.points[12].position.y <= -1.45, "y = {}", cloth.points[12].position.y);
1680    }
1681
1682    #[test]
1683    fn test_cloth_render_data() {
1684        let cloth = ClothStrip::new(3, 3, 1.0, Vec3::ZERO);
1685        let data = cloth.get_render_data();
1686        assert_eq!(data.len(), 9);
1687    }
1688
1689    #[test]
1690    fn test_cloth_wind() {
1691        let mut cloth = ClothStrip::new(3, 3, 1.0, Vec3::ZERO);
1692        cloth.pin_point(0);
1693        cloth.pin_point(1);
1694        cloth.pin_point(2);
1695        cloth.apply_wind(Vec3::X, 10.0, 2.0);
1696        cloth.step(0.016, 4);
1697        // Bottom row should have shifted in X
1698        assert!(cloth.points[6].position.x > 0.0);
1699    }
1700
1701    #[test]
1702    fn test_cloth_lifetime() {
1703        let mut cloth = ClothStrip::new(2, 2, 1.0, Vec3::ZERO);
1704        cloth.lifetime = 1.0;
1705        assert!(!cloth.is_expired());
1706        cloth.step(0.5, 1);
1707        assert!(!cloth.is_expired());
1708        cloth.step(0.6, 1);
1709        assert!(cloth.is_expired());
1710    }
1711
1712    #[test]
1713    fn test_rope_creation() {
1714        let rope = RopeChain::new(Vec3::ZERO, Vec3::new(5.0, 0.0, 0.0), 10);
1715        assert_eq!(rope.points.len(), 11);
1716        assert_eq!(rope.constraints.len(), 10);
1717    }
1718
1719    #[test]
1720    fn test_rope_step_gravity() {
1721        let mut rope = RopeChain::new(Vec3::ZERO, Vec3::new(5.0, 0.0, 0.0), 5);
1722        rope.attach_start(Vec3::ZERO);
1723        let y0 = rope.points[3].position.y;
1724        for _ in 0..20 {
1725            rope.step(0.016);
1726        }
1727        assert!(rope.points[3].position.y < y0);
1728    }
1729
1730    #[test]
1731    fn test_rope_sever() {
1732        let mut rope = RopeChain::new(Vec3::ZERO, Vec3::new(10.0, 0.0, 0.0), 8);
1733        let result = rope.sever_at(4);
1734        assert!(result.is_some());
1735        let new_rope = result.unwrap();
1736        assert!(!new_rope.points.is_empty());
1737        assert!(rope.points.len() <= 6);
1738    }
1739
1740    #[test]
1741    fn test_rope_attach() {
1742        let mut rope = RopeChain::new(Vec3::ZERO, Vec3::new(5.0, 0.0, 0.0), 3);
1743        rope.attach_start(Vec3::new(1.0, 1.0, 0.0));
1744        assert!(rope.points[0].pinned);
1745        assert!((rope.points[0].position.x - 1.0).abs() < 1e-6);
1746    }
1747
1748    #[test]
1749    fn test_rope_length() {
1750        let rope = RopeChain::new(Vec3::ZERO, Vec3::new(5.0, 0.0, 0.0), 5);
1751        let len = rope.current_length();
1752        assert!((len - 5.0).abs() < 0.01);
1753    }
1754
1755    #[test]
1756    fn test_rope_lifetime() {
1757        let mut rope = RopeChain::new(Vec3::ZERO, Vec3::X, 3);
1758        rope.lifetime = 0.5;
1759        rope.step(0.3);
1760        assert!(!rope.is_expired());
1761        rope.step(0.3);
1762        assert!(rope.is_expired());
1763    }
1764
1765    #[test]
1766    fn test_soft_body_blob_creation() {
1767        let blob = SoftBodyBlob::new(Vec3::ZERO, 1.0, 8);
1768        assert_eq!(blob.points.len(), 9); // 8 perimeter + 1 center
1769        assert_eq!(blob.perimeter_count, 8);
1770        assert_eq!(blob.center_index, 8);
1771    }
1772
1773    #[test]
1774    fn test_blob_hit_deformation() {
1775        let mut blob = SoftBodyBlob::new(Vec3::ZERO, 1.0, 8);
1776        let positions_before: Vec<Vec3> = blob.points.iter().map(|p| p.position).collect();
1777        blob.apply_hit(Vec3::X, 50.0);
1778        blob.step(0.016);
1779        // At least some points should have moved
1780        let moved = blob
1781            .points
1782            .iter()
1783            .zip(positions_before.iter())
1784            .any(|(p, b)| p.position.distance(*b) > 0.001);
1785        assert!(moved);
1786    }
1787
1788    #[test]
1789    fn test_blob_hull() {
1790        let blob = SoftBodyBlob::new(Vec3::ZERO, 1.0, 6);
1791        let hull = blob.get_hull();
1792        assert_eq!(hull.len(), 6);
1793    }
1794
1795    #[test]
1796    fn test_blob_oscillation() {
1797        let mut blob = SoftBodyBlob::new(Vec3::ZERO, 1.0, 6);
1798        let shape_a = vec![Vec3::X; 7];
1799        let shape_b = vec![Vec3::Y; 7];
1800        blob.oscillate_between(shape_a, shape_b, 2.0);
1801        assert!(blob.morphing);
1802        assert!((blob.morph_frequency - 2.0).abs() < 1e-6);
1803    }
1804
1805    #[test]
1806    fn test_blob_stiffness() {
1807        let mut blob = SoftBodyBlob::new(Vec3::ZERO, 1.0, 6);
1808        blob.set_stiffness(0.5);
1809        assert!((blob.spring_stiffness - 0.5).abs() < 1e-6);
1810        blob.set_stiffness(-1.0);
1811        assert!(blob.spring_stiffness >= 0.01);
1812    }
1813
1814    #[test]
1815    fn test_boss_cape_creation() {
1816        let cape = BossCape::new(Vec3::ZERO, 5, 8, 0.3);
1817        assert_eq!(cape.anchor_count, 5);
1818        assert_eq!(cape.cloth.width, 5);
1819        assert_eq!(cape.cloth.height, 8);
1820        // Top row should be pinned
1821        for i in 0..5 {
1822            assert!(cape.cloth.points[i].pinned);
1823        }
1824    }
1825
1826    #[test]
1827    fn test_boss_cape_update() {
1828        let mut cape = BossCape::new(Vec3::ZERO, 4, 6, 0.5);
1829        cape.update(Vec3::new(1.0, 0.0, 0.0), 0.016);
1830        // Anchors should have moved with boss
1831        assert!((cape.cloth.points[0].position.x - 1.0).abs() < 1e-4);
1832    }
1833
1834    #[test]
1835    fn test_boss_cape_vortex() {
1836        let mut cape = BossCape::new(Vec3::ZERO, 4, 6, 0.5);
1837        cape.apply_vortex(Vec3::ZERO, 100.0, 10.0);
1838        cape.update(Vec3::ZERO, 0.016);
1839        // Non-pinned points should have moved
1840        let moved = cape.cloth.points[8..].iter().any(|p| p.position.length() > 0.01);
1841        assert!(moved);
1842    }
1843
1844    #[test]
1845    fn test_hydra_tendril_creation() {
1846        let tendril = HydraTendril::new(Vec3::ZERO, Vec3::new(5.0, 0.0, 0.0), 6, 100.0);
1847        assert!(!tendril.severed);
1848        assert!((tendril.health - 100.0).abs() < 1e-6);
1849    }
1850
1851    #[test]
1852    fn test_hydra_tendril_damage_and_sever() {
1853        let mut tendril =
1854            HydraTendril::new(Vec3::ZERO, Vec3::new(5.0, 0.0, 0.0), 6, 50.0);
1855        assert!(!tendril.damage(30.0));
1856        assert!(tendril.is_alive());
1857        assert!(tendril.damage(25.0));
1858        assert!(!tendril.is_alive());
1859    }
1860
1861    #[test]
1862    fn test_hydra_tendril_update() {
1863        let mut tendril =
1864            HydraTendril::new(Vec3::ZERO, Vec3::new(5.0, 0.0, 0.0), 4, 100.0);
1865        tendril.update(Vec3::new(0.0, 1.0, 0.0), Vec3::new(5.0, 1.0, 0.0), 0.016);
1866        // Start point should follow hydra A
1867        assert!((tendril.rope.points[0].position.y - 1.0).abs() < 1e-4);
1868    }
1869
1870    #[test]
1871    fn test_player_robe_creation() {
1872        let robe = PlayerRobe::new(Vec3::ZERO, 3, 3, 5, 0.2);
1873        assert_eq!(robe.strips.len(), 3);
1874        // Top rows should be pinned
1875        for strip in &robe.strips {
1876            for c in 0..3 {
1877                assert!(strip.points[c].pinned);
1878            }
1879        }
1880    }
1881
1882    #[test]
1883    fn test_player_robe_update() {
1884        let mut robe = PlayerRobe::new(Vec3::ZERO, 2, 2, 4, 0.3);
1885        robe.update(Vec3::new(2.0, 0.0, 0.0), 0.016);
1886        // Anchors should follow player
1887        let data = robe.get_render_data();
1888        assert_eq!(data.len(), 2);
1889    }
1890
1891    #[test]
1892    fn test_necro_soul_chain() {
1893        let mut chain = NecroSoulChain::new(
1894            Vec3::ZERO,
1895            Vec3::new(5.0, 0.0, 0.0),
1896            6,
1897            20.0,
1898            0.5,
1899        );
1900        assert!(chain.is_active());
1901        chain.update(Vec3::ZERO, Vec3::new(5.0, 0.0, 0.0), 0.016);
1902        assert!(chain.is_active());
1903    }
1904
1905    #[test]
1906    fn test_necro_soul_chain_break_distance() {
1907        let mut chain = NecroSoulChain::new(
1908            Vec3::ZERO,
1909            Vec3::new(5.0, 0.0, 0.0),
1910            4,
1911            10.0,
1912            0.1,
1913        );
1914        // Move too far apart
1915        chain.update(Vec3::ZERO, Vec3::new(100.0, 0.0, 0.0), 0.016);
1916        assert!(!chain.is_active());
1917    }
1918
1919    #[test]
1920    fn test_necro_soul_chain_drain() {
1921        let mut chain = NecroSoulChain::new(
1922            Vec3::ZERO,
1923            Vec3::new(3.0, 0.0, 0.0),
1924            4,
1925            100.0,
1926            10.0, // very fast drain
1927        );
1928        for _ in 0..10 {
1929            chain.update(Vec3::ZERO, Vec3::new(3.0, 0.0, 0.0), 0.016);
1930        }
1931        // After enough updates, drain should complete
1932        // 10 * 0.016 * 10.0 = 1.6, which exceeds 1.0
1933        assert!(!chain.is_active());
1934    }
1935
1936    #[test]
1937    fn test_necro_particles() {
1938        let chain = NecroSoulChain::new(
1939            Vec3::ZERO,
1940            Vec3::new(5.0, 0.0, 0.0),
1941            4,
1942            20.0,
1943            0.5,
1944        );
1945        let particles = chain.get_particle_world_positions();
1946        assert_eq!(particles.len(), 5);
1947    }
1948
1949    #[test]
1950    fn test_slime_enemy_creation() {
1951        let slime = SlimeEnemy::new(Vec3::ZERO, 1.0, 8, 100.0);
1952        assert!((slime.hp - 100.0).abs() < 1e-6);
1953        assert!(!slime.is_dead());
1954    }
1955
1956    #[test]
1957    fn test_slime_take_hit() {
1958        let mut slime = SlimeEnemy::new(Vec3::ZERO, 1.0, 8, 100.0);
1959        slime.take_hit(Vec3::X, 20.0, 60.0);
1960        assert!((slime.hp - 40.0).abs() < 1e-6);
1961        slime.take_hit(Vec3::X, 20.0, 50.0);
1962        assert!(slime.is_dead());
1963    }
1964
1965    #[test]
1966    fn test_slime_stiffness_scales() {
1967        let mut slime = SlimeEnemy::new(Vec3::ZERO, 1.0, 8, 100.0);
1968        slime.update(0.016);
1969        let stiff_full = slime.blob.spring_stiffness;
1970        slime.hp = 10.0;
1971        slime.update(0.016);
1972        let stiff_low = slime.blob.spring_stiffness;
1973        assert!(stiff_low < stiff_full);
1974    }
1975
1976    #[test]
1977    fn test_quantum_blob_default() {
1978        let qb = QuantumBlob::new_default(Vec3::ZERO, 1.0, 8, 2.0);
1979        assert!(qb.superposed);
1980        assert!(qb.blob.morphing);
1981    }
1982
1983    #[test]
1984    fn test_quantum_blob_collapse() {
1985        let mut qb = QuantumBlob::new_default(Vec3::ZERO, 1.0, 8, 2.0);
1986        qb.collapse(0);
1987        assert!(!qb.superposed);
1988        assert!(!qb.blob.morphing);
1989        assert_eq!(qb.eigenstate, 0);
1990    }
1991
1992    #[test]
1993    fn test_quantum_blob_superposition() {
1994        let mut qb = QuantumBlob::new_default(Vec3::ZERO, 1.0, 8, 2.0);
1995        qb.collapse(1);
1996        qb.enter_superposition(3.0);
1997        assert!(qb.superposed);
1998        assert!(qb.blob.morphing);
1999        assert!((qb.blob.morph_frequency - 3.0).abs() < 1e-6);
2000    }
2001
2002    #[test]
2003    fn test_weapon_trail_creation() {
2004        let trail = WeaponTrail::new(Vec3::ZERO, 8, 0.1);
2005        assert_eq!(trail.rope.points.len(), 9);
2006        assert!(!trail.compressed);
2007    }
2008
2009    #[test]
2010    fn test_weapon_trail_impact() {
2011        let mut trail = WeaponTrail::new(Vec3::ZERO, 5, 0.2);
2012        trail.on_impact();
2013        assert!(trail.compressed);
2014        // After enough time, should decompress
2015        for _ in 0..20 {
2016            trail.update(Vec3::new(1.0, 0.0, 0.0), 0.016);
2017        }
2018        assert!(!trail.compressed);
2019    }
2020
2021    #[test]
2022    fn test_pendulum_trap_creation() {
2023        let trap = PendulumTrap::new(Vec3::new(0.0, 10.0, 0.0), 5.0, 6, 25.0, 0.5);
2024        assert!((trap.damage - 25.0).abs() < 1e-6);
2025        assert!(trap.rope.points[0].pinned);
2026    }
2027
2028    #[test]
2029    fn test_pendulum_swings() {
2030        let mut trap = PendulumTrap::new(Vec3::new(0.0, 10.0, 0.0), 5.0, 4, 10.0, 0.3);
2031        trap.push(Vec3::new(20.0, 0.0, 0.0));
2032        for _ in 0..50 {
2033            trap.update(0.016);
2034        }
2035        // The weight should have moved horizontally
2036        assert!(trap.weight_position().x.abs() > 0.01);
2037    }
2038
2039    #[test]
2040    fn test_pendulum_collision() {
2041        let trap = PendulumTrap::new(Vec3::new(0.0, 10.0, 0.0), 5.0, 4, 10.0, 1.0);
2042        let weight = trap.weight_position();
2043        assert!(trap.check_collision(weight));
2044        assert!(!trap.check_collision(Vec3::new(100.0, 100.0, 0.0)));
2045    }
2046
2047    #[test]
2048    fn test_treasure_chest_lid() {
2049        let mut lid = TreasureChestLid::new(Vec3::ZERO, 1.0);
2050        assert!(!lid.is_open);
2051        lid.open();
2052        assert!(lid.is_open);
2053        lid.close();
2054        assert!(!lid.is_open);
2055    }
2056
2057    #[test]
2058    fn test_treasure_chest_toggle() {
2059        let mut lid = TreasureChestLid::new(Vec3::ZERO, 1.0);
2060        lid.toggle();
2061        assert!(lid.is_open);
2062        lid.toggle();
2063        assert!(!lid.is_open);
2064    }
2065
2066    #[test]
2067    fn test_treasure_chest_update() {
2068        let mut lid = TreasureChestLid::new(Vec3::ZERO, 1.0);
2069        lid.open();
2070        for _ in 0..60 {
2071            lid.update(0.016);
2072        }
2073        // Angle should be close to PI/2
2074        assert!(lid.angle > 0.5);
2075    }
2076
2077    #[test]
2078    fn test_manager_creation() {
2079        let mgr = ClothRopeManager::new();
2080        assert_eq!(mgr.total_count(), 0);
2081    }
2082
2083    #[test]
2084    fn test_manager_add_cloth() {
2085        let mut mgr = ClothRopeManager::new();
2086        let cloth = ClothStrip::new(3, 3, 0.5, Vec3::ZERO);
2087        let id = mgr.add_cloth(cloth);
2088        assert!(id.is_some());
2089        assert_eq!(mgr.cloth_count(), 1);
2090    }
2091
2092    #[test]
2093    fn test_manager_add_rope() {
2094        let mut mgr = ClothRopeManager::new();
2095        let rope = RopeChain::new(Vec3::ZERO, Vec3::X * 5.0, 4);
2096        let id = mgr.add_rope(rope);
2097        assert!(id.is_some());
2098        assert_eq!(mgr.rope_count(), 1);
2099    }
2100
2101    #[test]
2102    fn test_manager_add_soft_body() {
2103        let mut mgr = ClothRopeManager::new();
2104        let blob = SoftBodyBlob::new(Vec3::ZERO, 1.0, 6);
2105        let id = mgr.add_soft_body(blob);
2106        assert!(id.is_some());
2107        assert_eq!(mgr.soft_body_count(), 1);
2108    }
2109
2110    #[test]
2111    fn test_manager_limits() {
2112        let mut mgr = ClothRopeManager::new();
2113        for _ in 0..MAX_CLOTH {
2114            let cloth = ClothStrip::new(2, 2, 0.5, Vec3::ZERO);
2115            assert!(mgr.add_cloth(cloth).is_some());
2116        }
2117        let cloth = ClothStrip::new(2, 2, 0.5, Vec3::ZERO);
2118        assert!(mgr.add_cloth(cloth).is_none());
2119    }
2120
2121    #[test]
2122    fn test_manager_remove() {
2123        let mut mgr = ClothRopeManager::new();
2124        let cloth = ClothStrip::new(2, 2, 0.5, Vec3::ZERO);
2125        let id = mgr.add_cloth(cloth).unwrap();
2126        assert_eq!(mgr.cloth_count(), 1);
2127        mgr.remove_cloth(id);
2128        assert_eq!(mgr.cloth_count(), 0);
2129    }
2130
2131    #[test]
2132    fn test_manager_step_all() {
2133        let mut mgr = ClothRopeManager::new();
2134        let mut cloth = ClothStrip::new(3, 3, 0.5, Vec3::ZERO);
2135        cloth.pin_point(0);
2136        cloth.pin_point(1);
2137        cloth.pin_point(2);
2138        mgr.add_cloth(cloth);
2139
2140        let rope = RopeChain::new(Vec3::ZERO, Vec3::X * 3.0, 3);
2141        mgr.add_rope(rope);
2142
2143        let blob = SoftBodyBlob::new(Vec3::ZERO, 1.0, 6);
2144        mgr.add_soft_body(blob);
2145
2146        mgr.step_all(0.016);
2147        assert_eq!(mgr.total_count(), 3);
2148    }
2149
2150    #[test]
2151    fn test_manager_expiry() {
2152        let mut mgr = ClothRopeManager::new();
2153        let mut cloth = ClothStrip::new(2, 2, 0.5, Vec3::ZERO);
2154        cloth.lifetime = 0.01;
2155        mgr.add_cloth(cloth);
2156        assert_eq!(mgr.cloth_count(), 1);
2157        mgr.step_all(0.02);
2158        assert_eq!(mgr.cloth_count(), 0);
2159    }
2160
2161    #[test]
2162    fn test_manager_render_data() {
2163        let mut mgr = ClothRopeManager::new();
2164        let cloth = ClothStrip::new(2, 2, 0.5, Vec3::ZERO);
2165        mgr.add_cloth(cloth);
2166        let data = mgr.cloth_render_data();
2167        assert_eq!(data.len(), 1);
2168        assert_eq!(data[0].1.len(), 4);
2169    }
2170
2171    #[test]
2172    fn test_manager_point_count() {
2173        let mut mgr = ClothRopeManager::new();
2174        let cloth = ClothStrip::new(3, 3, 0.5, Vec3::ZERO);
2175        mgr.add_cloth(cloth);
2176        let rope = RopeChain::new(Vec3::ZERO, Vec3::X, 4);
2177        mgr.add_rope(rope);
2178        assert_eq!(mgr.total_point_count(), 9 + 5);
2179    }
2180
2181    #[test]
2182    fn test_manager_get_mut() {
2183        let mut mgr = ClothRopeManager::new();
2184        let cloth = ClothStrip::new(2, 2, 0.5, Vec3::ZERO);
2185        let id = mgr.add_cloth(cloth).unwrap();
2186        let c = mgr.get_cloth_mut(id);
2187        assert!(c.is_some());
2188        let c = c.unwrap();
2189        c.pin_point(0);
2190        assert!(mgr.get_cloth_mut(id).unwrap().points[0].pinned);
2191    }
2192}