use super::canvas;
use crate::{
PColor,
pico8::{Gfx, GfxDirty, GfxSprite, Pico8Asset, Pico8Handle, Pico8State},
};
use bevy::{ecs::lifecycle::HookContext, 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 .init_resource::<ClearCache>()
.init_resource::<DespawnClearablesOnNextClear>()
.add_systems(Last, (handle_overflow).chain());
}
#[derive(Resource, Default)]
pub struct DespawnClearablesOnNextClear(pub bool);
#[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, context: HookContext) {
let id = context.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)]
pub(crate) fn clear_screen(
In(color): In<PColor>,
mut query: Query<(Entity, &mut Clearable, &mut Visibility)>,
mut commands: Commands,
mut state: ResMut<Pico8State>,
mut cache: ResMut<ClearCache>,
mut despawn_on_next_clear: ResMut<DespawnClearablesOnNextClear>,
mut gfxs: ResMut<Assets<Gfx>>,
one_color: Single<&mut Sprite, With<canvas::OneColorBackground>>,
background: Single<(Entity, &GfxSprite, &mut GfxDirty), With<canvas::Background>>,
pico8_handle: Option<Res<Pico8Handle>>,
pico8_assets: Res<Assets<Pico8Asset>>,
images: Res<Assets<Image>>,
) {
let Some(pico8_handle) = pico8_handle else {
warn!("clear_screen called but no Pico8Handle present.");
return;
};
state.draw_state.clear_screen();
let mut sprite = one_color.into_inner();
let Some(pico8_asset) = pico8_assets.get(&pico8_handle.handle) else {
warn!("No pico8 asset setup during clear event.");
return;
};
match pico8_asset
.palettes
.get_color(color, state.palette, &images)
{
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;
}
if despawn_on_next_clear.0 {
despawn_on_next_clear.0 = false;
for (id, _, _) in &mut query {
commands.entity(id).despawn();
}
DRAW_COUNTER.set(1);
return;
}
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);
}
}