use std::collections::BTreeMap;
use bevy_app::App;
use bevy_ecs::prelude::{Commands, IntoScheduleConfigs, NonSendMut, ResMut, Resource};
use bevy_time::{Real, Time};
use plurimus::core::ratatui_core::layout::Rect;
use plurimus::core::{
CompositeSystems, FrameBuffer, MainWorld, TerminalRenderApp, TerminalRenderAppExt,
TerminalRenderSystems,
};
use tachyonfx::{CellFilter, Effect, EffectTimer, Interpolation, fx};
use super::{Cue, Cues, Key, Motion, Play};
use crate::commands::tui::settings::Settings;
use crate::commands::tui::theme::ground;
const EASING: Interpolation = Interpolation::QuadOut;
#[derive(Resource, Default, Debug)]
struct Cued {
cues: Vec<Cue>,
motion: Motion,
delta: std::time::Duration,
}
struct Running {
play: Play,
effect: Effect,
is_arriving: bool,
}
#[derive(Default)]
struct Playing(BTreeMap<Key, Running>);
pub fn install(app: &mut App) {
app.add_extract_systems(extract);
app.sub_app_mut(TerminalRenderApp)
.init_resource::<Cued>()
.world_mut()
.insert_non_send(Playing::default());
app.add_terminal_systems(
TerminalRenderSystems::Composite,
play.in_set(CompositeSystems::PostProcess)
.after(ground::Grounded),
);
}
fn extract(mut main_world: ResMut<MainWorld>, mut commands: Commands) {
let delta = main_world.resource::<Time<Real>>().delta();
let motion = main_world.resource::<Settings>().motion;
let cues = std::mem::take(&mut main_world.resource_mut::<Cues>().0);
commands.insert_resource(Cued {
cues,
motion,
delta,
});
}
fn play(
mut frame: ResMut<FrameBuffer>,
mut extracted: ResMut<Cued>,
mut playing: NonSendMut<Playing>,
) {
let cues = std::mem::take(&mut extracted.cues);
if extracted.motion == Motion::Off {
playing.0.clear();
return;
}
for cue in cues {
start(&mut playing, cue, extracted.motion, *frame.0.area());
}
let tick = tachyonfx::Duration::from(extracted.delta);
let whole = *frame.0.area();
playing.0.retain(|_, running| {
let step = if std::mem::take(&mut running.is_arriving) {
tachyonfx::Duration::ZERO
} else {
tick
};
running.effect.process(step, &mut frame.0, whole);
!running.effect.done()
});
}
fn start(playing: &mut Playing, cue: Cue, motion: Motion, frame: Rect) {
let Cue::Play { play, area, spared } = cue else {
if let Cue::Stop(gone) = cue {
playing.0.remove(&gone);
}
return;
};
let key = play.key();
let area = area.intersection(frame);
let length = motion.length(play);
if area.is_empty() || length.is_zero() {
playing.0.remove(&key);
return;
}
if let Some(running) = playing.0.get_mut(&key)
&& running.play == play
&& play.is_held()
{
running.effect.filter(outside(area, spared));
return;
}
let millis = u32::try_from(length.as_millis()).unwrap_or(u32::MAX);
let timer = EffectTimer::from_ms(millis, EASING);
playing.0.insert(
key,
Running {
play,
effect: effect(play, area, spared, timer),
is_arriving: true,
},
);
}
fn outside(area: Rect, spared: Rect) -> CellFilter {
CellFilter::NoneOf(vec![CellFilter::Area(area), CellFilter::Area(spared)])
}
fn effect(play: Play, area: Rect, spared: Rect, timer: EffectTimer) -> Effect {
match play {
Play::Dim(colour) => {
fx::never_complete(fx::fade_to_fg(colour, timer)).with_filter(outside(area, spared))
}
Play::Receipt(colour) => fx::fade_from_fg(colour, timer).with_area(area),
Play::Coalesce => fx::coalesce(timer).with_area(area),
}
}