1use std::collections::HashMap;
9use std::f32::consts::{PI, TAU};
10
11use glam::Vec3;
12
13use crate::entity::EntityId;
14
15#[derive(Debug, Clone, Copy)]
21pub struct GlyphTransform {
22 pub position_offset: Vec3,
24 pub scale: f32,
26 pub rotation_z: f32,
28 pub emission: f32,
30}
31
32impl Default for GlyphTransform {
33 fn default() -> Self {
34 Self {
35 position_offset: Vec3::ZERO,
36 scale: 1.0,
37 rotation_z: 0.0,
38 emission: 0.0,
39 }
40 }
41}
42
43impl GlyphTransform {
44 pub fn lerp(a: &GlyphTransform, b: &GlyphTransform, t: f32) -> Self {
46 Self {
47 position_offset: a.position_offset + (b.position_offset - a.position_offset) * t,
48 scale: a.scale + (b.scale - a.scale) * t,
49 rotation_z: a.rotation_z + (b.rotation_z - a.rotation_z) * t,
50 emission: a.emission + (b.emission - a.emission) * t,
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
61pub struct AnimPose {
62 pub transforms: Vec<GlyphTransform>,
63}
64
65impl AnimPose {
66 pub fn identity(count: usize) -> Self {
67 Self {
68 transforms: vec![GlyphTransform::default(); count],
69 }
70 }
71
72 pub fn blend(&self, other: &AnimPose, t: f32) -> Self {
74 let len = self.transforms.len().min(other.transforms.len());
75 let mut transforms = Vec::with_capacity(len);
76 for i in 0..len {
77 transforms.push(GlyphTransform::lerp(&self.transforms[i], &other.transforms[i], t));
78 }
79 Self { transforms }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq)]
89pub enum BlendCurve {
90 Linear,
91 EaseIn,
92 EaseOut,
93 EaseInOut,
94}
95
96impl BlendCurve {
97 pub fn evaluate(&self, t: f32) -> f32 {
99 let t = t.clamp(0.0, 1.0);
100 match self {
101 BlendCurve::Linear => t,
102 BlendCurve::EaseIn => t * t,
103 BlendCurve::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
104 BlendCurve::EaseInOut => {
105 if t < 0.5 {
106 2.0 * t * t
107 } else {
108 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
109 }
110 }
111 }
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121pub enum PlayerAnimState {
122 Idle,
123 Walk,
124 Attack,
125 HeavyAttack,
126 Cast,
127 Defend,
128 Hurt,
129 Flee,
130 Channel,
131 Dodge,
132 Interact,
133}
134
135impl PlayerAnimState {
136 pub fn fixed_duration(&self) -> Option<f32> {
138 match self {
139 PlayerAnimState::Attack => Some(0.3),
140 PlayerAnimState::HeavyAttack => Some(1.0), PlayerAnimState::Cast => Some(0.75), PlayerAnimState::Hurt => Some(0.2),
143 PlayerAnimState::Dodge => Some(0.25),
144 PlayerAnimState::Interact => Some(0.5),
145 _ => None, }
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq)]
152enum HeavyAttackPhase {
153 Windup, Swing, FollowThru, }
157
158#[derive(Debug, Clone, Copy, PartialEq)]
160enum CastPhase {
161 Raise, Hold, Release, }
165
166pub struct PlayerAnimController {
172 pub current_state: PlayerAnimState,
173 pub prev_state: PlayerAnimState,
174 pub blend_factor: f32,
176 pub transition_time: f32,
178 pub state_timer: f32,
180 transition_elapsed: f32,
182 in_transition: bool,
184 transition_curve: BlendCurve,
186 glyph_count: usize,
188 pub damage_direction: Vec3,
190 pub move_direction: Vec3,
192 pub movement_speed: f32,
194 pub ik_targets: HashMap<String, IKTarget>,
196}
197
198impl PlayerAnimController {
199 pub fn new(glyph_count: usize) -> Self {
200 Self {
201 current_state: PlayerAnimState::Idle,
202 prev_state: PlayerAnimState::Idle,
203 blend_factor: 1.0,
204 transition_time: 0.0,
205 state_timer: 0.0,
206 transition_elapsed: 0.0,
207 in_transition: false,
208 transition_curve: BlendCurve::Linear,
209 glyph_count,
210 damage_direction: Vec3::NEG_X,
211 move_direction: Vec3::X,
212 movement_speed: 0.0,
213 ik_targets: HashMap::new(),
214 }
215 }
216
217 pub fn transition_to(&mut self, new_state: PlayerAnimState) -> bool {
219 if new_state == self.current_state && !self.in_transition {
220 return false;
221 }
222 let (duration, curve) = transition_params(self.current_state, new_state);
223 self.prev_state = self.current_state;
224 self.current_state = new_state;
225 self.transition_time = duration;
226 self.transition_elapsed = 0.0;
227 self.blend_factor = 0.0;
228 self.in_transition = true;
229 self.transition_curve = curve;
230 self.state_timer = 0.0;
231 true
232 }
233
234 pub fn update(&mut self, dt: f32) -> AnimPose {
236 self.state_timer += dt;
237
238 if self.in_transition {
240 self.transition_elapsed += dt;
241 if self.transition_elapsed >= self.transition_time {
242 self.blend_factor = 1.0;
243 self.in_transition = false;
244 } else {
245 let raw = self.transition_elapsed / self.transition_time.max(0.001);
246 self.blend_factor = self.transition_curve.evaluate(raw);
247 }
248 }
249
250 if !self.in_transition {
252 if let Some(dur) = self.current_state.fixed_duration() {
253 if self.state_timer >= dur {
254 self.transition_to(PlayerAnimState::Idle);
255 }
256 }
257 }
258
259 if self.in_transition {
261 let prev_pose = self.evaluate_state(self.prev_state);
262 let curr_pose = self.evaluate_state(self.current_state);
263 prev_pose.blend(&curr_pose, self.blend_factor)
264 } else {
265 self.evaluate_state(self.current_state)
266 }
267 }
268
269 fn evaluate_state(&self, state: PlayerAnimState) -> AnimPose {
271 let t = self.state_timer;
272 let n = self.glyph_count;
273 match state {
274 PlayerAnimState::Idle => self.pose_idle(t, n),
275 PlayerAnimState::Walk => self.pose_walk(t, n),
276 PlayerAnimState::Attack => self.pose_attack(t, n),
277 PlayerAnimState::HeavyAttack => self.pose_heavy_attack(t, n),
278 PlayerAnimState::Cast => self.pose_cast(t, n),
279 PlayerAnimState::Defend => self.pose_defend(t, n),
280 PlayerAnimState::Hurt => self.pose_hurt(t, n),
281 PlayerAnimState::Flee => self.pose_flee(t, n),
282 PlayerAnimState::Channel => self.pose_channel(t, n),
283 PlayerAnimState::Dodge => self.pose_dodge(t, n),
284 PlayerAnimState::Interact => self.pose_interact(t, n),
285 }
286 }
287
288 fn pose_idle(&self, t: f32, n: usize) -> AnimPose {
292 let mut transforms = Vec::with_capacity(n);
293 let breath_freq = 0.3;
294 let breath_scale_amp = 0.02; let bob_amp = 2.0; for i in 0..n {
298 let phase = i as f32 * 0.1; let breath = (TAU * breath_freq * t + phase).sin();
300 let scale = 1.0 + breath * breath_scale_amp;
301 let y_bob = breath * bob_amp;
302 transforms.push(GlyphTransform {
303 position_offset: Vec3::new(0.0, y_bob, 0.0),
304 scale,
305 rotation_z: 0.0,
306 emission: 0.0,
307 });
308 }
309 AnimPose { transforms }
310 }
311
312 fn pose_walk(&self, t: f32, n: usize) -> AnimPose {
314 let mut transforms = Vec::with_capacity(n);
315 let bob_freq = 1.5;
316 let bob_amp = 4.0;
317 let arm_swing_amp = 3.0; for i in 0..n {
320 let phase = i as f32 * 0.15;
321 let bob = (TAU * bob_freq * t + phase).sin();
322 let y_bob = bob * bob_amp;
323
324 let arm_phase = if i % 2 == 0 { 0.0 } else { PI };
326 let x_swing = (TAU * bob_freq * t + arm_phase).sin() * arm_swing_amp;
327
328 let lean = 0.02; transforms.push(GlyphTransform {
332 position_offset: Vec3::new(x_swing, y_bob, 0.0),
333 scale: 1.0,
334 rotation_z: lean,
335 emission: 0.0,
336 });
337 }
338 AnimPose { transforms }
339 }
340
341 fn pose_attack(&self, t: f32, n: usize) -> AnimPose {
343 let mut transforms = Vec::with_capacity(n);
344 let duration = 0.3;
345 let progress = (t / duration).clamp(0.0, 1.0);
346
347 let swing_curve = if progress < 0.6 {
349 (progress / 0.6 * PI * 0.5).sin()
350 } else {
351 ((1.0 - progress) / 0.4 * PI * 0.5).sin()
352 };
353
354 let lean_angle = swing_curve * 0.17; let forward_push = swing_curve * 3.0; for i in 0..n {
358 let arm_factor = if i >= n / 2 { 1.5 } else { 0.5 };
360 let swing_offset = swing_curve * 6.0 * arm_factor;
361
362 transforms.push(GlyphTransform {
363 position_offset: Vec3::new(swing_offset, -forward_push * 0.3, 0.0),
364 scale: 1.0 + swing_curve * 0.03,
365 rotation_z: lean_angle * arm_factor,
366 emission: swing_curve * 0.3,
367 });
368 }
369 AnimPose { transforms }
370 }
371
372 fn pose_heavy_attack(&self, t: f32, n: usize) -> AnimPose {
374 let mut transforms = Vec::with_capacity(n);
375
376 let (phase, phase_progress) = if t < 0.5 {
377 (HeavyAttackPhase::Windup, t / 0.5)
378 } else if t < 0.8 {
379 (HeavyAttackPhase::Swing, (t - 0.5) / 0.3)
380 } else {
381 (HeavyAttackPhase::FollowThru, ((t - 0.8) / 0.2).clamp(0.0, 1.0))
382 };
383
384 for i in 0..n {
385 let arm_factor = if i >= n / 2 { 1.8 } else { 0.6 };
386 let (offset, scale, rot, emit) = match phase {
387 HeavyAttackPhase::Windup => {
388 let pull = (phase_progress * PI * 0.5).sin();
390 let x = -pull * 5.0 * arm_factor;
391 let lean = -pull * 0.05;
392 (Vec3::new(x, pull * 1.5, 0.0), 1.0 + pull * 0.02, lean, 0.0)
393 }
394 HeavyAttackPhase::Swing => {
395 let swing = (phase_progress * PI * 0.5).sin();
397 let x = swing * 10.0 * arm_factor;
398 let lean = swing * 0.22; (Vec3::new(x, -swing * 3.0, 0.0), 1.0 + swing * 0.05, lean, swing * 0.6)
400 }
401 HeavyAttackPhase::FollowThru => {
402 let recover = 1.0 - phase_progress;
404 let x = recover * 4.0 * arm_factor;
405 let lean = recover * 0.1;
406 (Vec3::new(x, -recover * 1.0, 0.0), 1.0, lean, recover * 0.2)
407 }
408 };
409 transforms.push(GlyphTransform {
410 position_offset: offset,
411 scale,
412 rotation_z: rot,
413 emission: emit,
414 });
415 }
416 AnimPose { transforms }
417 }
418
419 fn pose_cast(&self, t: f32, n: usize) -> AnimPose {
421 let mut transforms = Vec::with_capacity(n);
422 let duration = 0.75;
423 let progress = (t / duration).clamp(0.0, 1.0);
424
425 let (cast_phase, phase_t) = if progress < 0.4 {
426 (CastPhase::Raise, progress / 0.4)
427 } else if progress < 0.8 {
428 (CastPhase::Hold, (progress - 0.4) / 0.4)
429 } else {
430 (CastPhase::Release, (progress - 0.8) / 0.2)
431 };
432
433 for i in 0..n {
434 let is_hand = i >= n * 2 / 3; let (offset, scale, rot, emit) = match cast_phase {
436 CastPhase::Raise => {
437 let rise = (phase_t * PI * 0.5).sin();
438 let y_up = if is_hand { rise * 8.0 } else { rise * 1.0 };
439 let glow = if is_hand { rise * 0.8 } else { 0.0 };
440 (Vec3::new(0.0, y_up, 0.0), 1.0 + rise * 0.01, -rise * 0.03, glow)
441 }
442 CastPhase::Hold => {
443 let pulse = (TAU * 3.0 * phase_t).sin() * 0.3 + 0.7;
444 let y_up = if is_hand { 8.0 } else { 1.0 };
445 let glow = if is_hand { pulse } else { pulse * 0.1 };
446 (Vec3::new(0.0, y_up, 0.0), 1.0 + 0.01, -0.03, glow)
447 }
448 CastPhase::Release => {
449 let drop = 1.0 - phase_t;
450 let y_up = if is_hand { drop * 8.0 } else { drop * 1.0 };
451 let glow = if is_hand { drop * 0.5 } else { 0.0 };
452 (Vec3::new(0.0, y_up, 0.0), 1.0, -drop * 0.03, glow)
453 }
454 };
455 transforms.push(GlyphTransform {
456 position_offset: offset,
457 scale,
458 rotation_z: rot,
459 emission: emit,
460 });
461 }
462 AnimPose { transforms }
463 }
464
465 fn pose_defend(&self, t: f32, n: usize) -> AnimPose {
467 let mut transforms = Vec::with_capacity(n);
468 let snap = (t / 0.15).clamp(0.0, 1.0);
470 let defend = snap;
471
472 for i in 0..n {
473 let is_arm = i >= n / 2;
474 let center_pull = if is_arm {
476 let base_x = if i % 2 == 0 { 3.0 } else { -3.0 };
477 Vec3::new(-base_x * defend, -defend * 2.0, 0.0)
478 } else {
479 Vec3::new(0.0, -defend * 1.0, 0.0)
480 };
481 let crouch_scale = 1.0 - defend * 0.05;
483 let hunch = defend * 0.08;
485
486 transforms.push(GlyphTransform {
487 position_offset: center_pull,
488 scale: crouch_scale,
489 rotation_z: hunch,
490 emission: 0.0,
491 });
492 }
493 AnimPose { transforms }
494 }
495
496 fn pose_hurt(&self, t: f32, n: usize) -> AnimPose {
498 let mut transforms = Vec::with_capacity(n);
499 let duration = 0.2;
500 let progress = (t / duration).clamp(0.0, 1.0);
501 let recoil = if progress < 0.3 {
503 progress / 0.3
504 } else {
505 1.0 - (progress - 0.3) / 0.7
506 };
507
508 let recoil_dir = self.damage_direction.normalize_or_zero();
509
510 for i in 0..n {
511 let phase = i as f32 * 2.7;
512 let shake_x = (t * 40.0 + phase).sin() * recoil * 2.0;
514 let shake_y = (t * 37.0 + phase * 1.3).sin() * recoil * 2.0;
515
516 let recoil_offset = recoil_dir * recoil * -5.0;
518
519 let spread = if i >= n / 2 {
521 let side = if i % 2 == 0 { 1.0 } else { -1.0 };
522 Vec3::new(side * recoil * 3.0, 0.0, 0.0)
523 } else {
524 Vec3::ZERO
525 };
526
527 transforms.push(GlyphTransform {
528 position_offset: recoil_offset + spread + Vec3::new(shake_x, shake_y, 0.0),
529 scale: 1.0 + recoil * 0.04,
530 rotation_z: (t * 30.0 + phase).sin() * recoil * 0.05,
531 emission: recoil * 0.4,
532 });
533 }
534 AnimPose { transforms }
535 }
536
537 fn pose_flee(&self, t: f32, n: usize) -> AnimPose {
539 let mut transforms = Vec::with_capacity(n);
540 let bob_freq = 3.0; let bob_amp = 5.0;
542
543 let turn_progress = (t / 0.3).clamp(0.0, 1.0);
545 let rotation = turn_progress * PI;
546
547 for i in 0..n {
548 let phase = i as f32 * 0.2;
549 let bob = (TAU * bob_freq * t + phase).sin();
550 let y_bob = bob * bob_amp;
551
552 let trail_emission = (i as f32 / n.max(1) as f32) * 0.6;
554
555 transforms.push(GlyphTransform {
556 position_offset: Vec3::new(0.0, y_bob, 0.0),
557 scale: 1.0,
558 rotation_z: rotation,
559 emission: trail_emission,
560 });
561 }
562 AnimPose { transforms }
563 }
564
565 fn pose_channel(&self, t: f32, n: usize) -> AnimPose {
567 let mut transforms = Vec::with_capacity(n);
568 let pulse_freq = 2.0;
569
570 for i in 0..n {
571 let is_hand = i >= n * 2 / 3;
572 let phase = i as f32 * TAU / n.max(1) as f32;
573
574 let y_up = if is_hand { 7.0 } else { 0.5 };
576
577 let pulse = ((TAU * pulse_freq * t + phase).sin() * 0.5 + 0.5).clamp(0.0, 1.0);
579 let glow = if is_hand { pulse * 0.9 } else { pulse * 0.15 };
580
581 let orbit_offset = if is_hand {
583 let orbit_angle = t * TAU * 0.5 + phase;
584 Vec3::new(orbit_angle.cos() * 1.5, y_up + orbit_angle.sin() * 1.5, 0.0)
585 } else {
586 Vec3::new(0.0, y_up, 0.0)
587 };
588
589 transforms.push(GlyphTransform {
590 position_offset: orbit_offset,
591 scale: 1.0 + pulse * 0.02,
592 rotation_z: if is_hand { t * 0.5 } else { 0.0 },
593 emission: glow,
594 });
595 }
596 AnimPose { transforms }
597 }
598
599 fn pose_dodge(&self, t: f32, n: usize) -> AnimPose {
601 let mut transforms = Vec::with_capacity(n);
602 let duration = 0.25;
603 let progress = (t / duration).clamp(0.0, 1.0);
604
605 let lateral = (progress * PI).sin() * 12.0;
607 let move_dir = self.move_direction.normalize_or_zero();
608
609 let squash = if progress < 0.5 {
611 progress / 0.5
612 } else {
613 1.0 - (progress - 0.5) / 0.5
614 };
615
616 for i in 0..n {
617 let offset = move_dir * lateral;
618 let scale_x = 1.0 - squash * 0.15;
620 let scale_y = 1.0 + squash * 0.15;
621 let avg_scale = (scale_x + scale_y) * 0.5;
623
624 transforms.push(GlyphTransform {
625 position_offset: offset,
626 scale: avg_scale,
627 rotation_z: squash * 0.1 * if i % 2 == 0 { 1.0 } else { -1.0 },
628 emission: squash * 0.2,
629 });
630 }
631 AnimPose { transforms }
632 }
633
634 fn pose_interact(&self, t: f32, n: usize) -> AnimPose {
636 let mut transforms = Vec::with_capacity(n);
637 let duration = 0.5;
638 let progress = (t / duration).clamp(0.0, 1.0);
639 let reach = (progress * PI).sin();
640
641 for i in 0..n {
642 let is_arm = i >= n / 2;
643 let forward = if is_arm { reach * 4.0 } else { reach * 1.0 };
644 let lean = reach * 0.06;
645
646 transforms.push(GlyphTransform {
647 position_offset: Vec3::new(forward, -reach * 0.5, 0.0),
648 scale: 1.0,
649 rotation_z: lean,
650 emission: 0.0,
651 });
652 }
653 AnimPose { transforms }
654 }
655}
656
657#[derive(Debug, Clone)]
663pub struct AnimTransitionDef {
664 pub from_state: PlayerAnimState,
665 pub to_state: PlayerAnimState,
666 pub duration: f32,
667 pub blend_curve: BlendCurve,
668}
669
670fn transition_params(from: PlayerAnimState, to: PlayerAnimState) -> (f32, BlendCurve) {
673 if to == PlayerAnimState::Hurt {
675 return (0.05, BlendCurve::Linear);
676 }
677
678 match (from, to) {
679 (PlayerAnimState::Idle, PlayerAnimState::Walk) => (0.2, BlendCurve::EaseInOut),
680 (PlayerAnimState::Walk, PlayerAnimState::Idle) => (0.2, BlendCurve::EaseInOut),
681 (PlayerAnimState::Walk, PlayerAnimState::Attack) => (0.1, BlendCurve::EaseIn),
682 (PlayerAnimState::Idle, PlayerAnimState::Attack) => (0.1, BlendCurve::EaseIn),
683 (PlayerAnimState::Attack, PlayerAnimState::Idle) => (0.3, BlendCurve::EaseOut),
684 (PlayerAnimState::HeavyAttack, PlayerAnimState::Idle) => (0.4, BlendCurve::EaseOut),
685 (PlayerAnimState::Hurt, PlayerAnimState::Idle) => (0.4, BlendCurve::EaseOut),
686 (PlayerAnimState::Idle, PlayerAnimState::Cast) => (0.3, BlendCurve::EaseIn),
687 (PlayerAnimState::Cast, PlayerAnimState::Idle) => (0.2, BlendCurve::EaseOut),
688 (PlayerAnimState::Idle, PlayerAnimState::Channel) => (0.3, BlendCurve::EaseIn),
689 (PlayerAnimState::Channel, PlayerAnimState::Idle) => (0.25, BlendCurve::EaseOut),
690 (PlayerAnimState::Idle, PlayerAnimState::Defend) => (0.15, BlendCurve::EaseIn),
691 (PlayerAnimState::Defend, PlayerAnimState::Idle) => (0.2, BlendCurve::EaseOut),
692 (PlayerAnimState::Idle, PlayerAnimState::Flee) => (0.15, BlendCurve::EaseIn),
693 (PlayerAnimState::Flee, PlayerAnimState::Idle) => (0.3, BlendCurve::EaseOut),
694 (PlayerAnimState::Idle, PlayerAnimState::Dodge) => (0.05, BlendCurve::Linear),
695 (PlayerAnimState::Dodge, PlayerAnimState::Idle) => (0.15, BlendCurve::EaseOut),
696 (PlayerAnimState::Walk, PlayerAnimState::Dodge) => (0.05, BlendCurve::Linear),
697 (PlayerAnimState::Idle, PlayerAnimState::Interact) => (0.2, BlendCurve::EaseInOut),
698 (PlayerAnimState::Interact, PlayerAnimState::Idle) => (0.2, BlendCurve::EaseOut),
699 _ => (0.2, BlendCurve::EaseInOut), }
701}
702
703pub fn build_transition_table() -> Vec<AnimTransitionDef> {
705 let entries: &[(PlayerAnimState, PlayerAnimState, f32, BlendCurve)] = &[
706 (PlayerAnimState::Idle, PlayerAnimState::Walk, 0.2, BlendCurve::EaseInOut),
707 (PlayerAnimState::Walk, PlayerAnimState::Attack, 0.1, BlendCurve::EaseIn),
708 (PlayerAnimState::Attack, PlayerAnimState::Idle, 0.3, BlendCurve::EaseOut),
709 (PlayerAnimState::Idle, PlayerAnimState::Cast, 0.3, BlendCurve::EaseIn),
710 (PlayerAnimState::Cast, PlayerAnimState::Idle, 0.2, BlendCurve::EaseOut),
711 (PlayerAnimState::Hurt, PlayerAnimState::Idle, 0.4, BlendCurve::EaseOut),
712 (PlayerAnimState::Idle, PlayerAnimState::Defend, 0.15, BlendCurve::EaseIn),
713 (PlayerAnimState::Defend, PlayerAnimState::Idle, 0.2, BlendCurve::EaseOut),
714 (PlayerAnimState::Idle, PlayerAnimState::Flee, 0.15, BlendCurve::EaseIn),
715 (PlayerAnimState::Idle, PlayerAnimState::Channel, 0.3, BlendCurve::EaseIn),
716 (PlayerAnimState::Idle, PlayerAnimState::Dodge, 0.05, BlendCurve::Linear),
717 (PlayerAnimState::Idle, PlayerAnimState::Interact,0.2, BlendCurve::EaseInOut),
718 ];
719 entries
720 .iter()
721 .map(|&(from, to, dur, curve)| AnimTransitionDef {
722 from_state: from,
723 to_state: to,
724 duration: dur,
725 blend_curve: curve,
726 })
727 .collect()
728}
729
730pub struct BlendTree1D {
736 pub parameter: f32,
738 entries: Vec<(f32, AnimPose)>,
740}
741
742impl BlendTree1D {
743 pub fn new() -> Self {
744 Self {
745 parameter: 0.0,
746 entries: Vec::new(),
747 }
748 }
749
750 pub fn add_entry(&mut self, param: f32, pose: AnimPose) {
752 self.entries.push((param, pose));
753 self.entries.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
754 }
755
756 pub fn evaluate(&self) -> AnimPose {
758 if self.entries.is_empty() {
759 return AnimPose::identity(0);
760 }
761 if self.entries.len() == 1 {
762 return self.entries[0].1.clone();
763 }
764
765 let p = self.parameter;
766
767 if p <= self.entries[0].0 {
769 return self.entries[0].1.clone();
770 }
771 if p >= self.entries.last().unwrap().0 {
773 return self.entries.last().unwrap().1.clone();
774 }
775
776 for i in 0..self.entries.len() - 1 {
778 let (p0, ref pose0) = self.entries[i];
779 let (p1, ref pose1) = self.entries[i + 1];
780 if p >= p0 && p <= p1 {
781 let range = p1 - p0;
782 let t = if range.abs() < 1e-6 { 0.0 } else { (p - p0) / range };
783 return pose0.blend(pose1, t);
784 }
785 }
786
787 self.entries.last().unwrap().1.clone()
788 }
789}
790
791pub struct IdleWalkBlend {
793 pub tree: BlendTree1D,
794}
795
796impl IdleWalkBlend {
797 pub fn new(glyph_count: usize) -> Self {
798 let ctrl = PlayerAnimController::new(glyph_count);
799 let idle_pose = ctrl.pose_idle(0.0, glyph_count);
800 let walk_pose = ctrl.pose_walk(0.0, glyph_count);
801
802 let mut tree = BlendTree1D::new();
803 tree.add_entry(0.0, idle_pose);
804 tree.add_entry(1.0, walk_pose);
805 Self { tree }
806 }
807
808 pub fn evaluate(&mut self, speed: f32) -> AnimPose {
809 self.tree.parameter = speed.clamp(0.0, 1.0);
810 self.tree.evaluate()
811 }
812}
813
814pub struct AttackPowerBlend {
816 pub tree: BlendTree1D,
817}
818
819impl AttackPowerBlend {
820 pub fn new(glyph_count: usize) -> Self {
821 let ctrl = PlayerAnimController::new(glyph_count);
822 let light = ctrl.pose_attack(0.15, glyph_count); let heavy = ctrl.pose_heavy_attack(0.65, glyph_count); let mut tree = BlendTree1D::new();
826 tree.add_entry(0.0, light);
827 tree.add_entry(1.0, heavy);
828 Self { tree }
829 }
830
831 pub fn evaluate(&mut self, force: f32) -> AnimPose {
832 self.tree.parameter = force.clamp(0.0, 1.0);
833 self.tree.evaluate()
834 }
835}
836
837pub struct DamageReactionBlend {
839 pub tree: BlendTree1D,
840}
841
842impl DamageReactionBlend {
843 pub fn new(glyph_count: usize) -> Self {
844 let mut ctrl_small = PlayerAnimController::new(glyph_count);
846 ctrl_small.damage_direction = Vec3::NEG_X;
847 let small_recoil = ctrl_small.pose_hurt(0.06, glyph_count); let mut ctrl_large = PlayerAnimController::new(glyph_count);
850 ctrl_large.damage_direction = Vec3::NEG_X;
851 let large_recoil = ctrl_large.pose_hurt(0.06, glyph_count);
852 let large_recoil = AnimPose {
854 transforms: large_recoil
855 .transforms
856 .into_iter()
857 .map(|mut gt| {
858 gt.position_offset *= 2.5;
859 gt.emission *= 2.0;
860 gt
861 })
862 .collect(),
863 };
864
865 let mut tree = BlendTree1D::new();
866 tree.add_entry(0.0, small_recoil);
867 tree.add_entry(1.0, large_recoil);
868 Self { tree }
869 }
870
871 pub fn evaluate(&mut self, damage_fraction: f32) -> AnimPose {
872 self.tree.parameter = damage_fraction.clamp(0.0, 1.0);
873 self.tree.evaluate()
874 }
875}
876
877pub struct CastIntensityBlend {
879 pub tree: BlendTree1D,
880}
881
882impl CastIntensityBlend {
883 pub fn new(glyph_count: usize) -> Self {
884 let ctrl = PlayerAnimController::new(glyph_count);
885 let small_cast = ctrl.pose_cast(0.3, glyph_count);
886 let large_cast_raw = ctrl.pose_cast(0.3, glyph_count);
887 let large_cast = AnimPose {
888 transforms: large_cast_raw
889 .transforms
890 .into_iter()
891 .map(|mut gt| {
892 gt.position_offset *= 1.5;
893 gt.emission *= 2.5;
894 gt.scale += 0.05;
895 gt
896 })
897 .collect(),
898 };
899
900 let mut tree = BlendTree1D::new();
901 tree.add_entry(0.0, small_cast);
902 tree.add_entry(1.0, large_cast);
903 Self { tree }
904 }
905
906 pub fn evaluate(&mut self, mana_fraction: f32) -> AnimPose {
907 self.tree.parameter = mana_fraction.clamp(0.0, 1.0);
908 self.tree.evaluate()
909 }
910}
911
912#[derive(Debug, Clone)]
918pub struct IKChain {
919 pub root_pos: Vec3,
920 pub mid_pos: Vec3,
921 pub end_pos: Vec3,
922 pub lengths: [f32; 2],
924}
925
926impl IKChain {
927 pub fn new(root: Vec3, mid: Vec3, end: Vec3) -> Self {
928 let l0 = (mid - root).length();
929 let l1 = (end - mid).length();
930 Self {
931 root_pos: root,
932 mid_pos: mid,
933 end_pos: end,
934 lengths: [l0, l1],
935 }
936 }
937
938 pub fn total_length(&self) -> f32 {
940 self.lengths[0] + self.lengths[1]
941 }
942}
943
944pub fn solve_two_bone(chain: &mut IKChain, target: Vec3) {
949 let l0 = chain.lengths[0];
950 let l1 = chain.lengths[1];
951 let total = l0 + l1;
952
953 let root = chain.root_pos;
954 let to_target = target - root;
955 let dist = to_target.length().max(0.001);
956
957 let dist_clamped = dist.min(total - 0.001).max((l0 - l1).abs() + 0.001);
959 let direction = to_target.normalize_or_zero();
960 let clamped_target = root + direction * dist_clamped;
961
962 let cos_angle = ((l0 * l0 + dist_clamped * dist_clamped - l1 * l1) / (2.0 * l0 * dist_clamped))
964 .clamp(-1.0, 1.0);
965 let angle = cos_angle.acos();
966
967 let up = if direction.dot(Vec3::Y).abs() > 0.99 {
970 Vec3::Z
971 } else {
972 Vec3::Y
973 };
974 let side = direction.cross(up).normalize_or_zero();
975 let bend_axis = side.cross(direction).normalize_or_zero();
976
977 let cos_a = angle.cos();
979 let sin_a = angle.sin();
980 let mid_dir = direction * cos_a + bend_axis * sin_a;
981 chain.mid_pos = root + mid_dir * l0;
982
983 let mid_to_target = (clamped_target - chain.mid_pos).normalize_or_zero();
985 chain.end_pos = chain.mid_pos + mid_to_target * l1;
986}
987
988#[derive(Debug, Clone)]
990pub enum IKTarget {
991 Enemy(EntityId),
993 Position(Vec3),
995 Offset(Vec3),
997 LookDirection(Vec3),
999 None,
1001}
1002
1003#[derive(Debug, Clone)]
1005pub struct IKLimb {
1006 pub name: String,
1007 pub chain: IKChain,
1008 pub target: IKTarget,
1009 pub glyph_indices: [usize; 3],
1011 pub weight: f32,
1013}
1014
1015impl IKLimb {
1016 pub fn new(name: &str, glyph_indices: [usize; 3], rest_positions: [Vec3; 3]) -> Self {
1017 Self {
1018 name: name.to_string(),
1019 chain: IKChain::new(rest_positions[0], rest_positions[1], rest_positions[2]),
1020 target: IKTarget::None,
1021 glyph_indices,
1022 weight: 1.0,
1023 }
1024 }
1025
1026 pub fn solve(&mut self, entity_pos: Vec3, entities: &HashMap<EntityId, Vec3>) -> [Vec3; 3] {
1028 let world_target = match &self.target {
1029 IKTarget::Enemy(id) => {
1030 entities.get(id).copied().unwrap_or(entity_pos + Vec3::X * 5.0)
1031 }
1032 IKTarget::Position(p) => *p,
1033 IKTarget::Offset(o) => entity_pos + *o,
1034 IKTarget::LookDirection(dir) => entity_pos + dir.normalize_or_zero() * 10.0,
1035 IKTarget::None => {
1036 return [Vec3::ZERO; 3];
1038 }
1039 };
1040
1041 solve_two_bone(&mut self.chain, world_target);
1042
1043 let offsets = [
1045 (self.chain.root_pos - entity_pos) * self.weight,
1046 (self.chain.mid_pos - entity_pos) * self.weight,
1047 (self.chain.end_pos - entity_pos) * self.weight,
1048 ];
1049 offsets
1050 }
1051}
1052
1053pub fn weapon_arm_ik(glyph_indices: [usize; 3], rest: [Vec3; 3]) -> IKLimb {
1057 let mut limb = IKLimb::new("weapon_arm", glyph_indices, rest);
1058 limb.weight = 0.8;
1059 limb
1060}
1061
1062pub fn look_at_ik(glyph_indices: [usize; 3], rest: [Vec3; 3]) -> IKLimb {
1064 let mut limb = IKLimb::new("look_at", glyph_indices, rest);
1065 limb.weight = 0.6;
1066 limb
1067}
1068
1069pub fn staff_aim_ik(glyph_indices: [usize; 3], rest: [Vec3; 3]) -> IKLimb {
1071 let mut limb = IKLimb::new("staff_aim", glyph_indices, rest);
1072 limb.weight = 0.9;
1073 limb
1074}
1075
1076pub fn shield_ik(glyph_indices: [usize; 3], rest: [Vec3; 3]) -> IKLimb {
1078 let mut limb = IKLimb::new("shield", glyph_indices, rest);
1079 limb.weight = 1.0;
1080 limb
1081}
1082
1083#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1089pub enum EnemyAnimState {
1090 Idle,
1091 Approach,
1092 Attack,
1093 Hurt,
1094 Die,
1095 Special,
1096}
1097
1098pub struct EnemyAnimController {
1100 pub current_state: EnemyAnimState,
1101 pub prev_state: EnemyAnimState,
1102 pub blend_factor: f32,
1103 pub transition_time: f32,
1104 pub state_timer: f32,
1105 transition_elapsed: f32,
1106 in_transition: bool,
1107 transition_curve: BlendCurve,
1108 glyph_count: usize,
1109 pub approach_direction: Vec3,
1111 pub death_progress: f32,
1113}
1114
1115impl EnemyAnimController {
1116 pub fn new(glyph_count: usize) -> Self {
1117 Self {
1118 current_state: EnemyAnimState::Idle,
1119 prev_state: EnemyAnimState::Idle,
1120 blend_factor: 1.0,
1121 transition_time: 0.0,
1122 state_timer: 0.0,
1123 transition_elapsed: 0.0,
1124 in_transition: false,
1125 transition_curve: BlendCurve::Linear,
1126 glyph_count,
1127 approach_direction: Vec3::X,
1128 death_progress: 0.0,
1129 }
1130 }
1131
1132 pub fn transition_to(&mut self, new_state: EnemyAnimState) -> bool {
1133 if new_state == self.current_state && !self.in_transition {
1134 return false;
1135 }
1136 let (duration, curve) = enemy_transition_params(self.current_state, new_state);
1137 self.prev_state = self.current_state;
1138 self.current_state = new_state;
1139 self.transition_time = duration;
1140 self.transition_elapsed = 0.0;
1141 self.blend_factor = 0.0;
1142 self.in_transition = true;
1143 self.transition_curve = curve;
1144 self.state_timer = 0.0;
1145 true
1146 }
1147
1148 pub fn update(&mut self, dt: f32) -> AnimPose {
1149 self.state_timer += dt;
1150
1151 if self.in_transition {
1152 self.transition_elapsed += dt;
1153 if self.transition_elapsed >= self.transition_time {
1154 self.blend_factor = 1.0;
1155 self.in_transition = false;
1156 } else {
1157 let raw = self.transition_elapsed / self.transition_time.max(0.001);
1158 self.blend_factor = self.transition_curve.evaluate(raw);
1159 }
1160 }
1161
1162 if self.in_transition {
1163 let prev = self.evaluate_state(self.prev_state);
1164 let curr = self.evaluate_state(self.current_state);
1165 prev.blend(&curr, self.blend_factor)
1166 } else {
1167 self.evaluate_state(self.current_state)
1168 }
1169 }
1170
1171 fn evaluate_state(&self, state: EnemyAnimState) -> AnimPose {
1172 let t = self.state_timer;
1173 let n = self.glyph_count;
1174 match state {
1175 EnemyAnimState::Idle => self.enemy_idle(t, n),
1176 EnemyAnimState::Approach => self.enemy_approach(t, n),
1177 EnemyAnimState::Attack => self.enemy_attack(t, n),
1178 EnemyAnimState::Hurt => self.enemy_hurt(t, n),
1179 EnemyAnimState::Die => self.enemy_die(t, n),
1180 EnemyAnimState::Special => self.enemy_special(t, n),
1181 }
1182 }
1183
1184 fn enemy_idle(&self, t: f32, n: usize) -> AnimPose {
1185 let mut transforms = Vec::with_capacity(n);
1186 for i in 0..n {
1187 let phase = i as f32 * 0.3;
1188 let bob = (TAU * 0.4 * t + phase).sin();
1189 transforms.push(GlyphTransform {
1190 position_offset: Vec3::new(0.0, bob * 1.5, 0.0),
1191 scale: 1.0 + bob * 0.01,
1192 rotation_z: 0.0,
1193 emission: 0.05,
1194 });
1195 }
1196 AnimPose { transforms }
1197 }
1198
1199 fn enemy_approach(&self, t: f32, n: usize) -> AnimPose {
1200 let mut transforms = Vec::with_capacity(n);
1201 let dir = self.approach_direction.normalize_or_zero();
1202 for i in 0..n {
1203 let phase = i as f32 * 0.25;
1204 let bob = (TAU * 1.2 * t + phase).sin();
1205 let lean = 0.1;
1206 transforms.push(GlyphTransform {
1207 position_offset: Vec3::new(dir.x * 2.0, bob * 3.0, 0.0),
1208 scale: 1.0,
1209 rotation_z: lean,
1210 emission: 0.1,
1211 });
1212 }
1213 AnimPose { transforms }
1214 }
1215
1216 fn enemy_attack(&self, t: f32, n: usize) -> AnimPose {
1217 let mut transforms = Vec::with_capacity(n);
1218 let progress = (t / 0.4).clamp(0.0, 1.0);
1219 let swing = (progress * PI).sin();
1220 for i in 0..n {
1221 let factor = if i >= n / 2 { 1.5 } else { 0.7 };
1222 transforms.push(GlyphTransform {
1223 position_offset: Vec3::new(swing * 5.0 * factor, -swing * 2.0, 0.0),
1224 scale: 1.0 + swing * 0.04,
1225 rotation_z: swing * 0.15 * factor,
1226 emission: swing * 0.5,
1227 });
1228 }
1229 AnimPose { transforms }
1230 }
1231
1232 fn enemy_hurt(&self, t: f32, n: usize) -> AnimPose {
1233 let mut transforms = Vec::with_capacity(n);
1234 let recoil = (1.0 - (t / 0.3).clamp(0.0, 1.0)).max(0.0);
1235 for i in 0..n {
1236 let phase = i as f32 * 3.1;
1237 let shake_x = (t * 35.0 + phase).sin() * recoil * 2.5;
1238 let shake_y = (t * 31.0 + phase).cos() * recoil * 2.5;
1239 transforms.push(GlyphTransform {
1240 position_offset: Vec3::new(shake_x - recoil * 3.0, shake_y, 0.0),
1241 scale: 1.0 + recoil * 0.03,
1242 rotation_z: (t * 25.0 + phase).sin() * recoil * 0.06,
1243 emission: recoil * 0.6,
1244 });
1245 }
1246 AnimPose { transforms }
1247 }
1248
1249 fn enemy_die(&self, t: f32, n: usize) -> AnimPose {
1250 let mut transforms = Vec::with_capacity(n);
1251 let progress = (t / 1.5).clamp(0.0, 1.0);
1252 self.death_progress;
1253
1254 for i in 0..n {
1255 let phase = i as f32 * 1.618;
1256 let angle = phase * TAU;
1258 let scatter = progress * progress * 8.0;
1259 let x = angle.cos() * scatter;
1260 let y = angle.sin() * scatter + progress * 3.0; let fade_scale = (1.0 - progress).max(0.0);
1263 transforms.push(GlyphTransform {
1264 position_offset: Vec3::new(x, y, 0.0),
1265 scale: fade_scale,
1266 rotation_z: progress * TAU * 0.5 * if i % 2 == 0 { 1.0 } else { -1.0 },
1267 emission: (1.0 - progress) * 0.8,
1268 });
1269 }
1270 AnimPose { transforms }
1271 }
1272
1273 fn enemy_special(&self, t: f32, n: usize) -> AnimPose {
1274 let mut transforms = Vec::with_capacity(n);
1276 let pulse = (TAU * 2.0 * t).sin() * 0.5 + 0.5;
1277 for i in 0..n {
1278 let phase = i as f32 * TAU / n.max(1) as f32;
1279 let orbit = (t * TAU * 0.3 + phase).sin() * 2.0;
1280 transforms.push(GlyphTransform {
1281 position_offset: Vec3::new(orbit, (t * TAU * 0.3 + phase).cos() * 2.0, 0.0),
1282 scale: 1.0 + pulse * 0.08,
1283 rotation_z: t * 0.2,
1284 emission: pulse * 0.7,
1285 });
1286 }
1287 AnimPose { transforms }
1288 }
1289}
1290
1291fn enemy_transition_params(from: EnemyAnimState, to: EnemyAnimState) -> (f32, BlendCurve) {
1292 if to == EnemyAnimState::Hurt {
1293 return (0.05, BlendCurve::Linear);
1294 }
1295 if to == EnemyAnimState::Die {
1296 return (0.1, BlendCurve::EaseIn);
1297 }
1298 match (from, to) {
1299 (EnemyAnimState::Idle, EnemyAnimState::Approach) => (0.2, BlendCurve::EaseInOut),
1300 (EnemyAnimState::Approach, EnemyAnimState::Attack) => (0.1, BlendCurve::EaseIn),
1301 (EnemyAnimState::Attack, EnemyAnimState::Idle) => (0.3, BlendCurve::EaseOut),
1302 (EnemyAnimState::Hurt, EnemyAnimState::Idle) => (0.35, BlendCurve::EaseOut),
1303 (EnemyAnimState::Idle, EnemyAnimState::Special) => (0.25, BlendCurve::EaseIn),
1304 _ => (0.2, BlendCurve::EaseInOut),
1305 }
1306}
1307
1308#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1314pub enum HydraAnimState {
1315 Normal,
1316 Splitting,
1318 Reforming,
1320}
1321
1322#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1324pub enum CommitteeAnimState {
1325 Normal,
1326 Voting,
1328 Verdict,
1330}
1331
1332#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1334pub enum AlgorithmAnimState {
1335 Phase1,
1336 PhaseTransition,
1338 Phase2,
1339 Phase3,
1340}
1341
1342pub struct BossAnimController {
1344 pub base: EnemyAnimController,
1345 pub boss_state: BossState,
1346 pub split_progress: f32,
1348 pub voting_index: usize,
1350 pub judge_count: usize,
1352 pub vote_timer: f32,
1354 pub phase_transition_progress: f32,
1356}
1357
1358#[derive(Debug, Clone, Copy, PartialEq)]
1360pub enum BossState {
1361 Hydra(HydraAnimState),
1362 Committee(CommitteeAnimState),
1363 Algorithm(AlgorithmAnimState),
1364}
1365
1366impl BossAnimController {
1367 pub fn new_hydra(glyph_count: usize) -> Self {
1368 Self {
1369 base: EnemyAnimController::new(glyph_count),
1370 boss_state: BossState::Hydra(HydraAnimState::Normal),
1371 split_progress: 0.0,
1372 voting_index: 0,
1373 judge_count: 5,
1374 vote_timer: 0.0,
1375 phase_transition_progress: 0.0,
1376 }
1377 }
1378
1379 pub fn new_committee(glyph_count: usize, judge_count: usize) -> Self {
1380 Self {
1381 base: EnemyAnimController::new(glyph_count),
1382 boss_state: BossState::Committee(CommitteeAnimState::Normal),
1383 split_progress: 0.0,
1384 voting_index: 0,
1385 judge_count,
1386 vote_timer: 0.0,
1387 phase_transition_progress: 0.0,
1388 }
1389 }
1390
1391 pub fn new_algorithm(glyph_count: usize) -> Self {
1392 Self {
1393 base: EnemyAnimController::new(glyph_count),
1394 boss_state: BossState::Algorithm(AlgorithmAnimState::Phase1),
1395 split_progress: 0.0,
1396 voting_index: 0,
1397 judge_count: 0,
1398 vote_timer: 0.0,
1399 phase_transition_progress: 0.0,
1400 }
1401 }
1402
1403 pub fn update(&mut self, dt: f32) -> AnimPose {
1404 let base_pose = self.base.update(dt);
1405 let n = base_pose.transforms.len();
1406
1407 match self.boss_state {
1408 BossState::Hydra(state) => self.apply_hydra(state, &base_pose, dt, n),
1409 BossState::Committee(state) => self.apply_committee(state, &base_pose, dt, n),
1410 BossState::Algorithm(state) => self.apply_algorithm(state, &base_pose, dt, n),
1411 }
1412 }
1413
1414 fn apply_hydra(&mut self, state: HydraAnimState, base: &AnimPose, dt: f32, n: usize) -> AnimPose {
1415 match state {
1416 HydraAnimState::Normal => base.clone(),
1417 HydraAnimState::Splitting => {
1418 self.split_progress = (self.split_progress + dt / 1.0).min(1.0);
1419 let p = self.split_progress;
1420 let mut transforms = Vec::with_capacity(n);
1421 for i in 0..n {
1422 let side = if i < n / 2 { -1.0 } else { 1.0 };
1423 let spread = p * 8.0 * side;
1424 let dissolve_scale = 1.0 - p * 0.3;
1425 let mut gt = base.transforms.get(i).copied().unwrap_or_default();
1426 gt.position_offset.x += spread;
1427 gt.scale *= dissolve_scale;
1428 gt.emission += p * 0.5;
1429 transforms.push(gt);
1430 }
1431 AnimPose { transforms }
1432 }
1433 HydraAnimState::Reforming => {
1434 self.split_progress = (self.split_progress - dt / 1.5).max(0.0);
1435 let p = self.split_progress;
1436 let mut transforms = Vec::with_capacity(n);
1437 for i in 0..n {
1438 let side = if i < n / 2 { -1.0 } else { 1.0 };
1439 let spread = p * 8.0 * side;
1440 let mut gt = base.transforms.get(i).copied().unwrap_or_default();
1441 gt.position_offset.x += spread;
1442 gt.emission += p * 0.3;
1443 transforms.push(gt);
1444 }
1445 AnimPose { transforms }
1446 }
1447 }
1448 }
1449
1450 fn apply_committee(
1451 &mut self,
1452 state: CommitteeAnimState,
1453 base: &AnimPose,
1454 dt: f32,
1455 n: usize,
1456 ) -> AnimPose {
1457 match state {
1458 CommitteeAnimState::Normal => base.clone(),
1459 CommitteeAnimState::Voting => {
1460 self.vote_timer += dt;
1461 if self.vote_timer >= 0.5 {
1462 self.vote_timer -= 0.5;
1463 self.voting_index += 1;
1464 if self.voting_index >= self.judge_count {
1465 self.boss_state = BossState::Committee(CommitteeAnimState::Verdict);
1466 self.voting_index = 0;
1467 }
1468 }
1469 let glyphs_per_judge = n / self.judge_count.max(1);
1470 let mut transforms = Vec::with_capacity(n);
1471 for i in 0..n {
1472 let judge_idx = i / glyphs_per_judge.max(1);
1473 let mut gt = base.transforms.get(i).copied().unwrap_or_default();
1474 if judge_idx == self.voting_index {
1475 gt.emission += 0.8;
1476 gt.scale += 0.05;
1477 }
1478 transforms.push(gt);
1479 }
1480 AnimPose { transforms }
1481 }
1482 CommitteeAnimState::Verdict => {
1483 let mut transforms = Vec::with_capacity(n);
1484 let t = self.base.state_timer;
1485 let flash = (TAU * 4.0 * t).sin() * 0.5 + 0.5;
1486 for i in 0..n {
1487 let mut gt = base.transforms.get(i).copied().unwrap_or_default();
1488 gt.emission += flash;
1489 gt.scale += flash * 0.03;
1490 transforms.push(gt);
1491 }
1492 AnimPose { transforms }
1493 }
1494 }
1495 }
1496
1497 fn apply_algorithm(
1498 &mut self,
1499 state: AlgorithmAnimState,
1500 base: &AnimPose,
1501 dt: f32,
1502 n: usize,
1503 ) -> AnimPose {
1504 match state {
1505 AlgorithmAnimState::Phase1 | AlgorithmAnimState::Phase2 | AlgorithmAnimState::Phase3 => {
1506 base.clone()
1507 }
1508 AlgorithmAnimState::PhaseTransition => {
1509 self.phase_transition_progress =
1510 (self.phase_transition_progress + dt / 2.0).min(1.0);
1511 let p = self.phase_transition_progress;
1512 let mut transforms = Vec::with_capacity(n);
1513 for i in 0..n {
1514 let phase = i as f32 * TAU / n.max(1) as f32;
1515 let chaos = (p * PI).sin(); let orbit_r = chaos * 6.0;
1517 let angle = phase + p * TAU * 2.0;
1518 let mut gt = base.transforms.get(i).copied().unwrap_or_default();
1519 gt.position_offset += Vec3::new(
1520 angle.cos() * orbit_r,
1521 angle.sin() * orbit_r,
1522 0.0,
1523 );
1524 gt.rotation_z += p * TAU;
1525 gt.emission += chaos * 0.7;
1526 gt.scale = gt.scale * (1.0 - chaos * 0.2);
1527 transforms.push(gt);
1528 }
1529 AnimPose { transforms }
1530 }
1531 }
1532 }
1533}
1534
1535pub struct AnimationManager {
1541 pub player_controllers: HashMap<EntityId, PlayerAnimController>,
1543 pub enemy_controllers: HashMap<EntityId, EnemyAnimController>,
1545 pub boss_controllers: HashMap<EntityId, BossAnimController>,
1547 pub ik_limbs: HashMap<(EntityId, String), IKLimb>,
1549 output_cache: HashMap<EntityId, Vec<GlyphTransform>>,
1551 pub entity_positions: HashMap<EntityId, Vec3>,
1553}
1554
1555impl AnimationManager {
1556 pub fn new() -> Self {
1557 Self {
1558 player_controllers: HashMap::new(),
1559 enemy_controllers: HashMap::new(),
1560 boss_controllers: HashMap::new(),
1561 ik_limbs: HashMap::new(),
1562 output_cache: HashMap::new(),
1563 entity_positions: HashMap::new(),
1564 }
1565 }
1566
1567 pub fn register_player(&mut self, entity_id: EntityId, glyph_count: usize) {
1569 self.player_controllers
1570 .insert(entity_id, PlayerAnimController::new(glyph_count));
1571 }
1572
1573 pub fn register_enemy(&mut self, entity_id: EntityId, glyph_count: usize) {
1575 self.enemy_controllers
1576 .insert(entity_id, EnemyAnimController::new(glyph_count));
1577 }
1578
1579 pub fn register_boss(&mut self, entity_id: EntityId, controller: BossAnimController) {
1581 self.boss_controllers.insert(entity_id, controller);
1582 }
1583
1584 pub fn register_ik_limb(&mut self, entity_id: EntityId, limb: IKLimb) {
1586 self.ik_limbs
1587 .insert((entity_id, limb.name.clone()), limb);
1588 }
1589
1590 pub fn update(&mut self, dt: f32) {
1592 self.output_cache.clear();
1593
1594 let player_ids: Vec<EntityId> = self.player_controllers.keys().copied().collect();
1596 for id in player_ids {
1597 if let Some(ctrl) = self.player_controllers.get_mut(&id) {
1598 let pose = ctrl.update(dt);
1599 self.output_cache.insert(id, pose.transforms);
1600 }
1601 }
1602
1603 let enemy_ids: Vec<EntityId> = self.enemy_controllers.keys().copied().collect();
1605 for id in enemy_ids {
1606 if let Some(ctrl) = self.enemy_controllers.get_mut(&id) {
1607 let pose = ctrl.update(dt);
1608 self.output_cache.insert(id, pose.transforms);
1609 }
1610 }
1611
1612 let boss_ids: Vec<EntityId> = self.boss_controllers.keys().copied().collect();
1614 for id in boss_ids {
1615 if let Some(ctrl) = self.boss_controllers.get_mut(&id) {
1616 let pose = ctrl.update(dt);
1617 self.output_cache.insert(id, pose.transforms);
1618 }
1619 }
1620
1621 let ik_keys: Vec<(EntityId, String)> = self.ik_limbs.keys().cloned().collect();
1623 for (entity_id, limb_name) in ik_keys {
1624 if let Some(limb) = self.ik_limbs.get_mut(&(entity_id, limb_name)) {
1625 let entity_pos = self.entity_positions.get(&entity_id).copied().unwrap_or(Vec3::ZERO);
1626 let offsets = limb.solve(entity_pos, &self.entity_positions);
1627 if let Some(transforms) = self.output_cache.get_mut(&entity_id) {
1629 for (slot, &glyph_idx) in limb.glyph_indices.iter().enumerate() {
1630 if glyph_idx < transforms.len() {
1631 let ik_blend = limb.weight;
1632 transforms[glyph_idx].position_offset = transforms[glyph_idx]
1633 .position_offset
1634 * (1.0 - ik_blend)
1635 + offsets[slot] * ik_blend;
1636 }
1637 }
1638 }
1639 }
1640 }
1641 }
1642
1643 pub fn trigger_player_state(&mut self, entity_id: EntityId, new_state: PlayerAnimState) -> bool {
1645 if let Some(ctrl) = self.player_controllers.get_mut(&entity_id) {
1646 ctrl.transition_to(new_state)
1647 } else {
1648 false
1649 }
1650 }
1651
1652 pub fn trigger_enemy_state(&mut self, entity_id: EntityId, new_state: EnemyAnimState) -> bool {
1654 if let Some(ctrl) = self.enemy_controllers.get_mut(&entity_id) {
1655 ctrl.transition_to(new_state)
1656 } else {
1657 false
1658 }
1659 }
1660
1661 pub fn set_ik_target(&mut self, entity_id: EntityId, chain_name: &str, target: IKTarget) {
1663 if let Some(limb) = self.ik_limbs.get_mut(&(entity_id, chain_name.to_string())) {
1664 limb.target = target;
1665 }
1666 }
1667
1668 pub fn get_glyph_transforms(&self, entity_id: EntityId) -> Vec<GlyphTransform> {
1670 self.output_cache
1671 .get(&entity_id)
1672 .cloned()
1673 .unwrap_or_default()
1674 }
1675
1676 pub fn remove_entity(&mut self, entity_id: EntityId) {
1678 self.player_controllers.remove(&entity_id);
1679 self.enemy_controllers.remove(&entity_id);
1680 self.boss_controllers.remove(&entity_id);
1681 self.output_cache.remove(&entity_id);
1682 self.entity_positions.remove(&entity_id);
1683 self.ik_limbs.retain(|(id, _), _| *id != entity_id);
1685 }
1686}
1687
1688impl Default for AnimationManager {
1689 fn default() -> Self {
1690 Self::new()
1691 }
1692}
1693
1694#[cfg(test)]
1699mod tests {
1700 use super::*;
1701
1702 #[test]
1705 fn glyph_transform_default_is_identity() {
1706 let gt = GlyphTransform::default();
1707 assert_eq!(gt.position_offset, Vec3::ZERO);
1708 assert!((gt.scale - 1.0).abs() < 1e-6);
1709 assert!((gt.rotation_z).abs() < 1e-6);
1710 assert!((gt.emission).abs() < 1e-6);
1711 }
1712
1713 #[test]
1714 fn glyph_transform_lerp_midpoint() {
1715 let a = GlyphTransform {
1716 position_offset: Vec3::ZERO,
1717 scale: 1.0,
1718 rotation_z: 0.0,
1719 emission: 0.0,
1720 };
1721 let b = GlyphTransform {
1722 position_offset: Vec3::new(10.0, 0.0, 0.0),
1723 scale: 2.0,
1724 rotation_z: 1.0,
1725 emission: 1.0,
1726 };
1727 let mid = GlyphTransform::lerp(&a, &b, 0.5);
1728 assert!((mid.position_offset.x - 5.0).abs() < 1e-6);
1729 assert!((mid.scale - 1.5).abs() < 1e-6);
1730 assert!((mid.rotation_z - 0.5).abs() < 1e-6);
1731 assert!((mid.emission - 0.5).abs() < 1e-6);
1732 }
1733
1734 #[test]
1737 fn blend_curve_endpoints() {
1738 for curve in &[BlendCurve::Linear, BlendCurve::EaseIn, BlendCurve::EaseOut, BlendCurve::EaseInOut] {
1739 let v0 = curve.evaluate(0.0);
1740 let v1 = curve.evaluate(1.0);
1741 assert!(v0.abs() < 1e-6, "curve {curve:?} at t=0 should be ~0, got {v0}");
1742 assert!((v1 - 1.0).abs() < 1e-6, "curve {curve:?} at t=1 should be ~1, got {v1}");
1743 }
1744 }
1745
1746 #[test]
1747 fn blend_curve_monotonic() {
1748 for curve in &[BlendCurve::Linear, BlendCurve::EaseIn, BlendCurve::EaseOut, BlendCurve::EaseInOut] {
1749 let mut prev = 0.0f32;
1750 for step in 1..=20 {
1751 let t = step as f32 / 20.0;
1752 let v = curve.evaluate(t);
1753 assert!(v >= prev - 1e-6, "curve {curve:?} not monotonic at t={t}");
1754 prev = v;
1755 }
1756 }
1757 }
1758
1759 #[test]
1762 fn player_idle_produces_correct_glyph_count() {
1763 let ctrl = PlayerAnimController::new(9);
1764 let pose = ctrl.pose_idle(0.5, 9);
1765 assert_eq!(pose.transforms.len(), 9);
1766 }
1767
1768 #[test]
1769 fn player_idle_breathing_range() {
1770 let ctrl = PlayerAnimController::new(1);
1771 let mut min_scale = f32::MAX;
1773 let mut max_scale = f32::MIN;
1774 for step in 0..100 {
1775 let t = step as f32 / 100.0 * (1.0 / 0.3); let pose = ctrl.pose_idle(t, 1);
1777 let s = pose.transforms[0].scale;
1778 min_scale = min_scale.min(s);
1779 max_scale = max_scale.max(s);
1780 }
1781 assert!(min_scale >= 0.97, "min scale {min_scale} should be >= 0.97");
1782 assert!(max_scale <= 1.03, "max scale {max_scale} should be <= 1.03");
1783 }
1784
1785 #[test]
1786 fn player_walk_higher_bob_than_idle() {
1787 let ctrl = PlayerAnimController::new(4);
1788 let idle = ctrl.pose_idle(0.25, 4);
1789 let walk = ctrl.pose_walk(0.25, 4);
1790 let idle_max_y: f32 = idle.transforms.iter().map(|t| t.position_offset.y.abs()).fold(0.0f32, f32::max);
1792 let walk_max_y: f32 = walk.transforms.iter().map(|t| t.position_offset.y.abs()).fold(0.0f32, f32::max);
1793 assert!(walk_max_y >= idle_max_y * 0.5, "walk bob should be comparable or larger than idle");
1796 }
1797
1798 #[test]
1799 fn player_attack_has_emission() {
1800 let ctrl = PlayerAnimController::new(6);
1801 let pose = ctrl.pose_attack(0.15, 6); let total_emission: f32 = pose.transforms.iter().map(|t| t.emission).sum();
1803 assert!(total_emission > 0.0, "attack should have emission glow");
1804 }
1805
1806 #[test]
1807 fn player_heavy_attack_phases() {
1808 let ctrl = PlayerAnimController::new(4);
1809 let windup = ctrl.pose_heavy_attack(0.25, 4);
1811 let swing = ctrl.pose_heavy_attack(0.65, 4);
1813 let swing_emission: f32 = swing.transforms.iter().map(|t| t.emission).sum();
1814 assert!(swing_emission > 0.0, "heavy attack swing should have emission");
1815 let follow = ctrl.pose_heavy_attack(0.9, 4);
1817 let follow_emission: f32 = follow.transforms.iter().map(|t| t.emission).sum();
1818 assert!(follow_emission < swing_emission, "follow-through emission should be less than swing");
1819 }
1820
1821 #[test]
1822 fn player_cast_hands_rise() {
1823 let ctrl = PlayerAnimController::new(9);
1824 let pose = ctrl.pose_cast(0.3, 9); let hand_y: f32 = pose.transforms[8].position_offset.y;
1827 let body_y: f32 = pose.transforms[0].position_offset.y;
1828 assert!(hand_y > body_y, "hand glyphs should rise higher than body glyphs");
1829 }
1830
1831 #[test]
1832 fn player_hurt_has_recoil() {
1833 let mut ctrl = PlayerAnimController::new(4);
1834 ctrl.damage_direction = Vec3::new(-1.0, 0.0, 0.0);
1835 let pose = ctrl.pose_hurt(0.05, 4); let avg_x: f32 = pose.transforms.iter().map(|t| t.position_offset.x).sum::<f32>() / 4.0;
1837 assert!(avg_x > 0.0, "hurt recoil should push away from damage source");
1839 }
1840
1841 #[test]
1842 fn player_defend_reduces_scale() {
1843 let ctrl = PlayerAnimController::new(6);
1844 let pose = ctrl.pose_defend(0.5, 6); for gt in &pose.transforms {
1846 assert!(gt.scale < 1.0, "defend should reduce scale (crouch), got {}", gt.scale);
1847 }
1848 }
1849
1850 #[test]
1851 fn player_dodge_has_lateral_movement() {
1852 let mut ctrl = PlayerAnimController::new(4);
1853 ctrl.move_direction = Vec3::X;
1854 let pose = ctrl.pose_dodge(0.125, 4); let avg_x: f32 = pose.transforms.iter().map(|t| t.position_offset.x).sum::<f32>() / 4.0;
1856 assert!(avg_x.abs() > 1.0, "dodge should have significant lateral movement");
1857 }
1858
1859 #[test]
1862 fn player_transition_idle_to_walk() {
1863 let mut ctrl = PlayerAnimController::new(4);
1864 assert!(ctrl.transition_to(PlayerAnimState::Walk));
1865 assert_eq!(ctrl.current_state, PlayerAnimState::Walk);
1866 assert_eq!(ctrl.prev_state, PlayerAnimState::Idle);
1867 assert!(ctrl.in_transition);
1868 }
1869
1870 #[test]
1871 fn player_transition_same_state_rejected() {
1872 let mut ctrl = PlayerAnimController::new(4);
1873 assert!(!ctrl.transition_to(PlayerAnimState::Idle));
1874 }
1875
1876 #[test]
1877 fn player_transition_blend_completes() {
1878 let mut ctrl = PlayerAnimController::new(4);
1879 ctrl.transition_to(PlayerAnimState::Walk);
1880 for _ in 0..20 {
1882 ctrl.update(0.016);
1883 }
1884 assert!(!ctrl.in_transition);
1885 assert!((ctrl.blend_factor - 1.0).abs() < 1e-6);
1886 }
1887
1888 #[test]
1889 fn player_hurt_transition_is_fast() {
1890 let (dur, curve) = transition_params(PlayerAnimState::Walk, PlayerAnimState::Hurt);
1891 assert!((dur - 0.05).abs() < 1e-6, "Any→Hurt should be 0.05s, got {dur}");
1892 assert_eq!(curve, BlendCurve::Linear);
1893 }
1894
1895 #[test]
1896 fn player_auto_return_to_idle_after_attack() {
1897 let mut ctrl = PlayerAnimController::new(4);
1898 ctrl.transition_to(PlayerAnimState::Attack);
1899 for _ in 0..50 {
1901 ctrl.update(0.016);
1902 }
1903 assert_eq!(ctrl.current_state, PlayerAnimState::Idle);
1905 }
1906
1907 #[test]
1910 fn blend_tree_1d_single_entry() {
1911 let mut tree = BlendTree1D::new();
1912 tree.add_entry(0.5, AnimPose::identity(3));
1913 tree.parameter = 0.0;
1914 let pose = tree.evaluate();
1915 assert_eq!(pose.transforms.len(), 3);
1916 }
1917
1918 #[test]
1919 fn blend_tree_1d_interpolation() {
1920 let pose_a = AnimPose {
1921 transforms: vec![GlyphTransform {
1922 position_offset: Vec3::ZERO,
1923 scale: 1.0,
1924 rotation_z: 0.0,
1925 emission: 0.0,
1926 }],
1927 };
1928 let pose_b = AnimPose {
1929 transforms: vec![GlyphTransform {
1930 position_offset: Vec3::new(10.0, 0.0, 0.0),
1931 scale: 2.0,
1932 rotation_z: 0.0,
1933 emission: 1.0,
1934 }],
1935 };
1936
1937 let mut tree = BlendTree1D::new();
1938 tree.add_entry(0.0, pose_a);
1939 tree.add_entry(1.0, pose_b);
1940
1941 tree.parameter = 0.5;
1942 let result = tree.evaluate();
1943 assert!((result.transforms[0].position_offset.x - 5.0).abs() < 1e-6);
1944 assert!((result.transforms[0].scale - 1.5).abs() < 1e-6);
1945 }
1946
1947 #[test]
1948 fn blend_tree_1d_clamp_below() {
1949 let pose = AnimPose::identity(2);
1950 let mut tree = BlendTree1D::new();
1951 tree.add_entry(0.3, pose.clone());
1952 tree.add_entry(0.7, AnimPose::identity(2));
1953
1954 tree.parameter = 0.0; let result = tree.evaluate();
1956 assert_eq!(result.transforms.len(), 2);
1957 }
1958
1959 #[test]
1960 fn idle_walk_blend_at_zero_is_idle() {
1961 let mut blend = IdleWalkBlend::new(4);
1962 let pose = blend.evaluate(0.0);
1963 assert_eq!(pose.transforms.len(), 4);
1964 }
1965
1966 #[test]
1967 fn attack_power_blend_range() {
1968 let mut blend = AttackPowerBlend::new(6);
1969 let light = blend.evaluate(0.0);
1970 let heavy = blend.evaluate(1.0);
1971 let light_emit: f32 = light.transforms.iter().map(|t| t.emission).sum();
1973 let heavy_emit: f32 = heavy.transforms.iter().map(|t| t.emission).sum();
1974 assert!(heavy_emit >= light_emit, "heavy attack should have >= emission");
1975 }
1976
1977 #[test]
1978 fn damage_reaction_blend_scales() {
1979 let mut blend = DamageReactionBlend::new(4);
1980 let small = blend.evaluate(0.0);
1981 let large = blend.evaluate(1.0);
1982 let small_displacement: f32 = small.transforms.iter().map(|t| t.position_offset.length()).sum();
1983 let large_displacement: f32 = large.transforms.iter().map(|t| t.position_offset.length()).sum();
1984 assert!(large_displacement > small_displacement, "large damage should have more displacement");
1985 }
1986
1987 #[test]
1988 fn cast_intensity_blend_emission() {
1989 let mut blend = CastIntensityBlend::new(9);
1990 let small = blend.evaluate(0.0);
1991 let large = blend.evaluate(1.0);
1992 let small_emit: f32 = small.transforms.iter().map(|t| t.emission).sum();
1993 let large_emit: f32 = large.transforms.iter().map(|t| t.emission).sum();
1994 assert!(large_emit > small_emit, "large cast should have more emission");
1995 }
1996
1997 #[test]
2000 fn ik_chain_construction() {
2001 let chain = IKChain::new(Vec3::ZERO, Vec3::new(3.0, 0.0, 0.0), Vec3::new(5.0, 0.0, 0.0));
2002 assert!((chain.lengths[0] - 3.0).abs() < 1e-6);
2003 assert!((chain.lengths[1] - 2.0).abs() < 1e-6);
2004 assert!((chain.total_length() - 5.0).abs() < 1e-6);
2005 }
2006
2007 #[test]
2008 fn ik_solve_reaches_target_within_range() {
2009 let mut chain = IKChain::new(
2010 Vec3::ZERO,
2011 Vec3::new(3.0, 0.0, 0.0),
2012 Vec3::new(5.0, 0.0, 0.0),
2013 );
2014 let target = Vec3::new(4.0, 1.0, 0.0);
2015 solve_two_bone(&mut chain, target);
2016
2017 let end_dist = (chain.end_pos - target).length();
2018 assert!(end_dist < 0.5, "IK end should be near target, dist = {end_dist}");
2019
2020 let bone0_len = (chain.mid_pos - chain.root_pos).length();
2022 let bone1_len = (chain.end_pos - chain.mid_pos).length();
2023 assert!((bone0_len - 3.0).abs() < 0.1, "bone0 length should be ~3, got {bone0_len}");
2024 assert!((bone1_len - 2.0).abs() < 0.1, "bone1 length should be ~2, got {bone1_len}");
2025 }
2026
2027 #[test]
2028 fn ik_solve_unreachable_target_extends() {
2029 let mut chain = IKChain::new(
2030 Vec3::ZERO,
2031 Vec3::new(3.0, 0.0, 0.0),
2032 Vec3::new(5.0, 0.0, 0.0),
2033 );
2034 let target = Vec3::new(100.0, 0.0, 0.0);
2036 solve_two_bone(&mut chain, target);
2037
2038 assert!(chain.end_pos.x > 3.0, "should extend toward target");
2040 }
2041
2042 #[test]
2043 fn ik_limb_no_target_returns_zero_offsets() {
2044 let mut limb = IKLimb::new(
2045 "test",
2046 [0, 1, 2],
2047 [Vec3::ZERO, Vec3::new(2.0, 0.0, 0.0), Vec3::new(4.0, 0.0, 0.0)],
2048 );
2049 limb.target = IKTarget::None;
2050 let offsets = limb.solve(Vec3::ZERO, &HashMap::new());
2051 for o in &offsets {
2052 assert!(o.length() < 1e-6, "None target should give zero offsets");
2053 }
2054 }
2055
2056 #[test]
2057 fn ik_limb_position_target() {
2058 let mut limb = IKLimb::new(
2059 "arm",
2060 [0, 1, 2],
2061 [Vec3::ZERO, Vec3::new(2.0, 0.0, 0.0), Vec3::new(4.0, 0.0, 0.0)],
2062 );
2063 limb.target = IKTarget::Position(Vec3::new(3.0, 1.0, 0.0));
2064 let offsets = limb.solve(Vec3::ZERO, &HashMap::new());
2065 assert!(offsets[2].length() > 0.1, "IK should produce non-zero offset for position target");
2067 }
2068
2069 #[test]
2072 fn enemy_idle_glyph_count() {
2073 let ctrl = EnemyAnimController::new(6);
2074 let pose = ctrl.enemy_idle(0.5, 6);
2075 assert_eq!(pose.transforms.len(), 6);
2076 }
2077
2078 #[test]
2079 fn enemy_transition_to_attack() {
2080 let mut ctrl = EnemyAnimController::new(6);
2081 assert!(ctrl.transition_to(EnemyAnimState::Approach));
2082 assert_eq!(ctrl.current_state, EnemyAnimState::Approach);
2083 }
2084
2085 #[test]
2086 fn enemy_die_fades_out() {
2087 let ctrl = EnemyAnimController::new(4);
2088 let early = ctrl.enemy_die(0.1, 4);
2089 let late = ctrl.enemy_die(1.4, 4);
2090 let early_scale: f32 = early.transforms.iter().map(|t| t.scale).sum();
2091 let late_scale: f32 = late.transforms.iter().map(|t| t.scale).sum();
2092 assert!(late_scale < early_scale, "dying entity should fade out over time");
2093 }
2094
2095 #[test]
2096 fn enemy_hurt_transition_is_fast() {
2097 let (dur, _) = enemy_transition_params(EnemyAnimState::Idle, EnemyAnimState::Hurt);
2098 assert!((dur - 0.05).abs() < 1e-6);
2099 }
2100
2101 #[test]
2104 fn boss_hydra_splitting_spreads_glyphs() {
2105 let mut boss = BossAnimController::new_hydra(8);
2106 boss.boss_state = BossState::Hydra(HydraAnimState::Splitting);
2107 let pose1 = boss.update(0.01);
2108 for _ in 0..50 {
2110 boss.update(0.016);
2111 }
2112 let pose2 = boss.update(0.016);
2113 let spread1: f32 = pose1.transforms.iter().map(|t| t.position_offset.x.abs()).sum();
2115 let spread2: f32 = pose2.transforms.iter().map(|t| t.position_offset.x.abs()).sum();
2116 assert!(spread2 > spread1, "splitting should spread glyphs apart");
2117 }
2118
2119 #[test]
2120 fn boss_committee_voting_advances() {
2121 let mut boss = BossAnimController::new_committee(10, 5);
2122 boss.boss_state = BossState::Committee(CommitteeAnimState::Voting);
2123 boss.voting_index = 0;
2124 boss.vote_timer = 0.0;
2125
2126 for _ in 0..35 {
2128 boss.update(0.016);
2129 }
2130 assert!(boss.voting_index > 0 || matches!(boss.boss_state, BossState::Committee(CommitteeAnimState::Verdict)),
2131 "voting should advance index or reach verdict");
2132 }
2133
2134 #[test]
2135 fn boss_algorithm_phase_transition_chaos() {
2136 let mut boss = BossAnimController::new_algorithm(8);
2137 boss.boss_state = BossState::Algorithm(AlgorithmAnimState::PhaseTransition);
2138 boss.phase_transition_progress = 0.0;
2139
2140 for _ in 0..60 {
2142 boss.update(0.016);
2143 }
2144 let pose = boss.update(0.016);
2145 let total_emission: f32 = pose.transforms.iter().map(|t| t.emission).sum();
2146 assert!(total_emission > 0.0, "phase transition should produce emission chaos");
2147 }
2148
2149 #[test]
2152 fn manager_register_and_update() {
2153 let mut mgr = AnimationManager::new();
2154 let player_id = EntityId(1);
2155 let enemy_id = EntityId(2);
2156 mgr.register_player(player_id, 9);
2157 mgr.register_enemy(enemy_id, 6);
2158 mgr.update(0.016);
2159
2160 let player_transforms = mgr.get_glyph_transforms(player_id);
2161 assert_eq!(player_transforms.len(), 9);
2162 let enemy_transforms = mgr.get_glyph_transforms(enemy_id);
2163 assert_eq!(enemy_transforms.len(), 6);
2164 }
2165
2166 #[test]
2167 fn manager_trigger_state() {
2168 let mut mgr = AnimationManager::new();
2169 let id = EntityId(1);
2170 mgr.register_player(id, 4);
2171 assert!(mgr.trigger_player_state(id, PlayerAnimState::Walk));
2172 assert!(!mgr.trigger_player_state(EntityId(999), PlayerAnimState::Walk)); }
2174
2175 #[test]
2176 fn manager_set_ik_target() {
2177 let mut mgr = AnimationManager::new();
2178 let id = EntityId(1);
2179 mgr.register_player(id, 6);
2180 let limb = weapon_arm_ik([0, 1, 2], [Vec3::ZERO, Vec3::X * 2.0, Vec3::X * 4.0]);
2181 mgr.register_ik_limb(id, limb);
2182 mgr.set_ik_target(id, "weapon_arm", IKTarget::Position(Vec3::new(3.0, 2.0, 0.0)));
2183 mgr.update(0.016);
2184 let transforms = mgr.get_glyph_transforms(id);
2185 assert_eq!(transforms.len(), 6);
2186 }
2187
2188 #[test]
2189 fn manager_remove_entity_cleans_up() {
2190 let mut mgr = AnimationManager::new();
2191 let id = EntityId(42);
2192 mgr.register_player(id, 4);
2193 mgr.update(0.016);
2194 assert!(!mgr.get_glyph_transforms(id).is_empty());
2195
2196 mgr.remove_entity(id);
2197 assert!(mgr.get_glyph_transforms(id).is_empty());
2198 assert!(!mgr.player_controllers.contains_key(&id));
2199 }
2200
2201 #[test]
2202 fn manager_boss_registration() {
2203 let mut mgr = AnimationManager::new();
2204 let id = EntityId(100);
2205 let boss = BossAnimController::new_hydra(12);
2206 mgr.register_boss(id, boss);
2207 mgr.update(0.016);
2208 let transforms = mgr.get_glyph_transforms(id);
2209 assert_eq!(transforms.len(), 12);
2210 }
2211
2212 #[test]
2215 fn transition_table_not_empty() {
2216 let table = build_transition_table();
2217 assert!(table.len() >= 10, "transition table should have at least 10 entries");
2218 }
2219
2220 #[test]
2221 fn transition_table_durations_positive() {
2222 let table = build_transition_table();
2223 for entry in &table {
2224 assert!(entry.duration > 0.0, "transition duration must be positive");
2225 }
2226 }
2227
2228 #[test]
2231 fn fixed_duration_states() {
2232 assert!(PlayerAnimState::Attack.fixed_duration().is_some());
2233 assert!(PlayerAnimState::HeavyAttack.fixed_duration().is_some());
2234 assert!(PlayerAnimState::Hurt.fixed_duration().is_some());
2235 assert!(PlayerAnimState::Idle.fixed_duration().is_none());
2236 assert!(PlayerAnimState::Walk.fixed_duration().is_none());
2237 }
2238
2239 #[test]
2240 fn pose_blend_preserves_length() {
2241 let a = AnimPose::identity(5);
2242 let b = AnimPose::identity(5);
2243 let blended = a.blend(&b, 0.5);
2244 assert_eq!(blended.transforms.len(), 5);
2245 }
2246
2247 #[test]
2248 fn ik_factory_functions() {
2249 let rest = [Vec3::ZERO, Vec3::X * 2.0, Vec3::X * 4.0];
2250 let indices = [0, 1, 2];
2251 let w = weapon_arm_ik(indices, rest);
2252 assert_eq!(w.name, "weapon_arm");
2253 assert!((w.weight - 0.8).abs() < 1e-6);
2254
2255 let l = look_at_ik(indices, rest);
2256 assert_eq!(l.name, "look_at");
2257
2258 let s = staff_aim_ik(indices, rest);
2259 assert_eq!(s.name, "staff_aim");
2260
2261 let sh = shield_ik(indices, rest);
2262 assert_eq!(sh.name, "shield");
2263 assert!((sh.weight - 1.0).abs() < 1e-6);
2264 }
2265}