diff --git a/Cargo.lock b/Cargo.lock index 4a8f430..9fe7278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1299,7 +1299,7 @@ dependencies = [ [[package]] name = "bevy_map" -version = "0.3.1" +version = "0.5.0" dependencies = [ "bevy", "bevy_map_animation", @@ -1313,7 +1313,7 @@ dependencies = [ [[package]] name = "bevy_map_animation" -version = "0.3.1" +version = "0.5.0" dependencies = [ "bevy", "serde", @@ -1323,7 +1323,7 @@ dependencies = [ [[package]] name = "bevy_map_autotile" -version = "0.3.1" +version = "0.5.0" dependencies = [ "bevy_map_core", "log", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "bevy_map_codegen" -version = "0.3.1" +version = "0.5.0" dependencies = [ "bevy_map_core", "bevy_map_schema", @@ -1349,7 +1349,7 @@ dependencies = [ [[package]] name = "bevy_map_core" -version = "0.3.1" +version = "0.5.0" dependencies = [ "bevy", "bevy_map_animation", @@ -1361,7 +1361,7 @@ dependencies = [ [[package]] name = "bevy_map_derive" -version = "0.3.1" +version = "0.5.0" dependencies = [ "proc-macro2", "quote", @@ -1370,7 +1370,7 @@ dependencies = [ [[package]] name = "bevy_map_dialogue" -version = "0.3.1" +version = "0.5.0" dependencies = [ "bevy", "serde", @@ -1380,7 +1380,7 @@ dependencies = [ [[package]] name = "bevy_map_editor" -version = "0.3.1" +version = "0.5.0" dependencies = [ "bevy", "bevy_ecs_tilemap", @@ -1416,7 +1416,7 @@ dependencies = [ [[package]] name = "bevy_map_runtime" -version = "0.3.1" +version = "0.5.0" dependencies = [ "avian2d", "bevy", @@ -1433,7 +1433,7 @@ dependencies = [ [[package]] name = "bevy_map_schema" -version = "0.3.1" +version = "0.5.0" dependencies = [ "bevy_map_core", "serde", diff --git a/crates/bevy_map_core/src/entity_type_config.rs b/crates/bevy_map_core/src/entity_type_config.rs index 9ba7d70..b252e95 100644 --- a/crates/bevy_map_core/src/entity_type_config.rs +++ b/crates/bevy_map_core/src/entity_type_config.rs @@ -491,7 +491,6 @@ mod tests { }; let json = serde_json::to_string_pretty(&config).unwrap(); - println!("Serialized config:\n{}", json); let deserialized: EntityTypeConfig = serde_json::from_str(&json).unwrap(); assert!(deserialized.physics.is_some()); diff --git a/crates/bevy_map_core/src/level.rs b/crates/bevy_map_core/src/level.rs index be41955..d3a119b 100644 --- a/crates/bevy_map_core/src/level.rs +++ b/crates/bevy_map_core/src/level.rs @@ -11,6 +11,7 @@ pub struct Level { pub name: String, pub width: u32, pub height: u32, + pub z_height: f32, pub layers: Vec, pub entities: Vec, /// World X position in pixels (for world view) @@ -32,6 +33,7 @@ impl Level { name, width, height, + z_height: 0.1, // Default z-height layers: Vec::new(), entities: Vec::new(), world_x: 0, @@ -47,6 +49,7 @@ impl Level { name, width, height, + z_height: 0.1, // Default z-height layers: Vec::new(), entities: Vec::new(), world_x, diff --git a/crates/bevy_map_core/src/lib.rs b/crates/bevy_map_core/src/lib.rs index 59d86d2..f17cfac 100644 --- a/crates/bevy_map_core/src/lib.rs +++ b/crates/bevy_map_core/src/lib.rs @@ -14,6 +14,7 @@ mod entity; mod entity_type_config; mod layer; mod level; +mod physics_layers; mod project; mod tileset; mod value; @@ -32,6 +33,7 @@ pub use layer::{ LayerData, LayerType, OCCUPIED_CELL, TILE_FLIP_MASK, TILE_FLIP_X, TILE_FLIP_Y, TILE_INDEX_MASK, }; pub use level::Level; +pub use physics_layers::{PhysicsLayerSet, PhysicsLayers}; pub use project::{EditorProject, MapProject, MapProjectBuilder}; pub use tileset::{TileProperties, Tileset, TilesetImage}; pub use value::Value; diff --git a/crates/bevy_map_core/src/physics_layers.rs b/crates/bevy_map_core/src/physics_layers.rs new file mode 100644 index 0000000..5ba4741 --- /dev/null +++ b/crates/bevy_map_core/src/physics_layers.rs @@ -0,0 +1,126 @@ +//! Physics Layers Config for Bevy Map Editor + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use uuid::Uuid; + +use crate::{CollisionData, CollisionShape, OneWayDirection}; + +/// Holds all physics layers for a tileset, allowing multiple physics configurations per tileset +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct PhysicsLayers { + /// All physics layers in the tileset + pub layers: Vec, +} + +impl PhysicsLayers { + pub fn new() -> Self { + Self { layers: Vec::new() } + } + + /// Add a physics layer set + pub fn add_physics_layer(&mut self, physics_layer: PhysicsLayerSet) { + self.layers.push(physics_layer); + } + + /// Get physics layer set by ID + pub fn get_physics_layer(&self, id: Uuid) -> Option<&PhysicsLayerSet> { + self.layers.iter().find(|ts| ts.id == id) + } + + /// Get mutable physics layer set by ID + pub fn get_physics_layer_mut(&mut self, id: Uuid) -> Option<&mut PhysicsLayerSet> { + self.layers.iter_mut().find(|ts| ts.id == id) + } + + /// Remove physics layer set by ID + pub fn remove_physics_layer(&mut self, id: Uuid) -> Option { + if let Some(pos) = self.layers.iter().position(|ts| ts.id == id) { + Some(self.layers.remove(pos)) + } else { + None + } + } +} + +/// A physics layer set attached to a tileset +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PhysicsLayerSet { + pub id: Uuid, + pub name: String, + + #[serde(default)] + pub debug_color: [u8; 3], + + pub layer: u8, + pub mask: u32, + + /// Physics assignments for each tile (tile_index -> CollisionData) + pub tile_physics: HashMap, +} + +impl PhysicsLayerSet { + /// Create a new physics layer set + pub fn new(name: String, layer: u8, mask: u32, debug_color: [u8; 3]) -> Self { + Self { + id: Uuid::new_v4(), + name, + debug_color, + layer, + mask, + tile_physics: HashMap::new(), + } + } + + /// Assign collision data to a tile index + pub fn assign_tile_physics(&mut self, tile_index: u32, collision: CollisionData) { + self.tile_physics.insert(tile_index, collision); + } + + /// Get collision data for a tile index + pub fn get_tile_physics(&self, tile_index: u32) -> Option<&CollisionData> { + self.tile_physics.get(&tile_index) + } + + /// Get collision data for a tile index + pub fn get_tile_physics_mut(&mut self, tile_index: u32) -> Option<&mut CollisionData> { + self.tile_physics.get_mut(&tile_index) + } + + pub fn set_tile_physics_shape(&mut self, tile_index: u32, shape: CollisionShape) { + if let Some(collision_data) = self.tile_physics.get_mut(&tile_index) { + collision_data.shape = shape; + } else { + self.tile_physics.insert( + tile_index, + CollisionData { + shape, + body_type: Default::default(), + one_way: Default::default(), + layer: self.layer, + mask: self.mask, + }, + ); + } + } + + pub fn set_tile_physics_one_way(&mut self, tile_index: u32, direction: OneWayDirection) { + if let Some(collision_data) = self.tile_physics.get_mut(&tile_index) { + collision_data.one_way = direction; + } + } + + pub fn update_all_tile_physics_layer(&mut self, layer: u8) { + self.layer = layer; + for collision_data in self.tile_physics.values_mut() { + collision_data.layer = layer; + } + } + + pub fn update_all_tile_physics_mask(&mut self, mask: u32) { + self.mask = mask; + for collision_data in self.tile_physics.values_mut() { + collision_data.mask = mask; + } + } +} diff --git a/crates/bevy_map_core/src/tileset.rs b/crates/bevy_map_core/src/tileset.rs index 016082d..49540e4 100644 --- a/crates/bevy_map_core/src/tileset.rs +++ b/crates/bevy_map_core/src/tileset.rs @@ -1,6 +1,7 @@ //! Tileset configuration with multi-image support use crate::collision::{CollisionData, CollisionShape, OneWayDirection}; +use crate::physics_layers::PhysicsLayers; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::HashMap; use uuid::Uuid; @@ -216,6 +217,9 @@ pub struct Tileset { /// Multiple image sources #[serde(default)] pub images: Vec, + /// Collision layers configuration + #[serde(default)] + pub physics_layers: PhysicsLayers, /// Per-tile properties (collision, animation, custom data) #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub tile_properties: HashMap, @@ -239,6 +243,7 @@ impl Tileset { name, tile_size, images: vec![image], + physics_layers: PhysicsLayers::new(), tile_properties: HashMap::new(), path: Some(path), columns, @@ -253,6 +258,7 @@ impl Tileset { name, tile_size, images: Vec::new(), + physics_layers: PhysicsLayers::new(), tile_properties: HashMap::new(), path: None, columns: 0, @@ -280,76 +286,96 @@ impl Tileset { } /// Check if a tile has collision - pub fn tile_has_collision(&self, tile_index: u32) -> bool { - self.tile_properties - .get(&tile_index) - .map(|p| p.has_collision()) - .unwrap_or(false) + pub fn tile_has_collision(&self, tile_index: u32, physics_layer: Uuid) -> bool { + if let Some(physics_layer_set) = self.physics_layers.get_physics_layer(physics_layer) { + physics_layer_set + .tile_physics + .get(&tile_index) + .map(|p| p.has_collision()) + .unwrap_or(false) + } else { + false + } } /// Get collision data for a tile - pub fn get_tile_collision(&self, tile_index: u32) -> Option<&CollisionData> { - self.tile_properties.get(&tile_index).map(|p| &p.collision) + pub fn get_tile_collision( + &self, + tile_index: u32, + physics_layer: Uuid, + ) -> Option<&CollisionData> { + if let Some(physics_layer_set) = self.physics_layers.get_physics_layer(physics_layer) { + physics_layer_set.tile_physics.get(&tile_index) + } else { + None + } } /// Set collision data for a tile - pub fn set_tile_collision(&mut self, tile_index: u32, collision: CollisionData) { - let props = self.get_tile_properties_mut(tile_index); - props.collision = collision; - // Clean up if properties are now empty - if props.is_empty() { - self.tile_properties.remove(&tile_index); + pub fn set_tile_collision( + &mut self, + tile_index: u32, + collision: CollisionData, + physics_layer: Uuid, + ) { + if let Some(physics_layer_set) = self.physics_layers.get_physics_layer_mut(physics_layer) { + physics_layer_set.tile_physics.insert(tile_index, collision); } } /// Set full collision for a tile (convenience method) - pub fn set_tile_full_collision(&mut self, tile_index: u32, has_collision: bool) { - let collision = if has_collision { - CollisionData::full() - } else { - CollisionData::none() - }; - self.set_tile_collision(tile_index, collision); + pub fn set_tile_full_collision( + &mut self, + tile_index: u32, + has_collision: bool, + physics_layer: Uuid, + ) { + if let Some(physics_layer_set) = self.physics_layers.get_physics_layer_mut(physics_layer) { + let collision = if has_collision { + CollisionData::full() + } else { + CollisionData::none() + }; + physics_layer_set.tile_physics.insert(tile_index, collision); + } } /// Set collision shape for a tile (preserving other collision properties) - pub fn set_tile_collision_shape(&mut self, tile_index: u32, shape: CollisionShape) { - let props = self.get_tile_properties_mut(tile_index); - props.collision.shape = shape; - // Clean up if properties are now empty - if props.is_empty() { - self.tile_properties.remove(&tile_index); - } + pub fn set_tile_collision_shape( + &mut self, + tile_index: u32, + shape: CollisionShape, + physics_layer: Uuid, + ) { + if let Some(physics_layer_set) = self.physics_layers.get_physics_layer_mut(physics_layer) { + physics_layer_set.set_tile_physics_shape(tile_index, shape); + }; } /// Set one-way direction for a tile collision - pub fn set_tile_one_way(&mut self, tile_index: u32, direction: OneWayDirection) { - let props = self.get_tile_properties_mut(tile_index); - props.collision.one_way = direction; - // Clean up if properties are now empty - if props.is_empty() { - self.tile_properties.remove(&tile_index); - } + pub fn set_tile_one_way( + &mut self, + tile_index: u32, + direction: OneWayDirection, + physics_layer: Uuid, + ) { + if let Some(physics_layer_set) = self.physics_layers.get_physics_layer_mut(physics_layer) { + physics_layer_set.set_tile_physics_one_way(tile_index, direction); + }; } /// Set collision layer for a tile - pub fn set_tile_collision_layer(&mut self, tile_index: u32, layer: u8) { - let props = self.get_tile_properties_mut(tile_index); - props.collision.layer = layer; - // Clean up if properties are now empty - if props.is_empty() { - self.tile_properties.remove(&tile_index); - } + pub fn set_tile_collision_layer(&mut self, _tile_index: u32, layer: u8, physics_layer: Uuid) { + if let Some(physics_layer_set) = self.physics_layers.get_physics_layer_mut(physics_layer) { + physics_layer_set.update_all_tile_physics_layer(layer); + }; } /// Set collision mask for a tile - pub fn set_tile_collision_mask(&mut self, tile_index: u32, mask: u32) { - let props = self.get_tile_properties_mut(tile_index); - props.collision.mask = mask; - // Clean up if properties are now empty - if props.is_empty() { - self.tile_properties.remove(&tile_index); - } + pub fn set_tile_collision_mask(&mut self, _tile_index: u32, mask: u32, physics_layer: Uuid) { + if let Some(physics_layer_set) = self.physics_layers.get_physics_layer_mut(physics_layer) { + physics_layer_set.update_all_tile_physics_mask(mask); + }; } /// Migrate legacy single-image format to multi-image format diff --git a/crates/bevy_map_editor/src/lib.rs b/crates/bevy_map_editor/src/lib.rs index c7e9142..cfc7360 100644 --- a/crates/bevy_map_editor/src/lib.rs +++ b/crates/bevy_map_editor/src/lib.rs @@ -588,6 +588,13 @@ pub struct EditorState { pub show_tileset_editor: bool, pub tileset_editor_state: TilesetEditorState, + // Physics Layer Editor + pub show_add_physics_layer_set_dialog: bool, + pub new_physics_layer_name: String, + pub new_physics_layer_mask: u32, + pub new_physics_layer_layer: u8, + pub new_physics_layer_debug_color: Color, + // SpriteSheet Editor (for spritesheet setup: image loading, grid config) pub show_spritesheet_editor: bool, pub spritesheet_editor_state: SpriteSheetEditorState, @@ -706,7 +713,7 @@ impl Default for EditorState { current_tool: EditorTool::Select, tool_mode: ToolMode::Point, show_grid: true, - show_collisions: false, + show_collisions: true, snap_to_grid: true, zoom: 1.0, camera_offset: bevy::math::Vec2::ZERO, @@ -769,6 +776,12 @@ impl Default for EditorState { show_tileset_editor: false, tileset_editor_state: TilesetEditorState::default(), + show_add_physics_layer_set_dialog: false, + new_physics_layer_name: String::new(), + new_physics_layer_mask: 0, + new_physics_layer_layer: 0, + new_physics_layer_debug_color: Color::srgba(0.2, 0.6, 1.0, 0.3), + show_spritesheet_editor: false, spritesheet_editor_state: SpriteSheetEditorState::new(), diff --git a/crates/bevy_map_editor/src/render/mod.rs b/crates/bevy_map_editor/src/render/mod.rs index 7d833b2..ef64cf6 100644 --- a/crates/bevy_map_editor/src/render/mod.rs +++ b/crates/bevy_map_editor/src/render/mod.rs @@ -394,7 +394,7 @@ fn spawn_level_tilemaps( // Z-offset: layer_index * 0.1 + image_index * 0.01 // This ensures proper ordering: all images in layer 0 render before layer 1 - let layer_z = layer_index as f32 * 0.1 + image_index as f32 * 0.01; + let layer_z = level.z_height * layer_index as f32 + image_index as f32 * 0.01; // Insert TilemapBundle first (which includes Visibility internally) // Use BottomLeft anchor so tiles at (0,0) start at world origin @@ -474,7 +474,7 @@ fn spawn_level_tilemaps( let world_y = y as f32 * tile_size_f32 + origin_y as f32; // Z-offset slightly above regular tiles in same layer - let layer_z = layer_index as f32 * 0.1 + image_index as f32 * 0.01 + 0.001; + let layer_z = layer_index as f32 * level.z_height + image_index as f32 * 0.01 + 0.001; let sprite_entity = commands .spawn(( @@ -624,7 +624,8 @@ pub fn update_tile( // World position: place sprite so origin aligns with grid cell corner let world_x = x as f32 * tile_size_f32 + origin_x as f32; let world_y = y as f32 * tile_size_f32 + origin_y as f32; - let layer_z = layer_index as f32 * 0.1 + image_index as f32 * 0.01 + 0.001; + let layer_z = + layer_index as f32 * level.z_height + image_index as f32 * 0.01 + 0.001; let sprite_entity = commands .spawn(( @@ -694,7 +695,8 @@ pub fn update_tile( let tile_storage = TileStorage::empty(map_size); let tilemap_entity = commands.spawn_empty().id(); - let layer_z = layer_index as f32 * 0.1 + image_index as f32 * 0.01; + let layer_z = + layer_index as f32 * level.z_height + image_index as f32 * 0.01; let layer_visible = layer.visible; commands.entity(tilemap_entity).insert(( @@ -907,8 +909,6 @@ fn sync_collision_rendering( return; }; - let collision_color = Color::srgba(0.0, 0.6, 1.0, 0.3); - // Iterate through tile layers for (layer_idx, layer) in level.layers.iter().enumerate() { if !layer.visible { @@ -932,19 +932,30 @@ fn sync_collision_rendering( let idx = (y * level.width + x) as usize; if let Some(&Some(tile_index)) = tiles.get(idx) { // Check if this tile has collision - if let Some(props) = tileset.get_tile_properties(tile_index) { - if props.collision.has_collision() { - // Spawn collision overlay sprite(s) - spawn_collision_overlay( - &mut commands, - &mut cache, - &props.collision.shape, - x, - y, - tile_size, - layer_idx, - collision_color, - ); + + for physics_layer in tileset.physics_layers.layers.iter() { + if let Some(collision) = + tileset.get_tile_collision(tile_index, physics_layer.id) + { + if collision.has_collision() { + let color = Color::srgba_u8( + physics_layer.debug_color[0], + physics_layer.debug_color[1], + physics_layer.debug_color[2], + 200, // Alpha value + ); + // Spawn collision overlay sprite(s) + spawn_collision_overlay( + &mut commands, + &mut cache, + &collision.shape, + x, + y, + tile_size, + layer_idx, + color, + ); + } } } } diff --git a/crates/bevy_map_editor/src/ui/mod.rs b/crates/bevy_map_editor/src/ui/mod.rs index 6e0b188..e1ffca7 100644 --- a/crates/bevy_map_editor/src/ui/mod.rs +++ b/crates/bevy_map_editor/src/ui/mod.rs @@ -1421,6 +1421,9 @@ fn render_ui( terrain::render_new_terrain_set_dialog(ctx, &mut editor_state, &mut project); terrain::render_add_terrain_to_set_dialog(ctx, &mut editor_state, &mut project); + // Physics dialogs + tileset_editor::render_new_physics_layer_set_dialog(ctx, &mut editor_state, &mut project); + // New level dialog (for World View) if let Some(params) = render_new_level_dialog(ctx, &mut editor_state) { let new_level = bevy_map_core::Level::new_at( diff --git a/crates/bevy_map_editor/src/ui/tileset_editor.rs b/crates/bevy_map_editor/src/ui/tileset_editor.rs index a881b46..155cc25 100644 --- a/crates/bevy_map_editor/src/ui/tileset_editor.rs +++ b/crates/bevy_map_editor/src/ui/tileset_editor.rs @@ -8,9 +8,11 @@ //! - Clicking assigns the selected terrain to that zone of that tile //! - Visual overlays show assigned terrain with curved boundaries +use bevy::color::Color; use bevy_egui::egui::{self, Color32, Pos2, Shape}; use bevy_map_autotile::terrain::Color as TerrainColor; use bevy_map_autotile::TerrainSetType; +use bevy_map_core::PhysicsLayerSet; use std::f32::consts::PI; use super::{find_base_tile_for_position, EditorTheme, TilesetTextureCache}; @@ -525,6 +527,7 @@ pub struct CollisionDragState { /// State for the collision editor within the tileset editor pub struct CollisionEditorState { + pub selected_physics_layer: Option, /// Currently selected tile for collision editing pub selected_tile: Option, /// Zoom level for the tile preview canvas (default 8.0 = 256px for 32px tiles) @@ -546,6 +549,7 @@ pub struct CollisionEditorState { impl Default for CollisionEditorState { fn default() -> Self { Self { + selected_physics_layer: None, selected_tile: None, preview_zoom: 8.0, grid_zoom: 1.0, @@ -1204,6 +1208,15 @@ fn render_tile_properties_tab( return; }; + let Some(physics_layer_id) = editor_state + .tileset_editor_state + .collision_editor + .selected_physics_layer + else { + ui.label("No physics layer selected"); + return; + }; + // Clone tileset data to avoid borrow conflicts let tileset_data = project .tilesets @@ -1270,7 +1283,11 @@ fn render_tile_properties_tab( if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { - tileset.set_tile_full_collision(tile_idx, has_collision); + tileset.set_tile_full_collision( + tile_idx, + has_collision, + physics_layer_id, + ); project.mark_dirty(); } } @@ -1343,10 +1360,7 @@ fn render_tile_properties_tab( let mut layer = collision_data.layer; ui.horizontal(|ui| { ui.label("Layer:"); - if ui - .add(egui::DragValue::new(&mut layer).range(0..=31)) - .changed() - { + if ui.add(egui::DragValue::new(&mut layer)).changed() { if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { @@ -1361,10 +1375,7 @@ fn render_tile_properties_tab( let mut mask = collision_data.mask; ui.horizontal(|ui| { ui.label("Mask:"); - if ui - .add(egui::DragValue::new(&mut mask).range(0..=31)) - .changed() - { + if ui.add(egui::DragValue::new(&mut mask)).changed() { if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { @@ -1955,6 +1966,51 @@ fn render_collision_tab( return; } + egui::SidePanel::left("Physics_list_panel") + .resizable(true) + .show_inside(ui, |ui| { + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.heading("Physics Layer Sets"); + + // List terrain sets for this tileset + let physics_layers = project + .tilesets + .iter() + .find(|t| t.id == tileset_id) + .map(|t| &t.physics_layers); + + if let Some(physics_layers) = physics_layers { + for p in physics_layers.layers.iter() { + ui.horizontal(|ui| { + let selected = editor_state + .tileset_editor_state + .collision_editor + .selected_physics_layer + == Some(p.id); + if ui.selectable_label(selected, &p.name).clicked() { + editor_state + .tileset_editor_state + .collision_editor + .selected_physics_layer = Some(p.id); + } + ui.small(format!("Layer: {} Mask: {}", p.layer, p.mask)); + }); + } + } + + ui.separator(); + + ui.horizontal(|ui| { + if ui.button("+ New Set").clicked() { + // TODO: Show dialog to create new physics layer set + editor_state.show_add_physics_layer_set_dialog = true; + } + }); + }); + }); + // Three-panel layout with resizable splitters // Left panel: Tile selector grid (resizable) egui::SidePanel::left("collision_tile_list") @@ -2118,6 +2174,26 @@ fn render_collision_tile_selector( let display_size = egui::vec2(tile_size * zoom, tile_size * zoom); let mut virtual_offset = 0u32; + let Some(physics_layer_id) = editor_state + .tileset_editor_state + .collision_editor + .selected_physics_layer + else { + ui.label("No physics layer selected"); + return; + }; + + let Some(physics_color) = project + .tilesets + .iter() + .find(|t| t.id == tileset_id) + .and_then(|t| t.physics_layers.get_physics_layer(physics_layer_id)) + .map(|layer| layer.debug_color) + else { + ui.label("Physics layer not found"); + return; + }; + for image in images { let texture_id = cache .and_then(|c| c.loaded.get(&image.id)) @@ -2147,8 +2223,9 @@ fn render_collision_tile_selector( // Get collision shape for this tile (if any) let collision_shape = tileset - .and_then(|t| t.get_tile_properties(virtual_index)) - .map(|p| p.collision.shape.clone()); + .and_then(|t| t.physics_layers.get_physics_layer(physics_layer_id)) + .and_then(|layer| layer.get_tile_physics(virtual_index)) + .map(|data| data.shape.clone()); let (rect, response) = ui.allocate_exact_size(display_size, egui::Sense::click()); @@ -2192,7 +2269,12 @@ fn render_collision_tile_selector( // Draw collision shape preview on tile if let Some(ref shape) = collision_shape { - draw_collision_shape_on_canvas(ui.painter(), rect, shape); + draw_collision_shape_on_canvas( + ui.painter(), + rect, + shape, + physics_color, + ); } if response.clicked() { @@ -2247,6 +2329,26 @@ fn render_collision_canvas( return; }; + let Some(physics_layer_id) = editor_state + .tileset_editor_state + .collision_editor + .selected_physics_layer + else { + ui.label("No physics layer selected"); + return; + }; + + let Some(physics_color) = project + .tilesets + .iter() + .find(|t| t.id == tileset_id) + .and_then(|t| t.physics_layers.get_physics_layer(physics_layer_id)) + .map(|layer| layer.debug_color) + else { + ui.label("Physics layer not found"); + return; + }; + let zoom = collision_state.preview_zoom; let canvas_size = egui::vec2(tile_size * zoom, tile_size * zoom); @@ -2327,8 +2429,9 @@ fn render_collision_canvas( // 3. Get current collision data (after input handling may have modified it) let tileset = project.tilesets.iter().find(|t| t.id == tileset_id); let collision_data = tileset - .and_then(|t| t.get_tile_properties(tile_idx)) - .map(|p| p.collision.clone()) + .and_then(|t| t.physics_layers.get_physics_layer(physics_layer_id)) + .and_then(|layer| layer.get_tile_physics(tile_idx)) + .cloned() .unwrap_or_default(); // 4. Draw collision shape overlay (skip if dragging vertex - preview handles it) @@ -2343,7 +2446,12 @@ fn render_collision_canvas( }) ); if !is_dragging_vertex { - draw_collision_shape_on_canvas(ui.painter(), canvas_rect, &collision_data.shape); + draw_collision_shape_on_canvas( + ui.painter(), + canvas_rect, + &collision_data.shape, + physics_color, + ); } // 5. Draw drag handles in select mode (skip if dragging vertex - preview handles it) @@ -2372,9 +2480,10 @@ fn draw_collision_shape_on_canvas( painter: &egui::Painter, canvas_rect: egui::Rect, shape: &bevy_map_core::CollisionShape, + color: [u8; 3], ) { - let fill = Color32::from_rgba_unmultiplied(0, 150, 255, 80); - let stroke = egui::Stroke::new(2.0, Color32::from_rgb(0, 120, 255)); + let fill = Color32::from_rgba_unmultiplied(color[0], color[1], color[2], 80); + let stroke = egui::Stroke::new(2.0, Color32::from_rgb(color[0], color[1], color[2])); match shape { bevy_map_core::CollisionShape::None => {} @@ -2484,6 +2593,14 @@ fn handle_collision_canvas_input( .collision_editor .drawing_mode; + let Some(physics_layer_id) = editor_state + .tileset_editor_state + .collision_editor + .selected_physics_layer + else { + return; + }; + // Handle double-click to finish polygon (Polygon mode) if response.double_clicked() && drawing_mode == CollisionDrawMode::Polygon { let polygon_points = &editor_state @@ -2496,7 +2613,7 @@ fn handle_collision_canvas_input( points: polygon_points.clone(), }; if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { - tileset.set_tile_collision_shape(tile_idx, shape); + tileset.set_tile_collision_shape(tile_idx, shape, physics_layer_id); project.mark_dirty(); } editor_state @@ -2586,24 +2703,28 @@ fn handle_collision_canvas_input( // Check if starting drag on a polygon vertex handle if let Some(tileset) = project.tilesets.iter().find(|t| t.id == tileset_id) { - if let Some(props) = tileset.get_tile_properties(tile_idx) { - if let bevy_map_core::CollisionShape::Polygon { points } = - &props.collision.shape - { - if let Some(vertex_idx) = - hit_test_polygon_vertex(canvas_rect, points, pointer_pos, 8.0) - { - editor_state - .tileset_editor_state - .collision_editor - .drag_state = Some(CollisionDragState { - start_pos: normalized, - current_pos: normalized, - operation: CollisionDragOperation::MoveVertex { - index: vertex_idx, - original: points[vertex_idx], - }, - }); + if let Some(physics_layer) = + tileset.physics_layers.get_physics_layer(physics_layer_id) + { + if let Some(props) = physics_layer.tile_physics.get(&tile_idx) { + let shape = &props.shape; + + if let bevy_map_core::CollisionShape::Polygon { points } = &shape { + if let Some(vertex_idx) = + hit_test_polygon_vertex(canvas_rect, points, pointer_pos, 8.0) + { + editor_state + .tileset_editor_state + .collision_editor + .drag_state = Some(CollisionDragState { + start_pos: normalized, + current_pos: normalized, + operation: CollisionDragOperation::MoveVertex { + index: vertex_idx, + original: points[vertex_idx], + }, + }); + } } } } @@ -2660,8 +2781,14 @@ fn handle_collision_canvas_input( if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { - tileset.set_tile_collision_shape(tile_idx, shape); - project.mark_dirty(); + if let Some(physics_layer_id) = editor_state + .tileset_editor_state + .collision_editor + .selected_physics_layer + { + tileset.set_tile_collision_shape(tile_idx, shape, physics_layer_id); + project.mark_dirty(); + } } } } @@ -2678,8 +2805,14 @@ fn handle_collision_canvas_input( if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { - tileset.set_tile_collision_shape(tile_idx, shape); - project.mark_dirty(); + if let Some(physics_layer_id) = editor_state + .tileset_editor_state + .collision_editor + .selected_physics_layer + { + tileset.set_tile_collision_shape(tile_idx, shape, physics_layer_id); + project.mark_dirty(); + } } } } @@ -2693,13 +2826,20 @@ fn handle_collision_canvas_input( // Update the polygon vertex in the tileset if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { - if let Some(props) = tileset.tile_properties.get_mut(&tile_idx) { - if let bevy_map_core::CollisionShape::Polygon { points } = - &mut props.collision.shape - { - if index < points.len() { - points[index] = clamped; - project.mark_dirty(); + if let Some(physics_layer) = tileset + .physics_layers + .get_physics_layer_mut(physics_layer_id) + { + if let Some(props) = physics_layer.tile_physics.get_mut(&tile_idx) { + let mut shape = &mut props.shape; + + if let bevy_map_core::CollisionShape::Polygon { points } = + &mut shape + { + if index < points.len() { + points[index] = clamped; + project.mark_dirty(); + } } } } @@ -2752,10 +2892,22 @@ fn handle_collision_canvas_input( CollisionDragOperation::MoveVertex { index, .. } => { // Draw the polygon with the dragged vertex at its new position if let Some(tileset) = project.tilesets.iter().find(|t| t.id == tileset_id) { - if let Some(props) = tileset.get_tile_properties(tile_idx) { - if let bevy_map_core::CollisionShape::Polygon { points } = - &props.collision.shape - { + let collision_shape = tileset + .physics_layers + .get_physics_layer(physics_layer_id) + .and_then(|layer| layer.get_tile_physics(tile_idx)) + .map(|data| data.shape.clone()); + + let Some(physics_color) = tileset + .physics_layers + .get_physics_layer(physics_layer_id) + .map(|layer| layer.debug_color) + else { + return; + }; + + if let Some(shape) = collision_shape { + if let bevy_map_core::CollisionShape::Polygon { points } = &shape { // Create a temporary copy with the moved vertex let mut preview_points = points.clone(); if *index < preview_points.len() { @@ -2773,6 +2925,7 @@ fn handle_collision_canvas_input( ui.painter(), canvas_rect, &preview_shape, + physics_color, ); draw_collision_handles(ui.painter(), canvas_rect, &preview_shape); } @@ -2789,19 +2942,26 @@ fn handle_collision_canvas_input( let normalized = canvas_point_to_normalized(canvas_rect, pointer_pos); // Check if on existing vertex - let vertex_idx = if let Some(tileset) = - project.tilesets.iter().find(|t| t.id == tileset_id) - { - tileset.get_tile_properties(tile_idx).and_then(|p| { - if let bevy_map_core::CollisionShape::Polygon { points } = &p.collision.shape { - hit_test_polygon_vertex(canvas_rect, points, pointer_pos, 8.0) + let vertex_idx = + if let Some(tileset) = project.tilesets.iter().find(|t| t.id == tileset_id) { + let collision_shape = tileset + .physics_layers + .get_physics_layer(physics_layer_id) + .and_then(|layer| layer.get_tile_physics(tile_idx)) + .map(|data| data.shape.clone()); + + if let Some(shape) = collision_shape { + if let bevy_map_core::CollisionShape::Polygon { points } = &shape { + hit_test_polygon_vertex(canvas_rect, points, pointer_pos, 8.0) + } else { + None + } } else { None } - }) - } else { - None - }; + } else { + None + }; editor_state .tileset_editor_state @@ -2833,13 +2993,8 @@ fn handle_collision_canvas_input( .tilesets .iter() .find(|t| t.id == tileset_id) - .and_then(|t| t.get_tile_properties(tile_idx)) - .map(|p| { - matches!( - p.collision.shape, - bevy_map_core::CollisionShape::Polygon { .. } - ) - }) + .and_then(|t| t.get_tile_collision(tile_idx, physics_layer_id)) + .map(|p| matches!(p.shape, bevy_map_core::CollisionShape::Polygon { .. })) .unwrap_or(false); if is_polygon { @@ -2847,18 +3002,26 @@ fn handle_collision_canvas_input( if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { - if let Some(props) = tileset.tile_properties.get_mut(&tile_idx) { - if let bevy_map_core::CollisionShape::Polygon { points } = - &mut props.collision.shape + if let Some(physics_layer) = tileset + .physics_layers + .get_physics_layer_mut(physics_layer_id) + { + if let Some(props) = + physics_layer.tile_physics.get_mut(&tile_idx) { - let clamped = [ - menu_pos[0].clamp(0.0, 1.0), - menu_pos[1].clamp(0.0, 1.0), - ]; - let insert_idx = - find_best_insertion_index(points, &clamped); - points.insert(insert_idx, clamped); - project.mark_dirty(); + let mut shape = &mut props.shape; + if let bevy_map_core::CollisionShape::Polygon { points } = + &mut shape + { + let clamped = [ + menu_pos[0].clamp(0.0, 1.0), + menu_pos[1].clamp(0.0, 1.0), + ]; + let insert_idx = + find_best_insertion_index(points, &clamped); + points.insert(insert_idx, clamped); + project.mark_dirty(); + } } } } @@ -2961,13 +3124,24 @@ fn render_collision_properties( return; }; + let Some(physics_layer_id) = editor_state + .tileset_editor_state + .collision_editor + .selected_physics_layer + else { + ui.label("No physics layer selected"); + return; + }; + ui.heading("Properties"); // Get current collision data let tileset = project.tilesets.iter().find(|t| t.id == tileset_id); + let collision_data = tileset - .and_then(|t| t.get_tile_properties(tile_idx)) - .map(|p| p.collision.clone()) + .and_then(|t| t.physics_layers.get_physics_layer(physics_layer_id)) + .and_then(|layer| layer.get_tile_physics(tile_idx)) + .cloned() .unwrap_or_default(); // Shape info @@ -3000,7 +3174,7 @@ fn render_collision_properties( if one_way != collision_data.one_way { if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { - tileset.set_tile_one_way(tile_idx, one_way); + tileset.set_tile_one_way(tile_idx, one_way, physics_layer_id); project.mark_dirty(); } } @@ -3009,12 +3183,9 @@ fn render_collision_properties( let mut layer = collision_data.layer; ui.horizontal(|ui| { ui.label("Layer:"); - if ui - .add(egui::DragValue::new(&mut layer).range(0..=31)) - .changed() - { + if ui.add(egui::DragValue::new(&mut layer)).changed() { if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { - tileset.set_tile_collision_layer(tile_idx, layer); + tileset.set_tile_collision_layer(tile_idx, layer, physics_layer_id); project.mark_dirty(); } } @@ -3024,12 +3195,9 @@ fn render_collision_properties( let mut mask = collision_data.mask; ui.horizontal(|ui| { ui.label("Mask:"); - if ui - .add(egui::DragValue::new(&mut mask).range(0..=31)) - .changed() - { + if ui.add(egui::DragValue::new(&mut mask)).changed() { if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { - tileset.set_tile_collision_mask(tile_idx, mask); + tileset.set_tile_collision_mask(tile_idx, mask, physics_layer_id); project.mark_dirty(); } } @@ -3040,15 +3208,35 @@ fn render_collision_properties( // Action buttons if ui.button("Set Full Collision").clicked() { if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { - tileset.set_tile_collision_shape(tile_idx, bevy_map_core::CollisionShape::Full); - project.mark_dirty(); + if let Some(physics_layer_id) = editor_state + .tileset_editor_state + .collision_editor + .selected_physics_layer + { + tileset.set_tile_collision_shape( + tile_idx, + bevy_map_core::CollisionShape::Full, + physics_layer_id, + ); + project.mark_dirty(); + } } } if ui.button("Clear Collision").clicked() { if let Some(tileset) = project.tilesets.iter_mut().find(|t| t.id == tileset_id) { - tileset.set_tile_collision_shape(tile_idx, bevy_map_core::CollisionShape::None); - project.mark_dirty(); + if let Some(physics_layer_id) = editor_state + .tileset_editor_state + .collision_editor + .selected_physics_layer + { + tileset.set_tile_collision_shape( + tile_idx, + bevy_map_core::CollisionShape::None, + physics_layer_id, + ); + project.mark_dirty(); + } } } } @@ -3141,3 +3329,87 @@ fn point_to_segment_distance(p: &[f32; 2], a: &[f32; 2], b: &[f32; 2]) -> f32 { let dy = p[1] - closest[1]; (dx * dx + dy * dy).sqrt() } + +/// Render the new physics layer set dialog (Tiled-compatible) +pub fn render_new_physics_layer_set_dialog( + ctx: &egui::Context, + editor_state: &mut EditorState, + project: &mut Project, +) { + if !editor_state.show_add_physics_layer_set_dialog { + return; + } + + let Some(tileset_id) = editor_state.selected_tileset else { + return; + }; + + egui::Window::new("New Physics Layer Set") + .collapsible(false) + .resizable(false) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Name:"); + ui.text_edit_singleline(&mut editor_state.new_physics_layer_name); + }); + + // Color picker + let mut rgb = [ + (editor_state.new_physics_layer_debug_color.to_srgba().red * 255.0) as u8, + (editor_state.new_physics_layer_debug_color.to_srgba().green * 255.0) as u8, + (editor_state.new_physics_layer_debug_color.to_srgba().blue * 255.0) as u8, + ]; + ui.label("Color:"); + if egui::color_picker::color_edit_button_srgb(ui, &mut rgb).changed() { + editor_state.new_physics_layer_debug_color = Color::srgba( + rgb[0] as f32 / 255.0, + rgb[1] as f32 / 255.0, + rgb[2] as f32 / 255.0, + 0.3, + ); + } + + ui.horizontal(|ui| { + ui.label("Layer:"); + ui.add(egui::DragValue::new( + &mut editor_state.new_physics_layer_layer, + )); + }); + + ui.horizontal(|ui| { + ui.label("Mask:"); + ui.add(egui::DragValue::new( + &mut editor_state.new_physics_layer_mask, + )); + }); + + ui.separator(); + + ui.horizontal(|ui| { + if ui.button("Create").clicked() { + let srgba = editor_state.new_physics_layer_debug_color.to_srgba(); + let debug_color = [ + (srgba.red * 255.0) as u8, + (srgba.green * 255.0) as u8, + (srgba.blue * 255.0) as u8, + ]; + let physics_layer_set = PhysicsLayerSet::new( + editor_state.new_physics_layer_name.clone(), + editor_state.new_physics_layer_layer, + editor_state.new_physics_layer_mask, + debug_color, + ); + let tileset = project.tilesets.iter_mut().find(|t| t.id == tileset_id); + if let Some(tileset) = tileset { + tileset.physics_layers.layers.push(physics_layer_set); + }; + project.mark_dirty(); + editor_state.show_add_physics_layer_set_dialog = false; + editor_state.new_physics_layer_name = String::new(); + } + if ui.button("Cancel").clicked() { + editor_state.show_add_physics_layer_set_dialog = false; + } + }); + }); +} diff --git a/crates/bevy_map_runtime/src/collision.rs b/crates/bevy_map_runtime/src/collision.rs index a1c67d0..a28caee 100644 --- a/crates/bevy_map_runtime/src/collision.rs +++ b/crates/bevy_map_runtime/src/collision.rs @@ -124,22 +124,25 @@ pub fn spawn_tile_colliders( let idx = (y * level.width + x) as usize; if let Some(&Some(tile_index)) = tiles.get(idx) { // Check if this tile has collision - if let Some(props) = tileset.get_tile_properties(tile_index) { - if props.collision.has_collision() { - spawn_collider_for_tile( - &mut commands, - map_entity, - &props.collision, - x, - y, - tile_size, - &map_size, - &grid_size, - &tilemap_tile_size, - &map_type, - &anchor, - ); - total_colliders += 1; + for physics_layer in tileset.physics_layers.layers.iter() { + if let Some(collision) = physics_layer.get_tile_physics(tile_index) + { + if collision.has_collision() { + spawn_collider_for_tile( + &mut commands, + map_entity, + collision, + x, + y, + tile_size, + &map_size, + &grid_size, + &tilemap_tile_size, + &map_type, + &anchor, + ); + total_colliders += 1; + } } } } diff --git a/crates/bevy_map_runtime/src/lib.rs b/crates/bevy_map_runtime/src/lib.rs index bae4c91..cf09e63 100644 --- a/crates/bevy_map_runtime/src/lib.rs +++ b/crates/bevy_map_runtime/src/lib.rs @@ -292,6 +292,7 @@ fn handle_map_handle_spawning( mut query: Query<(Entity, &MapHandle, &mut MapHandleState, Option<&Transform>)>, entity_registry: Res, mut map_dialogues: ResMut, + mut map_spawn_event: MessageWriter, ) { for (entity, map_handle, mut state, _transform) in query.iter_mut() { // Check if asset is loaded @@ -359,6 +360,13 @@ fn handle_map_handle_spawning( Some(&entity_registry), ); + map_spawn_event.write(SpawnMapEvent { + level: project.level.clone(), + transform: Transform::default(), + tile_size: textures.tile_size, + tileset_textures: Vec::new(), + }); + // Add MapRoot marker and make it a child commands.entity(map_entity).insert(MapRoot { handle: map_handle.0.clone(), @@ -936,21 +944,21 @@ pub struct RuntimeMap { pub struct MapLayerIndex(pub usize); fn handle_spawn_map_events( - mut commands: Commands, + _commands: Commands, mut spawn_events: MessageReader, - mut spawned_events: MessageWriter, - entity_registry: Res, + _spawned_events: MessageWriter, + _entity_registry: Res, ) { - for event in spawn_events.read() { - let map_entity = spawn_map( - &mut commands, - &event.level, - event.tile_size, - &event.tileset_textures, - event.transform, - Some(&entity_registry), - ); - spawned_events.write(MapSpawnedEvent { map_entity }); + for _event in spawn_events.read() { + // let map_entity = spawn_map( + // &mut commands, + // &event.level, + // event.tile_size, + // &event.tileset_textures, + // event.transform, + // Some(&entity_registry), + // ); + // spawned_events.write(MapSpawnedEvent { map_entity }); } } @@ -1031,7 +1039,7 @@ pub fn spawn_map( let map_type = TilemapType::Square; // Calculate layer z-offset based on layer index - let layer_z = layer_index as f32 * 0.1; + let layer_z = layer_index as f32 * level.z_height; commands.entity(tilemap_entity).insert(( TilemapBundle { @@ -1109,24 +1117,24 @@ pub struct SpawnMapProjectEvent { } fn handle_spawn_map_project_events( - mut commands: Commands, + _commands: Commands, mut spawn_events: MessageReader, - mut spawned_events: MessageWriter, - entity_registry: Res, - mut map_dialogues: ResMut, + _spawned_events: MessageWriter, + _entity_registry: Res, + _map_dialogues: ResMut, ) { - for event in spawn_events.read() { + for _event in spawn_events.read() { // Load dialogues from the project - map_dialogues.load_from_project(&event.project); - - let map_entity = spawn_map_project( - &mut commands, - &event.project, - &event.textures, - event.transform, - Some(&entity_registry), - ); - spawned_events.write(MapSpawnedEvent { map_entity }); + // map_dialogues.load_from_project(&event.project); + + // let map_entity = spawn_map_project( + // &mut commands, + // &event.project, + // &event.textures, + // event.transform, + // Some(&entity_registry), + // ); + // spawned_events.write(MapSpawnedEvent { map_entity }); } } @@ -1283,7 +1291,7 @@ pub fn spawn_map_project( // Z-offset: layer_index * 0.1 + image_index * 0.01 // This ensures proper ordering: all images in layer 0 render before layer 1 - let layer_z = layer_index as f32 * 0.1 + image_index as f32 * 0.01; + let layer_z = layer_index as f32 * level.z_height + image_index as f32 * 0.01; commands.entity(tilemap_entity).insert(( TilemapBundle {