#[cfg(feature = "debug")]
pub mod debug_collider {
#[derive(Resource, Default)]
pub struct ColliderDebugSettings {
pub show_colliders: bool,
}
use crate::app_state::overworld::character::components::PlayerControlled;
use crate::app_state::overworld::tilemap::systems::TilemapCollider;
use crate::app_state::overworld::tilemap::*;
use crate::app_state::overworld::trigger::TriggerZone;
use crate::core::collision::{PhysicsCollider, Rect2DCollider, TriggerCollider};
use bevy::prelude::*;
use bevy_smud::prelude::*;
#[derive(Component)]
pub struct ColliderVisualizer {
pub parent: Entity,
}
#[derive(Component)]
pub struct BattleColliderVisualized;
#[derive(Component)]
pub struct AmMaskVisualizer {
pub mask_layer_id: u64,
}
#[derive(Component)]
pub struct DebugVisualizerRoot;
pub fn setup_collider_debug(app: &mut App) {
app.init_resource::<ColliderDebugSettings>()
.add_systems(Startup, setup_debug_visualizer_root_system)
.add_systems(
Update,
(
toggle_collider_visibility_system,
render_rect_colliders_system,
update_collider_visualizer_positions_system,
render_battle_colliders_system,
update_battle_collider_visualizer_positions_system,
),
);
app.add_systems(Update, render_trigger_zones_system);
app.add_systems(
Update,
(
render_am_masks_system,
update_am_mask_visualizer_positions_system,
),
);
}
fn setup_debug_visualizer_root_system(mut commands: Commands) {
commands.spawn((
DebugVisualizerRoot,
Name::new("DebugVisualizers"),
Transform::default(),
Visibility::default(),
));
}
fn toggle_collider_visibility_system(
keyboard: Res<ButtonInput<KeyCode>>,
mut settings: ResMut<ColliderDebugSettings>,
) {
if keyboard.just_pressed(KeyCode::F3) {
settings.show_colliders = !settings.show_colliders;
info!(
"Collider visualization: {}",
if settings.show_colliders { "ON" } else { "OFF" }
);
}
}
#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
fn render_rect_colliders_system(
mut commands: Commands,
mut shaders: ResMut<Assets<Shader>>,
settings: Res<ColliderDebugSettings>,
debug_root: Query<Entity, With<DebugVisualizerRoot>>,
player_colliders: Query<
(Entity, &Transform, &Rect2DCollider),
(
With<PlayerControlled>,
Without<SmudShape>,
Without<ColliderVisualizer>,
),
>,
tilemap_colliders: Query<
(Entity, &Transform, &Rect2DCollider),
(
With<TilemapCollider>,
Without<PlayerControlled>,
Without<SmudShape>,
Without<ColliderVisualizer>,
),
>,
object_colliders: Query<
(Entity, &Transform, &Rect2DCollider),
(
With<ObjectCollider>,
Without<PlayerControlled>,
Without<SmudShape>,
Without<ColliderVisualizer>,
),
>,
battle_physics_colliders: Query<Entity, With<PhysicsCollider>>,
battle_trigger_colliders: Query<Entity, With<TriggerCollider>>,
battle_boxes: Query<Entity, With<crate::app_state::battle::collision::BattleBox>>,
fre_trigger_zones: Query<Entity, With<TriggerZone>>,
existing_visualizers: Query<(Entity, &ColliderVisualizer)>,
) {
let Ok(debug_root_entity) = debug_root.single() else {
return;
};
if !settings.show_colliders {
for (visualizer_entity, _) in existing_visualizers.iter() {
commands.entity(visualizer_entity).despawn();
}
return;
}
for (visualizer_entity, visualizer) in existing_visualizers.iter() {
let mut parent_exists = player_colliders.get(visualizer.parent).is_ok()
|| tilemap_colliders.get(visualizer.parent).is_ok()
|| object_colliders.get(visualizer.parent).is_ok()
|| battle_physics_colliders.get(visualizer.parent).is_ok()
|| battle_trigger_colliders.get(visualizer.parent).is_ok()
|| battle_boxes.get(visualizer.parent).is_ok();
parent_exists = parent_exists || fre_trigger_zones.get(visualizer.parent).is_ok();
if !parent_exists {
commands.entity(visualizer_entity).despawn();
}
}
let mut create_visualizer = |entity: Entity,
transform: &Transform,
collider: &Rect2DCollider,
color: Color,
name: String| {
let has_visualizer = existing_visualizers
.iter()
.any(|(_, vis)| vis.parent == entity);
if has_visualizer {
return;
}
let border_sdf = shaders.add_sdf_expr(format!(
"abs(smud::sd_box(p, vec2<f32>({}, {}))) - {}",
collider.size.x / 2.0,
collider.size.y / 2.0,
0.125
));
let frame_size = (collider.size.x.max(collider.size.y) / 2.0) + 2.0;
let final_position = transform.translation + collider.offset.extend(0.1);
commands.entity(debug_root_entity).with_children(|parent| {
parent.spawn((
ColliderVisualizer { parent: entity },
SmudShape {
color,
sdf: border_sdf,
frame: Frame::Quad(frame_size),
fill: SIMPLE_FILL_HANDLE,
..default()
},
Transform::from_translation(final_position),
Name::new(name),
));
});
};
for (entity, transform, collider) in player_colliders.iter() {
create_visualizer(
entity,
transform,
collider,
Color::hsl(120.0, 1.0, 0.5),
"Player Collider".to_string(),
);
}
for (entity, transform, collider) in tilemap_colliders.iter() {
create_visualizer(
entity,
transform,
collider,
Color::hsl(120.0, 0.75, 0.75),
"Tilemap Collider".to_string(),
);
}
for (entity, transform, collider) in object_colliders.iter() {
create_visualizer(
entity,
transform,
collider,
Color::hsl(120.0, 0.75, 0.75),
"Object Collider".to_string(),
);
}
}
#[allow(clippy::type_complexity)]
fn render_trigger_zones_system(
mut commands: Commands,
mut shaders: ResMut<Assets<Shader>>,
settings: Res<ColliderDebugSettings>,
debug_root: Query<Entity, With<DebugVisualizerRoot>>,
trigger_zones: Query<
(Entity, &Transform, &Rect2DCollider, &TriggerZone),
(Without<SmudShape>, Without<ColliderVisualizer>),
>,
existing_visualizers: Query<(Entity, &ColliderVisualizer)>,
) {
let Ok(debug_root_entity) = debug_root.single() else {
return;
};
if !settings.show_colliders {
return;
}
for (entity, transform, collider, trigger_zone) in trigger_zones.iter() {
let has_visualizer = existing_visualizers
.iter()
.any(|(_, vis)| vis.parent == entity);
if has_visualizer {
continue;
}
let border_sdf = shaders.add_sdf_expr(format!(
"abs(smud::sd_box(p, vec2<f32>({}, {}))) - {}",
collider.size.x / 2.0,
collider.size.y / 2.0,
0.125
));
let frame_size = (collider.size.x.max(collider.size.y) / 2.0) + 2.0;
let final_position = transform.translation + collider.offset.extend(0.1);
let color = if trigger_zone.player_inside {
Color::hsl(180.0, 1.0, 0.7) } else {
Color::hsl(180.0, 1.0, 0.5) };
commands.entity(debug_root_entity).with_children(|parent| {
parent.spawn((
ColliderVisualizer { parent: entity },
SmudShape {
color,
sdf: border_sdf,
frame: Frame::Quad(frame_size),
fill: SIMPLE_FILL_HANDLE,
..default()
},
Transform::from_translation(final_position),
Name::new(format!("TriggerZone_{}", trigger_zone.id)),
));
});
}
}
#[allow(clippy::type_complexity)]
fn update_collider_visualizer_positions_system(
mut visualizers: Query<(&mut Transform, &ColliderVisualizer), Without<Rect2DCollider>>,
colliders: Query<
(&Transform, &Rect2DCollider),
(With<Rect2DCollider>, Without<ColliderVisualizer>),
>,
) {
for (mut vis_transform, visualizer) in visualizers.iter_mut() {
if let Ok((parent_transform, collider)) = colliders.get(visualizer.parent) {
let final_position = parent_transform.translation + collider.offset.extend(0.1);
vis_transform.translation = final_position;
}
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn render_battle_colliders_system(
mut commands: Commands,
mut shaders: ResMut<Assets<Shader>>,
settings: Res<ColliderDebugSettings>,
debug_root: Query<Entity, With<DebugVisualizerRoot>>,
physics_colliders: Query<
(Entity, &Transform, &crate::core::collision::PhysicsCollider),
(
Without<SmudShape>,
Without<ColliderVisualizer>,
Without<BattleColliderVisualized>,
),
>,
trigger_colliders: Query<
(
Entity,
&GlobalTransform,
&crate::core::collision::TriggerCollider,
Option<&crate::core::collision::HitboxOffset>,
),
(
Without<SmudShape>,
Without<ColliderVisualizer>,
Without<BattleColliderVisualized>,
),
>,
chase_hitboxes: Query<Entity, With<crate::app_state::overworld::chase::ChasePlayerHitbox>>,
battle_boxes: Query<
(
Entity,
&GlobalTransform,
&crate::core::ui::components::UIBox,
),
(
With<crate::app_state::battle::collision::BattleBox>,
Without<SmudShape>,
Without<ColliderVisualizer>,
Without<BattleColliderVisualized>,
Without<crate::core::collision::PhysicsCollider>,
),
>,
am_battle_boxes: Query<
(
Entity,
&GlobalTransform,
&crate::app_state::battle::collision::AmBattleBoxBounds,
),
(
With<crate::app_state::battle::collision::BattleBox>,
Without<crate::core::ui::components::UIBox>,
Without<SmudShape>,
Without<ColliderVisualizer>,
Without<BattleColliderVisualized>,
Without<crate::core::collision::PhysicsCollider>,
),
>,
existing_visualizers: Query<(Entity, &ColliderVisualizer)>,
) {
use crate::core::collision::{PhysicsCollider, TriggerCollider};
let Ok(debug_root_entity) = debug_root.single() else {
return;
};
if !settings.show_colliders {
return;
}
for (entity, transform, physics_collider) in physics_colliders.iter() {
let has_visualizer = existing_visualizers
.iter()
.any(|(_, vis)| vis.parent == entity);
if has_visualizer {
continue;
}
let (sdf, frame_size, name) = match physics_collider {
PhysicsCollider::Circle { radius } => {
let sdf = shaders
.add_sdf_expr(format!("abs(smud::sd_circle(p, {})) - 0.125", radius));
(sdf, radius + 2.0, "Physics Collider (Circle)")
}
PhysicsCollider::Box { half_size } => {
let sdf = shaders.add_sdf_expr(format!(
"abs(smud::sd_box(p, vec2<f32>({}, {}))) - 0.125",
half_size.x, half_size.y
));
let max_dim = half_size.x.max(half_size.y) + 2.0;
(sdf, max_dim, "Physics Collider (Box)")
}
};
commands.entity(debug_root_entity).with_children(|parent| {
parent.spawn((
SmudShape {
color: Color::hsl(120.0, 1.0, 0.5),
sdf,
frame: Frame::Quad(frame_size),
fill: SIMPLE_FILL_HANDLE,
..default()
},
Transform::from_translation(transform.translation + Vec3::new(0.0, 0.0, 20.0)),
ColliderVisualizer { parent: entity },
Name::new(name.to_string()),
));
});
}
for (entity, transform, trigger_collider, hitbox_offset) in trigger_colliders.iter() {
let has_visualizer = existing_visualizers
.iter()
.any(|(_, vis)| vis.parent == entity);
if has_visualizer {
continue;
}
let is_chase_hitbox = chase_hitboxes.get(entity).is_ok();
let color = if is_chase_hitbox {
Color::hsl(0.0, 1.0, 0.5) } else {
Color::hsl(120.0, 1.0, 0.5) };
let (sdf, frame_size, name) = match trigger_collider {
TriggerCollider::Circle { radius } => {
let sdf = shaders
.add_sdf_expr(format!("abs(smud::sd_circle(p, {})) - 0.125", radius));
(
sdf,
radius + 2.0,
if is_chase_hitbox {
"Chase Hitbox (Circle)"
} else {
"Trigger Collider (Circle)"
},
)
}
TriggerCollider::Box { half_size } => {
let sdf = shaders.add_sdf_expr(format!(
"abs(smud::sd_box(p, vec2<f32>({}, {}))) - 0.125",
half_size.x, half_size.y
));
let max_dim = half_size.x.max(half_size.y) + 2.0;
(
sdf,
max_dim,
if is_chase_hitbox {
"Chase Hitbox (Box)"
} else {
"Trigger Collider (Box)"
},
)
}
};
let offset = hitbox_offset.map(|o| o.0).unwrap_or(Vec2::ZERO);
let position = transform.translation() + offset.extend(20.0);
commands.entity(debug_root_entity).with_children(|parent| {
parent.spawn((
SmudShape {
color,
sdf,
frame: Frame::Quad(frame_size),
fill: SIMPLE_FILL_HANDLE,
..default()
},
Transform::from_translation(position),
ColliderVisualizer { parent: entity },
Name::new(name.to_string()),
));
});
}
for (entity, global_transform, ui_box) in battle_boxes.iter() {
let has_visualizer = existing_visualizers
.iter()
.any(|(_, vis)| vis.parent == entity);
if has_visualizer {
continue;
}
let half_width = ui_box.width() / 2.0;
let half_height = ui_box.height() / 2.0;
let sdf = shaders.add_sdf_expr(format!(
"abs(smud::sd_box(p, vec2<f32>({}, {}))) - 0.125",
half_width, half_height
));
let frame_size = half_width.max(half_height) + 2.0;
commands.entity(debug_root_entity).with_children(|parent| {
parent.spawn((
SmudShape {
color: Color::hsl(120.0, 1.0, 0.5),
sdf,
frame: Frame::Quad(frame_size),
fill: SIMPLE_FILL_HANDLE,
..default()
},
Transform::from_translation(
global_transform.translation() + Vec3::new(0.0, 0.0, 50.0),
),
ColliderVisualizer { parent: entity },
Name::new("Battle Box Debug"),
));
});
}
for (entity, global_transform, am_bounds) in am_battle_boxes.iter() {
let has_visualizer = existing_visualizers
.iter()
.any(|(_, vis)| vis.parent == entity);
if has_visualizer {
continue;
}
let half_width = am_bounds.width / 2.0;
let half_height = am_bounds.height / 2.0;
let sdf = shaders.add_sdf_expr(format!(
"abs(smud::sd_box(p, vec2<f32>({}, {}))) - 0.125",
half_width, half_height
));
let frame_size = half_width.max(half_height) + 2.0;
let center_pos = global_transform.translation()
+ Vec3::new(am_bounds.center_offset.x, am_bounds.center_offset.y, 50.0);
commands.entity(debug_root_entity).with_children(|parent| {
parent.spawn((
SmudShape {
color: Color::hsl(180.0, 1.0, 0.5), sdf,
frame: Frame::Quad(frame_size),
fill: SIMPLE_FILL_HANDLE,
..default()
},
Transform::from_translation(center_pos),
ColliderVisualizer { parent: entity },
Name::new("AM Battle Box Debug"),
));
});
}
}
#[allow(clippy::type_complexity)]
fn update_battle_collider_visualizer_positions_system(
mut visualizers: Query<
(&mut Transform, &ColliderVisualizer),
(
Without<crate::core::collision::PhysicsCollider>,
Without<crate::core::collision::TriggerCollider>,
),
>,
physics_colliders: Query<
&Transform,
(
With<crate::core::collision::PhysicsCollider>,
Without<ColliderVisualizer>,
),
>,
trigger_colliders: Query<
(
&GlobalTransform,
Option<&crate::core::collision::HitboxOffset>,
),
(
With<crate::core::collision::TriggerCollider>,
Without<ColliderVisualizer>,
),
>,
battle_boxes: Query<
&GlobalTransform,
(
With<crate::core::ui::components::UIBox>,
With<crate::app_state::battle::collision::BattleBox>,
Without<ColliderVisualizer>,
),
>,
am_battle_boxes: Query<
(
&GlobalTransform,
&crate::app_state::battle::collision::AmBattleBoxBounds,
),
(
With<crate::app_state::battle::collision::BattleBox>,
Without<crate::core::ui::components::UIBox>,
Without<ColliderVisualizer>,
),
>,
) {
for (mut vis_transform, visualizer) in visualizers.iter_mut() {
if let Ok(parent_transform) = physics_colliders.get(visualizer.parent) {
vis_transform.translation.x = parent_transform.translation.x;
vis_transform.translation.y = parent_transform.translation.y;
} else if let Ok((parent_global, hitbox_offset)) =
trigger_colliders.get(visualizer.parent)
{
let offset = hitbox_offset.map(|o| o.0).unwrap_or(Vec2::ZERO);
vis_transform.translation.x = parent_global.translation().x + offset.x;
vis_transform.translation.y = parent_global.translation().y + offset.y;
} else if let Ok(parent_global) = battle_boxes.get(visualizer.parent) {
vis_transform.translation.x = parent_global.translation().x;
vis_transform.translation.y = parent_global.translation().y;
} else if let Ok((parent_global, am_bounds)) = am_battle_boxes.get(visualizer.parent) {
vis_transform.translation.x =
parent_global.translation().x + am_bounds.center_offset.x;
vis_transform.translation.y =
parent_global.translation().y + am_bounds.center_offset.y;
}
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn render_am_masks_system(
mut commands: Commands,
mut shaders: ResMut<Assets<Shader>>,
settings: Res<ColliderDebugSettings>,
debug_root: Query<Entity, With<DebugVisualizerRoot>>,
sdf_query: Query<(
Entity,
&MeshMaterial2d<bevy_alight_motion::sdf_material::SdfMaterial>,
)>,
sdf_materials: Res<Assets<bevy_alight_motion::sdf_material::SdfMaterial>>,
existing_visualizers: Query<(Entity, &AmMaskVisualizer)>,
) {
if settings.show_colliders {
bevy::log::debug!("[MaskDebug] System running, show_colliders=true");
}
let Ok(debug_root_entity) = debug_root.single() else {
bevy::log::warn!("[MaskDebug] No DebugVisualizerRoot found!");
return;
};
if !settings.show_colliders {
for (visualizer_entity, _) in existing_visualizers.iter() {
commands.entity(visualizer_entity).despawn();
}
return;
}
let mut found_mask_params: Option<(f32, f32, f32, f32)> = None;
let mut total_sdf_shapes = 0;
let mut masked_shapes = 0;
for (_, material_handle) in sdf_query.iter() {
total_sdf_shapes += 1;
if let Some(material) = sdf_materials.get(&material_handle.0) {
let mask_type = material.uniform_data.mask_type;
if mask_type > 0.5 {
masked_shapes += 1;
let params = material.uniform_data.mask_params;
if found_mask_params.is_none() {
found_mask_params = Some((params.x, params.y, params.z, params.w));
}
}
}
}
let has_existing_visualizer = !existing_visualizers.is_empty();
if let Some((center_x, center_y, half_width, half_height)) = found_mask_params {
if !has_existing_visualizer {
let outline_sdf = shaders.add_sdf_expr(format!(
"abs(smud::sd_box(p, vec2<f32>({}, {}))) - 0.5",
half_width, half_height
));
let frame_size = half_width.max(half_height) + 5.0;
bevy::log::info!(
"[MaskDebug] Creating visualizer: center=({:.1},{:.1}), half_size=({:.1},{:.1}), frame={:.1}",
center_x,
center_y,
half_width,
half_height,
frame_size
);
bevy::log::info!(
"[MaskDebug] Stats: total_sdf_shapes={}, masked_shapes={}",
total_sdf_shapes,
masked_shapes
);
commands.entity(debug_root_entity).with_children(|parent| {
parent.spawn((
AmMaskVisualizer { mask_layer_id: 0 },
SmudShape {
color: Color::hsl(0.0, 1.0, 0.5), sdf: outline_sdf,
frame: Frame::Quad(frame_size),
fill: SIMPLE_FILL_HANDLE,
..default()
},
Transform::from_translation(Vec3::new(center_x, center_y, 100.0)),
Name::new("AM Mask Debug (from shader params)"),
));
});
}
} else {
for (visualizer_entity, _) in existing_visualizers.iter() {
commands.entity(visualizer_entity).despawn();
}
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn update_am_mask_visualizer_positions_system(
sdf_query: Query<&MeshMaterial2d<bevy_alight_motion::sdf_material::SdfMaterial>>,
sdf_materials: Res<Assets<bevy_alight_motion::sdf_material::SdfMaterial>>,
mut visualizers: Query<(&mut Transform, &AmMaskVisualizer)>,
) {
let mut found_mask_params: Option<(f32, f32)> = None;
for material_handle in sdf_query.iter() {
if let Some(material) = sdf_materials.get(&material_handle.0) {
let mask_type = material.uniform_data.mask_type;
if mask_type > 0.5 {
let params = material.uniform_data.mask_params;
found_mask_params = Some((params.x, params.y)); break;
}
}
}
if let Some((center_x, center_y)) = found_mask_params {
for (mut vis_transform, _) in visualizers.iter_mut() {
vis_transform.translation.x = center_x;
vis_transform.translation.y = center_y;
}
}
}
}