use super::canvas;
use crate::{
PColor,
pico8::{Gfx, GfxDirty, GfxSprite, Palettes, Pico8State},
};
use bevy::prelude::*;
use mashmap::MashMap;
mod counter;
use counter::DrawCounter;
static DRAW_COUNTER: DrawCounter = DrawCounter::new(1);
const MAX_EXPECTED_CLEARABLES: f32 = 1000.0;
pub(crate) fn plugin(app: &mut App) {
app.register_type::<Clearable>()
.add_event::<ClearEvent>()
.init_resource::<ClearCache>()
.add_systems(Last, (handle_overflow).chain())
.add_observer(handle_clear_event);
}
#[derive(Debug, Event, Clone, Copy)]
pub struct ClearEvent {
color: PColor,
}
impl ClearEvent {
pub fn new(color: PColor) -> Self {
Self { color }
}
}
#[derive(Resource, Default)]
pub(crate) struct ClearCache(MashMap<u64, Entity>);
impl ClearCache {
pub fn insert(&mut self, clearable: &Clearable, id: Entity) -> bool {
assert!(matches!(clearable.state, ClearState::Visible));
match clearable.hash {
Some(hash) => {
self.0.insert(hash, id);
true
}
None => false,
}
}
pub fn take(&mut self, hash: &u64) -> Option<Entity> {
self.0.remove_one(hash)
}
pub fn remove(&mut self, clearable: &Clearable, id: Entity) -> bool {
if let Some(hash) = clearable.hash {
let result = self.0.drain_key_if(&hash, |v| *v == id).next().is_some();
if matches!(clearable.state, ClearState::Visible) {
}
result
} else {
false
}
}
}
#[derive(Debug, Component, Clone, Copy, Reflect)]
#[component(on_remove = on_remove_hook)]
pub struct Clearable {
pub(crate) draw_count: usize,
pub time_to_live: u8,
pub hash: Option<u64>,
pub state: ClearState,
}
#[derive(Debug, Component, Clone, Copy, Reflect)]
pub enum ClearState {
Visible,
Hidden { time_to_live: u8 },
}
impl ClearState {
pub fn decrement_ttl(&mut self) -> Option<u8> {
match self {
ClearState::Visible => None,
ClearState::Hidden { time_to_live } => {
let last_ttl = *time_to_live;
*time_to_live = time_to_live.saturating_sub(1);
Some(last_ttl)
}
}
}
}
fn on_remove_hook(
mut world: bevy::ecs::world::DeferredWorld,
hook: bevy::ecs::component::HookContext,
) {
let id = hook.entity;
let Some(clearable) = world.get::<Clearable>(id).copied() else {
return;
};
let Some(mut cache) = world.get_resource_mut::<ClearCache>() else {
return;
};
cache.remove(&clearable, id);
}
impl Default for Clearable {
fn default() -> Self {
Clearable {
draw_count: DRAW_COUNTER.increment(),
time_to_live: 0,
hash: None,
state: ClearState::Visible,
}
}
}
impl Clearable {
pub fn new(time_to_live: u8) -> Self {
Clearable {
draw_count: DRAW_COUNTER.increment(),
time_to_live,
hash: None,
state: ClearState::Visible,
}
}
pub fn is_cached(&self) -> bool {
!matches!(self.state, ClearState::Visible)
}
pub fn mark_cached(&mut self) -> bool {
if !self.is_cached() {
self.state = ClearState::Hidden {
time_to_live: self.time_to_live,
};
true
} else {
false
}
}
pub fn with_hash(mut self, hash: u64) -> Self {
self.hash = Some(hash);
self
}
pub fn suggest_z(&self) -> f32 {
1.0 + self.draw_count as f32 / MAX_EXPECTED_CLEARABLES
}
pub fn resurrect(&mut self) {
self.state = ClearState::Visible;
self.draw_count = DRAW_COUNTER.increment();
}
}
fn handle_overflow(mut query: Query<&mut Clearable>) {
if DRAW_COUNTER.overflowed() {
for mut clearable in &mut query {
clearable.draw_count = 0;
}
DRAW_COUNTER.reset_overflowed()
}
}
#[allow(clippy::too_many_arguments)]
fn handle_clear_event(
trigger: Trigger<ClearEvent>,
mut query: Query<(Entity, &mut Clearable, &mut Visibility)>,
mut commands: Commands,
mut state: ResMut<Pico8State>,
mut cache: ResMut<ClearCache>,
mut gfxs: ResMut<Assets<Gfx>>,
one_color: Single<&mut Sprite, With<canvas::OneColorBackground>>,
background: Single<(Entity, &GfxSprite, &mut GfxDirty), With<canvas::Background>>,
palettes: Res<Palettes>,
) {
state.draw_state.clear_screen();
let mut sprite = one_color.into_inner();
match palettes.get_color(trigger.color, state.palette) {
Ok(color) => {
sprite.color = color;
}
Err(e) => {
error!("Could not clear to color: {e}");
sprite.color = Srgba::rgb(1.0, 0.0, 1.0).into(); }
}
let (_background_id, gfx_sprite, mut gfx_dirty) = background.into_inner();
if gfx_dirty.0 {
if let Some(gfx) = gfxs.get_mut(&gfx_sprite.image) {
trace!("Clearing Background pixels.");
gfx.data.set_elements(0x00);
}
gfx_dirty.0 = false;
}
for (id, mut clearable, mut visibility) in &mut query {
match clearable.state.decrement_ttl() {
Some(0) => {
commands.entity(id).despawn();
}
Some(_ttl) => {
}
None => {
if clearable.time_to_live == 0 || clearable.hash.is_none() {
commands.entity(id).despawn();
} else {
*visibility = Visibility::Hidden;
if cache.insert(&clearable, id) {
assert!(clearable.mark_cached());
} else {
panic!();
}
}
}
}
}
DRAW_COUNTER.set(1);
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test0() {
static COUNTER: DrawCounter = DrawCounter::new(0);
assert_eq!(COUNTER.increment(), 0);
assert_eq!(COUNTER.increment(), 1);
assert_eq!(COUNTER.get(), 2);
}
}