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