use std::io::Write;
use rand::Rng;
use super::bobbing;
use super::frames::{MascotFrames, queue_frame, queue_frame_owned};
use super::fx;
use crate::layout::{centered_block_origin, top_two_thirds};
use crate::{Component, Renderer};
#[derive(Clone, Debug)]
pub struct MascotComponent {
pub frames: MascotFrames,
pub entrance: f64,
pub dissolve: f64,
pub floating: bool,
distance_map: fx::DistanceMap,
entrance_tick: usize,
dissolve_tick: usize,
dissolve_state: Option<fx::DissolveState>,
}
impl MascotComponent {
pub fn blizz() -> Self {
let frames = MascotFrames::blizz();
let distance_map = fx::build_distance_map(frames.lines());
Self {
frames,
entrance: 0.0,
dissolve: 0.0,
floating: false,
distance_map,
entrance_tick: 0,
dissolve_tick: 0,
dissolve_state: None,
}
}
pub fn advance_effect_ticks(&mut self) {
if self.entrance < 1.0 && self.dissolve <= 0.0 {
self.entrance_tick += 1;
}
if self.dissolve > 0.0 && self.dissolve < 1.0 {
self.dissolve_tick += 1;
}
}
pub fn init_dissolve(&mut self, rng: &mut Box<dyn Rng>) -> usize {
let state = fx::build_dissolve_state(self.frames.lines(), rng);
let total = state.total_ticks;
self.dissolve_state = Some(state);
total
}
}
#[cfg(not(tarpaulin_include))]
impl MascotComponent {
fn compute_origin(
&self,
terminal_size: crate::layout::Size,
elapsed: std::time::Duration,
) -> crate::layout::Position {
let region = top_two_thirds(terminal_size);
if self.floating {
let bob = bobbing::bobbing_frame(elapsed);
bobbing::bobbing_origin(region, self.frames.size(), bob.row_offset)
} else {
centered_block_origin(region, self.frames.size())
}
}
fn render_frame<W: Write, R: Rng>(
&self,
writer: &mut W,
origin: crate::layout::Position,
rng: &mut R,
) -> std::io::Result<()> {
let lines = self.frames.lines();
if self.dissolve > 0.0
&& let Some(state) = &self.dissolve_state
{
let frame = fx::dissolve_frame(lines, state, self.dissolve_tick, rng);
return queue_frame_owned(writer, origin, &frame);
}
if self.entrance < 1.0 {
let frame = fx::entrance_frame(lines, &self.distance_map, self.entrance_tick, rng);
return queue_frame_owned(writer, origin, &frame);
}
queue_frame(writer, origin, lines)
}
}
#[cfg(not(tarpaulin_include))]
impl Component for MascotComponent {
fn render<W: Write>(&self, renderer: &mut Renderer<W>) -> std::io::Result<u16> {
if (self.entrance <= 0.0 && self.dissolve <= 0.0) || self.dissolve >= 1.0 {
return Ok(0);
}
let terminal_size = renderer.ctx().terminal_size;
let elapsed = renderer.ctx().elapsed;
renderer.with_panel(|writer, _panel, rng| {
let origin = self.compute_origin(terminal_size, elapsed);
self.render_frame(writer, origin, rng)?;
Ok(0)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blizz_starts_with_zero_progress() {
let m = MascotComponent::blizz();
assert_eq!(m.entrance, 0.0);
assert_eq!(m.dissolve, 0.0);
assert!(!m.floating);
assert!(m.dissolve_state.is_none());
}
#[test]
fn advance_effect_ticks_increments_entrance_when_entering() {
let mut m = MascotComponent::blizz();
m.entrance = 0.5;
m.advance_effect_ticks();
assert_eq!(m.entrance_tick, 1);
assert_eq!(m.dissolve_tick, 0);
}
#[test]
fn advance_effect_ticks_increments_dissolve_when_dissolving() {
let mut m = MascotComponent::blizz();
m.entrance = 1.0;
m.dissolve = 0.5;
m.advance_effect_ticks();
assert_eq!(
m.entrance_tick, 0,
"entrance tick should not advance once entrance is done"
);
assert_eq!(m.dissolve_tick, 1);
}
#[test]
fn advance_effect_ticks_noop_when_idle() {
let mut m = MascotComponent::blizz();
m.entrance = 1.0;
m.dissolve = 0.0;
m.advance_effect_ticks();
assert_eq!(m.entrance_tick, 0);
assert_eq!(m.dissolve_tick, 0);
}
#[test]
fn init_dissolve_stores_state_and_returns_total_ticks() {
let mut m = MascotComponent::blizz();
let mut rng: Box<dyn rand::Rng> = Box::new(rand::rng());
let total = m.init_dissolve(&mut rng);
assert!(total > 0);
assert!(m.dissolve_state.is_some());
}
}