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