blizz_ui/components/mascot/
component.rs1use std::io::Write;
2
3use rand::Rng;
4
5use super::bobbing;
6use super::frames::{MascotFrames, queue_frame, queue_frame_owned};
7use super::fx;
8use crate::layout::{centered_block_origin, top_two_thirds};
9use crate::{Component, Renderer};
10
11#[derive(Clone, Debug)]
16pub struct MascotComponent {
17 pub frames: MascotFrames,
18 pub entrance: f64,
19 pub dissolve: f64,
20 pub floating: bool,
21 distance_map: fx::DistanceMap,
22 entrance_tick: usize,
23 dissolve_tick: usize,
24 dissolve_state: Option<fx::DissolveState>,
25}
26
27impl MascotComponent {
28 pub fn blizz() -> Self {
29 let frames = MascotFrames::blizz();
30 let distance_map = fx::build_distance_map(frames.lines());
31 Self {
32 frames,
33 entrance: 0.0,
34 dissolve: 0.0,
35 floating: false,
36 distance_map,
37 entrance_tick: 0,
38 dissolve_tick: 0,
39 dissolve_state: None,
40 }
41 }
42
43 pub fn advance_effect_ticks(&mut self) {
45 if self.entrance < 1.0 && self.dissolve <= 0.0 {
46 self.entrance_tick += 1;
47 }
48 if self.dissolve > 0.0 && self.dissolve < 1.0 {
49 self.dissolve_tick += 1;
50 }
51 }
52
53 pub fn init_dissolve(&mut self, rng: &mut Box<dyn Rng>) -> usize {
56 let state = fx::build_dissolve_state(self.frames.lines(), rng);
57 let total = state.total_ticks;
58 self.dissolve_state = Some(state);
59 total
60 }
61}
62
63#[cfg(not(tarpaulin_include))]
64impl MascotComponent {
65 fn compute_origin(
66 &self,
67 terminal_size: crate::layout::Size,
68 elapsed: std::time::Duration,
69 ) -> crate::layout::Position {
70 let region = top_two_thirds(terminal_size);
71 if self.floating {
72 let bob = bobbing::bobbing_frame(elapsed);
73 bobbing::bobbing_origin(region, self.frames.size(), bob.row_offset)
74 } else {
75 centered_block_origin(region, self.frames.size())
76 }
77 }
78
79 fn render_frame<W: Write, R: Rng>(
80 &self,
81 writer: &mut W,
82 origin: crate::layout::Position,
83 rng: &mut R,
84 ) -> std::io::Result<()> {
85 let lines = self.frames.lines();
86
87 if self.dissolve > 0.0
88 && let Some(state) = &self.dissolve_state
89 {
90 let frame = fx::dissolve_frame(lines, state, self.dissolve_tick, rng);
91 return queue_frame_owned(writer, origin, &frame);
92 }
93
94 if self.entrance < 1.0 {
95 let frame = fx::entrance_frame(lines, &self.distance_map, self.entrance_tick, rng);
96 return queue_frame_owned(writer, origin, &frame);
97 }
98
99 queue_frame(writer, origin, lines)
100 }
101}
102
103#[cfg(not(tarpaulin_include))]
104impl Component for MascotComponent {
105 fn render<W: Write>(&self, renderer: &mut Renderer<W>) -> std::io::Result<u16> {
106 if (self.entrance <= 0.0 && self.dissolve <= 0.0) || self.dissolve >= 1.0 {
107 return Ok(0);
108 }
109 let terminal_size = renderer.ctx().terminal_size;
110 let elapsed = renderer.ctx().elapsed;
111 renderer.with_panel(|writer, _panel, rng| {
112 let origin = self.compute_origin(terminal_size, elapsed);
113 self.render_frame(writer, origin, rng)?;
114 Ok(0)
115 })
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn blizz_starts_with_zero_progress() {
125 let m = MascotComponent::blizz();
126 assert_eq!(m.entrance, 0.0);
127 assert_eq!(m.dissolve, 0.0);
128 assert!(!m.floating);
129 assert!(m.dissolve_state.is_none());
130 }
131
132 #[test]
133 fn advance_effect_ticks_increments_entrance_when_entering() {
134 let mut m = MascotComponent::blizz();
135 m.entrance = 0.5;
136 m.advance_effect_ticks();
137 assert_eq!(m.entrance_tick, 1);
138 assert_eq!(m.dissolve_tick, 0);
139 }
140
141 #[test]
142 fn advance_effect_ticks_increments_dissolve_when_dissolving() {
143 let mut m = MascotComponent::blizz();
144 m.entrance = 1.0;
145 m.dissolve = 0.5;
146 m.advance_effect_ticks();
147 assert_eq!(
148 m.entrance_tick, 0,
149 "entrance tick should not advance once entrance is done"
150 );
151 assert_eq!(m.dissolve_tick, 1);
152 }
153
154 #[test]
155 fn advance_effect_ticks_noop_when_idle() {
156 let mut m = MascotComponent::blizz();
157 m.entrance = 1.0;
158 m.dissolve = 0.0;
159 m.advance_effect_ticks();
160 assert_eq!(m.entrance_tick, 0);
161 assert_eq!(m.dissolve_tick, 0);
162 }
163
164 #[test]
165 fn init_dissolve_stores_state_and_returns_total_ticks() {
166 let mut m = MascotComponent::blizz();
167 let mut rng: Box<dyn rand::Rng> = Box::new(rand::rng());
168 let total = m.init_dissolve(&mut rng);
169 assert!(total > 0);
170 assert!(m.dissolve_state.is_some());
171 }
172}