use crate::app_state::overworld::{OverworldEntity, character};
use crate::core::animation::components::SpriteAnimationClip;
use crate::core::camera::components::Followable;
use crate::core::collision::Rect2DCollider;
use bevy::asset::{AssetServer, Assets};
use bevy::log::info;
use bevy::prelude::{
Added, Camera, Commands, Component, Entity, Name, Query, Res, ResMut, Sprite, Transform, Vec2,
Visibility, Window, With, Without,
};
use bevy_ecs_tiled::prelude::{
TiledLayer, TiledMap, TiledMapAsset, TiledMapLayerZOffset, TiledObject, TilemapAnchor, tiled,
};
#[derive(Component)]
pub struct TilemapCollider;
#[derive(Component)]
pub struct CollisionTileGroup;
#[derive(Component)]
pub struct ObjectCollisionGroup;
pub fn setup_tilemap_system(
mut commands: Commands,
asset_server: Res<AssetServer>,
souprune_config: Res<crate::config::SoupruneConfig>,
) {
commands.spawn((
OverworldEntity(),
TiledMap(asset_server.load(&souprune_config.game.initial_map_path)),
TilemapAnchor::Center,
TiledMapLayerZOffset(souprune_config.render.z_layer_tilemap),
));
}
pub fn initialize_tilemap_system(
mut commands: Commands,
layers_query: Query<(Entity, &Name), Added<TiledLayer>>,
souprune_config: Res<crate::config::SoupruneConfig>,
) {
let mut layers: Vec<_> = layers_query.iter().collect();
layers.sort_by_key(|(entity, _)| entity.index());
let hidden_keywords = &souprune_config.game.hidden_layer_keywords;
for (index, (layer_entity, layer_name)) in layers.iter().enumerate() {
let layer_name_str = layer_name.as_str();
let name_lower = layer_name_str.to_ascii_lowercase();
if hidden_keywords.iter().any(|s| name_lower.contains(s)) {
if name_lower.contains("collision") {
info!(
"Hide collision layer: {} and generate collision tiles",
layer_name_str
);
} else {
info!("Hide prototype layer: {}", layer_name_str);
}
commands.entity(*layer_entity).insert(Visibility::Hidden);
} else {
info!("Show layers: {}", layer_name_str);
let z_offset = souprune_config.render.z_layer_base
- (layers.len() as f32 - 1.0 - index as f32) * souprune_config.render.z_layer_step;
commands
.entity(*layer_entity)
.insert(Transform::from_xyz(0.0, 0.0, z_offset));
info!(
"Set the Z-axis position of layer {} (order {}): {}",
layer_name_str, index, z_offset
);
}
}
}
type CollisionLayersQuery<'w, 's> = Query<
'w,
's,
(Entity, &'static Name, &'static TiledLayer),
(Added<TiledLayer>, With<Visibility>),
>;
pub fn generate_collision_tiles_system(
mut commands: Commands,
tiled_map_assets: Res<Assets<TiledMapAsset>>,
tiled_maps_query: Query<(Entity, &TiledMap)>,
collision_layers: CollisionLayersQuery,
existing_object_colliders: Query<&crate::app_state::overworld::tilemap::ObjectCollider>,
collision_tile_group: Query<Entity, With<CollisionTileGroup>>,
object_collision_group: Query<Entity, With<ObjectCollisionGroup>>,
) {
let collision_tile_group_entity = if let Ok(entity) = collision_tile_group.single() {
entity
} else {
commands
.spawn((
OverworldEntity(),
CollisionTileGroup,
Name::new("CollisionTiles"),
Transform::default(),
Visibility::default(),
))
.id()
};
let object_collision_group_entity = if let Ok(entity) = object_collision_group.single() {
entity
} else {
commands
.spawn((
OverworldEntity(),
ObjectCollisionGroup,
Name::new("ObjectCollisions"),
Transform::default(),
Visibility::default(),
))
.id()
};
for (layer_entity, layer_name, _tiled_layer) in collision_layers.iter() {
if !is_collision_layer(layer_name.as_str()) {
continue;
}
if let Some((tiled_map_asset, matching_layer)) =
find_matching_layer(&tiled_maps_query, &tiled_map_assets, layer_name.as_str())
{
generate_tiles_for_layer(
&mut commands,
tiled_map_asset,
&matching_layer,
collision_tile_group_entity,
);
if existing_object_colliders.is_empty() {
generate_object_colliders(
&mut commands,
tiled_map_asset,
object_collision_group_entity,
);
}
}
commands.entity(layer_entity).insert(TilemapCollider);
}
}
fn is_collision_layer(layer_name: &str) -> bool {
layer_name.to_ascii_lowercase().contains("collision")
}
fn find_matching_layer<'a>(
tiled_maps_query: &Query<(Entity, &TiledMap)>,
tiled_map_assets: &'a Res<Assets<TiledMapAsset>>,
layer_name: &str,
) -> Option<(&'a TiledMapAsset, tiled::Layer<'a>)> {
for (_map_entity, tiled_map_handle) in tiled_maps_query.iter() {
let tiled_map_asset = tiled_map_assets.get(&tiled_map_handle.0)?;
for layer in tiled_map_asset.map.layers() {
if is_layer_match(&layer.name, layer_name) {
return Some((tiled_map_asset, layer));
}
}
}
None
}
fn is_layer_match(layer_name: &str, target_name: &str) -> bool {
layer_name == target_name
|| layer_name.to_ascii_lowercase().contains("collision")
|| target_name.contains(layer_name)
}
fn generate_tiles_for_layer(
commands: &mut Commands,
tiled_map_asset: &TiledMapAsset,
layer: &tiled::Layer,
parent_entity: Entity,
) {
let Some(tile_layer) = layer.as_tile_layer() else {
return;
};
let tile_size = tiled_map_asset.map.tile_width as f32;
let tile_height = tiled_map_asset.map.tile_height as f32;
let map_width = tiled_map_asset.map.width as f32 * tile_size;
let map_height = tiled_map_asset.map.height as f32 * tile_height;
let center_offset_x = -map_width / 2.0;
let center_offset_y = -map_height / 2.0;
tiled_map_asset.for_each_tile(
&tile_layer,
|layer_tile, _tile_data, tile_pos, _chunk_pos| {
if layer_tile.get_tile().is_some() {
let world_x = center_offset_x + (tile_pos.x as f32 * tile_size) + (tile_size / 2.0);
let world_y =
center_offset_y + (tile_pos.y as f32 * tile_height) + (tile_height / 2.0);
commands.entity(parent_entity).with_children(|parent| {
parent.spawn((
TilemapCollider,
Rect2DCollider::new(Vec2::new(tile_size, tile_height), Vec2::ZERO),
Transform::from_xyz(world_x, world_y, 0.0),
Name::new(format!("CollisionTile({},{})", tile_pos.x, tile_pos.y)),
));
});
}
},
);
}
fn generate_object_colliders(
commands: &mut Commands,
tiled_map_asset: &TiledMapAsset,
parent_entity: Entity,
) {
let tile_size = tiled_map_asset.map.tile_width as f32;
let tile_height = tiled_map_asset.map.tile_height as f32;
let map_width = tiled_map_asset.map.width as f32 * tile_size;
let map_height = tiled_map_asset.map.height as f32 * tile_height;
let center_offset_x = -map_width / 2.0;
let center_offset_y = -map_height / 2.0;
for layer in tiled_map_asset.map.layers() {
if let Some(object_layer) = layer.as_object_layer() {
info!(
"Processing object layer '{}' with {} objects",
layer.name,
object_layer.objects().count()
);
for object_data in object_layer.objects() {
let tiled::ObjectShape::Rect { width, height } = object_data.shape else {
continue;
};
let world_x = center_offset_x + object_data.x + width / 2.0;
let world_y = center_offset_y
+ (tiled_map_asset.map.height as f32 * tile_height
- object_data.y
- height / 2.0);
let size = Vec2::new(width, height);
if let Some(collision_value) = object_data.properties.get("collision")
&& let tiled::PropertyValue::BoolValue(true) = collision_value
{
info!(
"Creating collision object '{}' at world pos ({}, {}) with size ({}, {})",
object_data.name, world_x, world_y, width, height
);
commands.entity(parent_entity).with_children(|parent| {
parent.spawn((
crate::app_state::overworld::tilemap::ObjectCollider,
TilemapCollider,
Rect2DCollider::new(size, Vec2::ZERO),
Transform::from_xyz(world_x, world_y, 0.0),
Visibility::Hidden,
Name::new(format!("ObjectCollision_{}", object_data.name)),
));
});
}
#[cfg(feature = "experimental")]
if let Some(trigger_value) = object_data.properties.get("trigger")
&& let tiled::PropertyValue::BoolValue(true) = trigger_value
{
let trigger_id = if let Some(id_value) =
object_data.properties.get("trigger_id")
&& let tiled::PropertyValue::StringValue(id) = id_value
{
id.clone()
} else if !object_data.name.is_empty() {
object_data.name.clone()
} else {
format!("trigger_{}", object_data.id())
};
info!(
"Creating trigger zone '{}' at world pos ({}, {}) with size ({}, {})",
trigger_id, world_x, world_y, width, height
);
commands.entity(parent_entity).with_children(|parent| {
parent.spawn((
crate::app_state::overworld::trigger::TriggerZone::new(&trigger_id),
Rect2DCollider::new(size, Vec2::ZERO),
Transform::from_xyz(world_x, world_y, 0.0),
Visibility::Hidden,
Name::new(format!("TriggerZone_{}", trigger_id)),
));
});
}
}
}
}
}
type ObjectsQuery<'w, 's> = Query<
'w,
's,
&'static mut Transform,
(
With<TiledObject>,
Without<character::components::PlayerControlled>,
),
>;
type PlayerQuery<'w, 's> = Query<
'w,
's,
(
&'static Transform,
&'static SpriteAnimationClip,
&'static Sprite,
),
(
With<character::components::PlayerControlled>,
Without<TiledObject>,
),
>;
type ObjectLayersQuery<'w, 's> = Query<
'w,
's,
&'static Transform,
(
With<TiledLayer>,
Without<TiledObject>,
Without<character::components::PlayerControlled>,
),
>;
pub fn update_objects_order_with_player_system(
mut objects: ObjectsQuery,
player: PlayerQuery,
object_layers: ObjectLayersQuery,
) {
let (player_transform, player_anim, current_sprite) = if let Ok(result) = player.single() {
result
} else {
return;
};
let player_y = if let Some(rect) = current_sprite.rect {
player_transform.translation.y - rect.height() / 2.0
} else {
let anim_sprite = player_anim.get_current_sprite();
if let Some(rect) = anim_sprite.rect {
player_transform.translation.y - rect.height() / 2.0
} else {
player_transform.translation.y - 16.0
}
};
let object_layer_z = object_layers
.iter()
.map(|transform| transform.translation.z)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(-2.0);
for mut transform in objects.iter_mut() {
if transform.translation.y > player_y {
transform.translation.z = -1.0 - object_layer_z;
} else {
transform.translation.z = 1.0 - object_layer_z;
}
}
}
pub fn setup_camera_bounds_system(
mut followable_cameras: Query<&mut Followable, With<Camera>>,
tiled_map_assets: Res<Assets<TiledMapAsset>>,
tiled_maps_query: Query<&TiledMap>,
windows: Query<&Window>,
cameras: Query<&Transform, (With<Camera>, Without<Followable>)>,
souprune_config: Res<crate::config::SoupruneConfig>,
) {
let Ok(tiled_map_handle) = tiled_maps_query.single() else {
return;
};
let Some(tiled_map_asset) = tiled_map_assets.get(&tiled_map_handle.0) else {
return;
};
let tile_width = tiled_map_asset.map.tile_width as f32;
let tile_height = tiled_map_asset.map.tile_height as f32;
let map_width = tiled_map_asset.map.width as f32 * tile_width;
let map_height = tiled_map_asset.map.height as f32 * tile_height;
let default_width = souprune_config.render.base_resolution_width as f32;
let default_height = souprune_config.render.base_resolution_height as f32;
let viewport_width = if let Ok(window) = windows.single() {
if let Ok(camera_transform) = cameras.single() {
let scale = camera_transform.scale.x;
window.resolution.width() * scale
} else {
default_width
}
} else {
default_width
};
let viewport_height = if let Ok(window) = windows.single() {
if let Ok(camera_transform) = cameras.single() {
let scale = camera_transform.scale.y;
window.resolution.height() * scale
} else {
default_height
}
} else {
default_height
};
let half_viewport_width = viewport_width / 2.0;
let half_viewport_height = viewport_height / 2.0;
let min_x = (-map_width / 2.0) + half_viewport_width;
let max_x = (map_width / 2.0) - half_viewport_width;
let min_y = (-map_height / 2.0) + half_viewport_height;
let max_y = (map_height / 2.0) - half_viewport_height;
for mut followable in followable_cameras.iter_mut() {
if !followable.bounds_enabled {
followable.enable_bounds(min_x, max_x, min_y, max_y);
info!(
"Enabled camera bounds: X({:.1}, {:.1}), Y({:.1}, {:.1}) for viewport {}x{} on map {}x{}",
min_x, max_x, min_y, max_y, viewport_width, viewport_height, map_width, map_height
);
}
}
}
#[cfg(all(feature = "bevy_kira_audio", not(feature = "firewheel")))]
pub fn update_map_bgm_system(
mut current_bgm: ResMut<super::CurrentMapBgm>,
mut bgm_handle: ResMut<super::CurrentBgmHandle>,
tiled_maps: Query<&TiledMap>,
tiled_map_assets: Res<Assets<TiledMapAsset>>,
audio: Res<bevy_kira_audio::Audio>,
mut audio_instances: ResMut<Assets<bevy_kira_audio::AudioInstance>>,
asset_server: Res<AssetServer>,
) {
for tiled_map in tiled_maps.iter() {
if let Some(map_asset) = tiled_map_assets.get(&tiled_map.0)
&& let Some(bgm_prop) = map_asset.map.properties.get("bgm")
&& let tiled::PropertyValue::StringValue(bgm_path) = bgm_prop
&& current_bgm.0.as_deref() != Some(bgm_path)
{
if let Some(handle) = &bgm_handle.0
&& let Some(instance) = audio_instances.get_mut(handle)
{
instance.stop(bevy_kira_audio::AudioTween::default());
}
info!("Switching BGM to: {}", bgm_path);
let handle = crate::core::audio::play_bgm(&audio, &asset_server, bgm_path);
current_bgm.0 = Some(bgm_path.clone());
bgm_handle.0 = Some(handle);
}
}
}
#[cfg(feature = "firewheel")]
pub fn update_map_bgm_system(
mut commands: Commands,
mut current_bgm: ResMut<super::CurrentMapBgm>,
mut bgm_handle: ResMut<super::CurrentBgmHandle>,
tiled_maps: Query<&TiledMap>,
tiled_map_assets: Res<Assets<TiledMapAsset>>,
asset_server: Res<AssetServer>,
) {
for tiled_map in tiled_maps.iter() {
if let Some(map_asset) = tiled_map_assets.get(&tiled_map.0)
&& let Some(bgm_prop) = map_asset.map.properties.get("bgm")
&& let tiled::PropertyValue::StringValue(bgm_path) = bgm_prop
&& current_bgm.0.as_deref() != Some(bgm_path)
{
if let Some(entity) = bgm_handle.0 {
commands.entity(entity).despawn();
}
info!("Switching BGM to: {}", bgm_path);
let entity = crate::core::audio::play_bgm(&mut commands, &asset_server, bgm_path);
current_bgm.0 = Some(bgm_path.clone());
bgm_handle.0 = Some(entity);
}
}
}