1use glam::{Vec2, Vec3, Vec4};
28use std::collections::HashMap;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum TransitionState {
35 None,
37 FadingOut,
39 Hold,
41 FadingIn,
43 Completed,
45}
46
47#[derive(Debug, Clone)]
51pub enum TransitionType {
52 FadeBlack {
54 out_time: f32,
55 hold_time: f32,
56 in_time: f32,
57 },
58 Dissolve {
60 duration: f32,
61 },
62 SlideLeft {
64 duration: f32,
65 },
66 SlideRight {
68 duration: f32,
69 },
70 ZoomIn {
72 duration: f32,
73 },
74 ChaosWipe {
76 duration: f32,
77 },
78 Cut,
80}
81
82impl TransitionType {
83 pub fn total_duration(&self) -> f32 {
85 match self {
86 Self::FadeBlack { out_time, hold_time, in_time } => out_time + hold_time + in_time,
87 Self::Dissolve { duration } => *duration,
88 Self::SlideLeft { duration } => *duration,
89 Self::SlideRight { duration } => *duration,
90 Self::ZoomIn { duration } => *duration,
91 Self::ChaosWipe { duration } => *duration,
92 Self::Cut => 0.0,
93 }
94 }
95
96 pub fn swap_point(&self) -> f32 {
98 match self {
99 Self::FadeBlack { out_time, hold_time, in_time } => {
100 let total = out_time + hold_time + in_time;
101 if total < 1e-6 { return 0.5; }
102 (out_time + hold_time * 0.5) / total
103 }
104 Self::Dissolve { .. } => 0.5,
105 Self::SlideLeft { .. } => 0.5,
106 Self::SlideRight { .. } => 0.5,
107 Self::ZoomIn { .. } => 0.5,
108 Self::ChaosWipe { .. } => 0.5,
109 Self::Cut => 0.0,
110 }
111 }
112}
113
114#[derive(Debug, Clone)]
118pub struct TransitionOverlay {
119 pub color: Vec4,
121 pub coverage: f32,
123 pub dissolve_threshold: f32,
125 pub slide_offset: f32,
127 pub zoom_scale: f32,
129 pub wipe_front: f32,
131 pub chaos_particle_count: u32,
133 pub effect: TransitionEffect,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum TransitionEffect {
140 None,
141 FadeBlack,
142 Dissolve,
143 SlideLeft,
144 SlideRight,
145 ZoomIn,
146 ChaosWipe,
147}
148
149impl Default for TransitionOverlay {
150 fn default() -> Self {
151 Self {
152 color: Vec4::new(0.0, 0.0, 0.0, 0.0),
153 coverage: 0.0,
154 dissolve_threshold: 0.0,
155 slide_offset: 0.0,
156 zoom_scale: 1.0,
157 wipe_front: 0.0,
158 chaos_particle_count: 0,
159 effect: TransitionEffect::None,
160 }
161 }
162}
163
164#[derive(Debug, Clone)]
171pub struct Screenshot {
172 pub width: u32,
173 pub height: u32,
174 pub captured_at: f32, pub texture_id: Option<u32>,
177}
178
179impl Screenshot {
180 pub fn placeholder(w: u32, h: u32, time: f32) -> Self {
181 Self { width: w, height: h, captured_at: time, texture_id: None }
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum TransitionEasing {
190 Linear,
191 EaseIn,
192 EaseOut,
193 EaseInOut,
194 SmoothStep,
195}
196
197impl TransitionEasing {
198 pub fn apply(&self, t: f32) -> f32 {
199 let t = t.clamp(0.0, 1.0);
200 match self {
201 Self::Linear => t,
202 Self::EaseIn => t * t,
203 Self::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
204 Self::EaseInOut => {
205 if t < 0.5 {
206 2.0 * t * t
207 } else {
208 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
209 }
210 }
211 Self::SmoothStep => t * t * (3.0 - 2.0 * t),
212 }
213 }
214}
215
216pub struct TransitionManager {
228 state: TransitionState,
229 progress: f32,
230 elapsed: f32,
231 transition_type: TransitionType,
232 easing: TransitionEasing,
233 from_screen: Option<Screenshot>,
234 swap_pending: bool,
235 swap_acknowledged: bool,
236 pub tag: String,
238 pub stats: TransitionStats,
240}
241
242#[derive(Debug, Clone, Default)]
244pub struct TransitionStats {
245 pub state: &'static str,
246 pub progress: f32,
247 pub elapsed: f32,
248 pub total_duration: f32,
249}
250
251impl TransitionManager {
252 pub fn new() -> Self {
253 Self {
254 state: TransitionState::None,
255 progress: 0.0,
256 elapsed: 0.0,
257 transition_type: TransitionType::Cut,
258 easing: TransitionEasing::SmoothStep,
259 from_screen: None,
260 swap_pending: false,
261 swap_acknowledged: false,
262 tag: String::new(),
263 stats: TransitionStats::default(),
264 }
265 }
266
267 pub fn start(&mut self, transition: TransitionType) {
269 self.transition_type = transition;
270 self.state = if self.transition_type.total_duration() < 1e-6 {
271 self.swap_pending = true;
273 TransitionState::Hold
274 } else {
275 TransitionState::FadingOut
276 };
277 self.progress = 0.0;
278 self.elapsed = 0.0;
279 self.swap_pending = false;
280 self.swap_acknowledged = false;
281 }
282
283 pub fn start_tagged(&mut self, transition: TransitionType, tag: impl Into<String>) {
285 self.tag = tag.into();
286 self.start(transition);
287 }
288
289 pub fn start_with_easing(&mut self, transition: TransitionType, easing: TransitionEasing) {
291 self.easing = easing;
292 self.start(transition);
293 }
294
295 pub fn capture_screen(&mut self, width: u32, height: u32, time: f32) {
297 self.from_screen = Some(Screenshot::placeholder(width, height, time));
298 }
299
300 pub fn tick(&mut self, dt: f32) {
302 if self.state == TransitionState::None || self.state == TransitionState::Completed {
303 return;
304 }
305
306 self.elapsed += dt;
307 let total = self.transition_type.total_duration();
308
309 if total < 1e-6 {
310 self.progress = 1.0;
312 self.state = TransitionState::Completed;
313 self.swap_pending = true;
314 self.update_stats();
315 return;
316 }
317
318 self.progress = (self.elapsed / total).clamp(0.0, 1.0);
319 let swap_point = self.transition_type.swap_point();
320
321 match &self.transition_type {
323 TransitionType::FadeBlack { out_time, hold_time, in_time } => {
324 let total = out_time + hold_time + in_time;
325 if self.elapsed < *out_time {
326 self.state = TransitionState::FadingOut;
327 } else if self.elapsed < out_time + hold_time {
328 self.state = TransitionState::Hold;
329 if !self.swap_pending && !self.swap_acknowledged {
330 self.swap_pending = true;
331 }
332 } else if self.elapsed < total {
333 self.state = TransitionState::FadingIn;
334 } else {
335 self.state = TransitionState::Completed;
336 }
337 }
338 _ => {
339 if self.progress < swap_point {
341 self.state = TransitionState::FadingOut;
342 } else if !self.swap_acknowledged {
343 self.state = TransitionState::Hold;
344 if !self.swap_pending {
345 self.swap_pending = true;
346 }
347 } else if self.progress < 1.0 {
348 self.state = TransitionState::FadingIn;
349 } else {
350 self.state = TransitionState::Completed;
351 }
352 }
353 }
354
355 if self.elapsed >= total {
356 self.state = TransitionState::Completed;
357 }
358
359 self.update_stats();
360 }
361
362 fn update_stats(&self) {
363 }
365
366 pub fn should_swap_state(&self) -> bool {
368 self.swap_pending && !self.swap_acknowledged
369 }
370
371 pub fn acknowledge_swap(&mut self) {
373 self.swap_acknowledged = true;
374 self.swap_pending = false;
375 }
376
377 pub fn is_done(&self) -> bool {
379 self.state == TransitionState::None || self.state == TransitionState::Completed
380 }
381
382 pub fn is_active(&self) -> bool {
384 !self.is_done()
385 }
386
387 pub fn state(&self) -> TransitionState { self.state }
389
390 pub fn progress(&self) -> f32 { self.progress }
392
393 pub fn clear(&mut self) {
395 self.state = TransitionState::None;
396 self.progress = 0.0;
397 self.elapsed = 0.0;
398 self.swap_pending = false;
399 self.swap_acknowledged = false;
400 self.from_screen = None;
401 }
402
403 pub fn render_overlay(&self, _screen_w: f32, _screen_h: f32) -> TransitionOverlay {
409 if self.state == TransitionState::None || self.state == TransitionState::Completed {
410 return TransitionOverlay::default();
411 }
412
413 match &self.transition_type {
414 TransitionType::FadeBlack { out_time, hold_time, in_time } => {
415 self.render_fade_black(*out_time, *hold_time, *in_time)
416 }
417 TransitionType::Dissolve { duration } => {
418 self.render_dissolve(*duration)
419 }
420 TransitionType::SlideLeft { duration } => {
421 self.render_slide(*duration, -1.0)
422 }
423 TransitionType::SlideRight { duration } => {
424 self.render_slide(*duration, 1.0)
425 }
426 TransitionType::ZoomIn { duration } => {
427 self.render_zoom(*duration)
428 }
429 TransitionType::ChaosWipe { duration } => {
430 self.render_chaos_wipe(*duration)
431 }
432 TransitionType::Cut => TransitionOverlay::default(),
433 }
434 }
435
436 fn render_fade_black(&self, out_time: f32, hold_time: f32, in_time: f32) -> TransitionOverlay {
439 let alpha = if self.elapsed < out_time {
440 let t = if out_time > 1e-6 { self.elapsed / out_time } else { 1.0 };
442 self.easing.apply(t)
443 } else if self.elapsed < out_time + hold_time {
444 1.0
446 } else {
447 let fade_in_elapsed = self.elapsed - out_time - hold_time;
449 let t = if in_time > 1e-6 { fade_in_elapsed / in_time } else { 1.0 };
450 1.0 - self.easing.apply(t)
451 };
452
453 TransitionOverlay {
454 color: Vec4::new(0.0, 0.0, 0.0, alpha),
455 coverage: alpha,
456 effect: TransitionEffect::FadeBlack,
457 ..Default::default()
458 }
459 }
460
461 fn render_dissolve(&self, duration: f32) -> TransitionOverlay {
464 let t = if duration > 1e-6 { self.elapsed / duration } else { 1.0 };
465 let threshold = self.easing.apply(t.clamp(0.0, 1.0));
466
467 TransitionOverlay {
468 dissolve_threshold: threshold,
469 coverage: threshold,
470 effect: TransitionEffect::Dissolve,
471 ..Default::default()
472 }
473 }
474
475 fn render_slide(&self, duration: f32, direction: f32) -> TransitionOverlay {
478 let t = if duration > 1e-6 { self.elapsed / duration } else { 1.0 };
479 let eased = self.easing.apply(t.clamp(0.0, 1.0));
480 let offset = eased * direction;
481
482 let effect = if direction < 0.0 {
483 TransitionEffect::SlideLeft
484 } else {
485 TransitionEffect::SlideRight
486 };
487
488 TransitionOverlay {
489 slide_offset: offset,
490 coverage: eased.min(1.0 - eased) * 2.0, effect,
492 ..Default::default()
493 }
494 }
495
496 fn render_zoom(&self, duration: f32) -> TransitionOverlay {
499 let t = if duration > 1e-6 { self.elapsed / duration } else { 1.0 };
500 let eased = self.easing.apply(t.clamp(0.0, 1.0));
501
502 let zoom = if eased < 0.5 {
504 1.0 + eased * 4.0 } else {
506 3.0 - (eased - 0.5) * 4.0 };
508
509 let flash_alpha = if eased > 0.4 && eased < 0.6 {
511 let flash_t = ((eased - 0.4) / 0.2).clamp(0.0, 1.0);
512 if flash_t < 0.5 {
513 flash_t * 2.0
514 } else {
515 (1.0 - flash_t) * 2.0
516 }
517 } else {
518 0.0
519 };
520
521 TransitionOverlay {
522 color: Vec4::new(1.0, 1.0, 1.0, flash_alpha),
523 zoom_scale: zoom.max(0.01),
524 coverage: flash_alpha,
525 effect: TransitionEffect::ZoomIn,
526 ..Default::default()
527 }
528 }
529
530 fn render_chaos_wipe(&self, duration: f32) -> TransitionOverlay {
533 let t = if duration > 1e-6 { self.elapsed / duration } else { 1.0 };
534 let eased = self.easing.apply(t.clamp(0.0, 1.0));
535
536 let wipe_front = eased;
539
540 let intensity = if eased < 0.5 { eased * 2.0 } else { (1.0 - eased) * 2.0 };
542 let particle_count = (intensity * 200.0) as u32;
543
544 TransitionOverlay {
545 wipe_front,
546 chaos_particle_count: particle_count,
547 coverage: eased,
548 color: Vec4::new(0.0, 0.0, 0.0, 0.0), effect: TransitionEffect::ChaosWipe,
550 ..Default::default()
551 }
552 }
553}
554
555impl Default for TransitionManager {
556 fn default() -> Self { Self::new() }
557}
558
559pub struct GameTransitions;
563
564impl GameTransitions {
565 pub fn title_to_character_creation() -> TransitionType {
567 TransitionType::FadeBlack {
568 out_time: 0.2,
569 hold_time: 0.05,
570 in_time: 0.2,
571 }
572 }
573
574 pub fn character_creation_to_floor_nav() -> TransitionType {
576 TransitionType::FadeBlack {
577 out_time: 0.15,
578 hold_time: 0.05,
579 in_time: 0.2,
580 }
581 }
582
583 pub fn floor_nav_to_combat() -> TransitionType {
585 TransitionType::ChaosWipe {
586 duration: 0.3,
587 }
588 }
589
590 pub fn combat_to_floor_nav() -> TransitionType {
592 TransitionType::FadeBlack {
593 out_time: 0.2,
594 hold_time: 0.05,
595 in_time: 0.2,
596 }
597 }
598
599 pub fn to_death() -> TransitionType {
601 TransitionType::FadeBlack {
602 out_time: 0.5,
603 hold_time: 0.5,
604 in_time: 0.3,
605 }
606 }
607
608 pub fn to_boss() -> TransitionType {
610 TransitionType::ZoomIn {
611 duration: 0.3,
612 }
613 }
614
615 pub fn floor_transition() -> TransitionType {
617 TransitionType::Dissolve {
618 duration: 0.4,
619 }
620 }
621
622 pub fn menu_transition() -> TransitionType {
624 TransitionType::FadeBlack {
625 out_time: 0.1,
626 hold_time: 0.02,
627 in_time: 0.1,
628 }
629 }
630
631 pub fn pause_overlay() -> TransitionType {
633 TransitionType::FadeBlack {
634 out_time: 0.08,
635 hold_time: 0.0,
636 in_time: 0.08,
637 }
638 }
639
640 pub fn to_victory() -> TransitionType {
642 TransitionType::FadeBlack {
643 out_time: 0.3,
644 hold_time: 0.2,
645 in_time: 0.5,
646 }
647 }
648
649 pub fn panel_slide_left() -> TransitionType {
651 TransitionType::SlideLeft { duration: 0.25 }
652 }
653
654 pub fn panel_slide_right() -> TransitionType {
656 TransitionType::SlideRight { duration: 0.25 }
657 }
658}
659
660#[derive(Debug, Clone)]
664pub struct TransitionRequest {
665 pub transition: TransitionType,
666 pub tag: String,
667 pub easing: TransitionEasing,
668 pub delay: f32,
669}
670
671impl TransitionRequest {
672 pub fn new(transition: TransitionType) -> Self {
673 Self {
674 transition,
675 tag: String::new(),
676 easing: TransitionEasing::SmoothStep,
677 delay: 0.0,
678 }
679 }
680
681 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
682 self.tag = tag.into();
683 self
684 }
685
686 pub fn with_easing(mut self, easing: TransitionEasing) -> Self {
687 self.easing = easing;
688 self
689 }
690
691 pub fn with_delay(mut self, delay: f32) -> Self {
692 self.delay = delay;
693 self
694 }
695}
696
697pub struct TransitionQueue {
700 pub manager: TransitionManager,
701 pending: Vec<TransitionRequest>,
702 delay_timer: f32,
703}
704
705impl TransitionQueue {
706 pub fn new() -> Self {
707 Self {
708 manager: TransitionManager::new(),
709 pending: Vec::new(),
710 delay_timer: 0.0,
711 }
712 }
713
714 pub fn enqueue(&mut self, request: TransitionRequest) {
716 self.pending.push(request);
717 }
718
719 pub fn enqueue_simple(&mut self, transition: TransitionType) {
721 self.pending.push(TransitionRequest::new(transition));
722 }
723
724 pub fn tick(&mut self, dt: f32) {
726 self.manager.tick(dt);
728
729 if self.manager.is_done() && !self.pending.is_empty() {
731 if self.delay_timer > 0.0 {
733 self.delay_timer -= dt;
734 return;
735 }
736
737 let request = self.pending.remove(0);
738 if request.delay > 0.0 && self.delay_timer <= 0.0 {
739 self.delay_timer = request.delay;
740 self.pending.insert(0, TransitionRequest {
741 delay: 0.0,
742 ..request
743 });
744 return;
745 }
746
747 self.manager.easing = request.easing;
748 self.manager.start_tagged(request.transition, request.tag);
749 }
750 }
751
752 pub fn should_swap_state(&self) -> bool {
754 self.manager.should_swap_state()
755 }
756
757 pub fn acknowledge_swap(&mut self) {
759 self.manager.acknowledge_swap();
760 }
761
762 pub fn render_overlay(&self, w: f32, h: f32) -> TransitionOverlay {
764 self.manager.render_overlay(w, h)
765 }
766
767 pub fn is_busy(&self) -> bool {
769 self.manager.is_active() || !self.pending.is_empty()
770 }
771
772 pub fn clear(&mut self) {
774 self.manager.clear();
775 self.pending.clear();
776 self.delay_timer = 0.0;
777 }
778
779 pub fn pending_count(&self) -> usize {
781 self.pending.len()
782 }
783}
784
785impl Default for TransitionQueue {
786 fn default() -> Self { Self::new() }
787}
788
789#[cfg(test)]
792mod tests {
793 use super::*;
794
795 #[test]
796 fn fade_black_phases() {
797 let mut tm = TransitionManager::new();
798 tm.start(TransitionType::FadeBlack {
799 out_time: 0.2,
800 hold_time: 0.1,
801 in_time: 0.2,
802 });
803
804 assert_eq!(tm.state(), TransitionState::FadingOut);
805
806 tm.tick(0.15);
808 assert_eq!(tm.state(), TransitionState::FadingOut);
809
810 tm.tick(0.1);
812 assert_eq!(tm.state(), TransitionState::Hold);
813 assert!(tm.should_swap_state());
814
815 tm.acknowledge_swap();
816 assert!(!tm.should_swap_state());
817
818 tm.tick(0.1);
820 assert_eq!(tm.state(), TransitionState::FadingIn);
821
822 tm.tick(0.2);
824 assert_eq!(tm.state(), TransitionState::Completed);
825 assert!(tm.is_done());
826 }
827
828 #[test]
829 fn dissolve_transition() {
830 let mut tm = TransitionManager::new();
831 tm.start(TransitionType::Dissolve { duration: 1.0 });
832
833 tm.tick(0.25);
834 assert!(tm.is_active());
835 let overlay = tm.render_overlay(800.0, 600.0);
836 assert_eq!(overlay.effect, TransitionEffect::Dissolve);
837 assert!(overlay.dissolve_threshold > 0.0);
838
839 tm.tick(0.75);
840 assert!(tm.is_done());
841 }
842
843 #[test]
844 fn chaos_wipe_particles() {
845 let mut tm = TransitionManager::new();
846 tm.start(TransitionType::ChaosWipe { duration: 0.3 });
847
848 tm.tick(0.15);
849 let overlay = tm.render_overlay(800.0, 600.0);
850 assert_eq!(overlay.effect, TransitionEffect::ChaosWipe);
851 assert!(overlay.chaos_particle_count > 0);
852 assert!(overlay.wipe_front > 0.0);
853 }
854
855 #[test]
856 fn zoom_in_flash() {
857 let mut tm = TransitionManager::new();
858 tm.start(TransitionType::ZoomIn { duration: 1.0 });
859
860 tm.tick(0.5);
862 let overlay = tm.render_overlay(800.0, 600.0);
863 assert_eq!(overlay.effect, TransitionEffect::ZoomIn);
864 assert!(overlay.zoom_scale > 1.0);
865 }
866
867 #[test]
868 fn instant_cut() {
869 let mut tm = TransitionManager::new();
870 tm.start(TransitionType::Cut);
871 tm.tick(0.0);
872 assert!(tm.is_done());
873 }
874
875 #[test]
876 fn slide_left() {
877 let mut tm = TransitionManager::new();
878 tm.start(TransitionType::SlideLeft { duration: 0.5 });
879 tm.tick(0.25);
880 let overlay = tm.render_overlay(800.0, 600.0);
881 assert_eq!(overlay.effect, TransitionEffect::SlideLeft);
882 assert!(overlay.slide_offset < 0.0);
883 }
884
885 #[test]
886 fn game_preset_durations() {
887 assert!(GameTransitions::title_to_character_creation().total_duration() > 0.0);
888 assert!(GameTransitions::to_death().total_duration() > 1.0);
889 assert!(GameTransitions::to_boss().total_duration() > 0.0);
890 assert!(GameTransitions::floor_transition().total_duration() > 0.0);
891 }
892
893 #[test]
894 fn easing_bounds() {
895 for easing in &[
896 TransitionEasing::Linear,
897 TransitionEasing::EaseIn,
898 TransitionEasing::EaseOut,
899 TransitionEasing::EaseInOut,
900 TransitionEasing::SmoothStep,
901 ] {
902 assert!((easing.apply(0.0) - 0.0).abs() < 1e-6, "{:?} at 0", easing);
903 assert!((easing.apply(1.0) - 1.0).abs() < 1e-6, "{:?} at 1", easing);
904 let mid = easing.apply(0.5);
906 assert!(mid >= 0.0 && mid <= 1.0, "{:?} mid={}", easing, mid);
907 }
908 }
909
910 #[test]
911 fn transition_queue_sequences() {
912 let mut queue = TransitionQueue::new();
913 queue.enqueue_simple(TransitionType::FadeBlack {
914 out_time: 0.1, hold_time: 0.0, in_time: 0.1,
915 });
916 queue.enqueue_simple(TransitionType::Dissolve { duration: 0.2 });
917
918 queue.tick(0.01);
920 assert!(queue.is_busy());
921 assert_eq!(queue.pending_count(), 1);
922
923 queue.tick(0.05);
925 queue.acknowledge_swap();
926 queue.tick(0.15);
927
928 queue.tick(0.01);
930 assert!(queue.is_busy());
931 assert_eq!(queue.pending_count(), 0);
932 }
933
934 #[test]
935 fn overlay_default_when_inactive() {
936 let tm = TransitionManager::new();
937 let overlay = tm.render_overlay(800.0, 600.0);
938 assert_eq!(overlay.effect, TransitionEffect::None);
939 assert_eq!(overlay.coverage, 0.0);
940 }
941}