use bevy::prelude::*;
use tracing::debug;
use super::loader::TextureManifest;
use super::materials::{
BaselineMaterialKind, CanonicalMaterialHandles, FallbackRegistry, ProfileMaterialBank,
};
use crate::{Border, GroundPlane};
pub struct LevelOverridesPlugin;
impl Plugin for LevelOverridesPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<LevelPresentation>();
app.add_systems(
Update,
(
refresh_presentation_on_manifest_change,
apply_level_overrides,
)
.chain(),
);
}
}
#[derive(Resource, Debug, Clone, Default)]
pub struct LevelPresentation {
level_number: u32,
ground_profile: Option<String>,
background_profile: Option<String>,
sidewall_profile: Option<String>,
tint: Option<Color>,
}
impl LevelPresentation {
pub fn for_level(level_number: u32, manifest: &TextureManifest) -> Self {
let Some(level_set) = manifest.level_overrides.get(&level_number) else {
return Self {
level_number,
ground_profile: None,
background_profile: None,
sidewall_profile: None,
tint: None,
};
};
Self {
level_number,
ground_profile: level_set.ground_profile.clone(),
background_profile: level_set.background_profile.clone(),
sidewall_profile: level_set.sidewall_profile.clone(),
tint: level_set.tint,
}
}
pub fn level_number(&self) -> u32 {
self.level_number
}
pub fn ground_profile(&self) -> Option<&String> {
self.ground_profile.as_ref()
}
pub fn background_profile(&self) -> Option<&String> {
self.background_profile.as_ref()
}
pub fn sidewall_profile(&self) -> Option<&String> {
self.sidewall_profile.as_ref()
}
pub fn tint(&self) -> Option<Color> {
self.tint
}
pub fn reset(&mut self) {
self.level_number = 0;
self.ground_profile = None;
self.background_profile = None;
self.sidewall_profile = None;
self.tint = None;
}
pub fn update_from_level_and_manifest(
&mut self,
level: &crate::level_loader::LevelDefinition,
manifest: &TextureManifest,
) {
#[cfg(feature = "texture_manifest")]
let level_set = level
.presentation
.as_ref()
.or_else(|| manifest.level_overrides.get(&level.number));
#[cfg(not(feature = "texture_manifest"))]
let level_set = manifest.level_overrides.get(&level.number);
if let Some(level_set) = level_set {
*self = Self {
level_number: level.number,
ground_profile: level_set.ground_profile.clone(),
background_profile: level_set.background_profile.clone(),
sidewall_profile: level_set.sidewall_profile.clone(),
tint: level_set.tint,
};
} else {
*self = Self {
level_number: level.number,
ground_profile: None,
background_profile: None,
sidewall_profile: None,
tint: None,
};
}
}
pub fn override_profile_for(&self, kind: BaselineMaterialKind) -> Option<&String> {
match kind {
BaselineMaterialKind::Ground => self.ground_profile.as_ref(),
BaselineMaterialKind::Background => self.background_profile.as_ref(),
BaselineMaterialKind::Sidewall => self.sidewall_profile.as_ref(),
BaselineMaterialKind::Ball
| BaselineMaterialKind::Paddle
| BaselineMaterialKind::Brick => None,
}
}
pub fn resolve_material(
&self,
kind: BaselineMaterialKind,
bank: &ProfileMaterialBank,
mut fallback: Option<&mut FallbackRegistry>,
) -> Option<Handle<StandardMaterial>> {
let profile_id = self.override_profile_for(kind)?;
if let Some(handle) = bank.handle(profile_id) {
return Some(handle);
}
if let Some(fb) = fallback.as_mut() {
fb.log_once(format!(
"level {}: override profile '{}' not found for {:?}",
self.level_number, profile_id, kind
));
}
None
}
}
fn apply_level_overrides(
presentation: Res<LevelPresentation>,
bank: Res<ProfileMaterialBank>,
canonical: Option<Res<CanonicalMaterialHandles>>,
mut fallback: Option<ResMut<FallbackRegistry>>,
mut ground_query: Query<&mut MeshMaterial3d<StandardMaterial>, With<GroundPlane>>,
mut border_query: Query<
&mut MeshMaterial3d<StandardMaterial>,
(With<Border>, Without<GroundPlane>),
>,
) {
if !presentation.is_changed() {
return;
}
if let Some(handle) = resolve_override_or_canonical(
&presentation,
BaselineMaterialKind::Ground,
&bank,
canonical.as_deref(),
fallback.as_deref_mut(),
) {
for mut material in ground_query.iter_mut() {
debug!(
target: "textures::overrides",
level = presentation.level_number(),
profile = ?presentation.ground_profile(),
"Applying ground material override"
);
material.0 = handle.clone();
}
}
if let Some(handle) = resolve_override_or_canonical(
&presentation,
BaselineMaterialKind::Sidewall,
&bank,
canonical.as_deref(),
fallback.as_deref_mut(),
) {
for mut material in border_query.iter_mut() {
debug!(
target: "textures::overrides",
level = presentation.level_number(),
profile = ?presentation.sidewall_profile(),
"Applying sidewall material override"
);
material.0 = handle.clone();
}
}
}
fn refresh_presentation_on_manifest_change(
current_level: Option<Res<crate::level_loader::CurrentLevel>>,
manifest: Option<Res<TextureManifest>>,
bank: Option<Res<ProfileMaterialBank>>,
mut presentation: ResMut<LevelPresentation>,
) {
let manifest_changed = manifest.as_ref().is_some_and(|m| m.is_changed());
let bank_changed = bank.as_ref().is_some_and(|b| b.is_changed());
if !manifest_changed && !bank_changed {
return;
}
let level_number = presentation.level_number();
if level_number == 0 {
return;
}
if let Some(manifest) = manifest {
if let Some(level) = current_level {
debug!(
target: "textures::overrides::hot_reload",
level = level_number,
manifest_changed,
bank_changed,
"Refreshing level presentation after manifest/asset change"
);
presentation.update_from_level_and_manifest(&level.0, &manifest);
}
}
}
fn resolve_override_or_canonical(
presentation: &LevelPresentation,
kind: BaselineMaterialKind,
bank: &ProfileMaterialBank,
canonical: Option<&CanonicalMaterialHandles>,
mut fallback: Option<&mut FallbackRegistry>,
) -> Option<Handle<StandardMaterial>> {
if let Some(handle) = presentation.resolve_material(kind, bank, fallback.as_deref_mut()) {
return Some(handle);
}
if let Some(handles) = canonical {
return handles.get(kind);
}
fallback.map(|fb| fb.handle(kind.into()).clone())
}