use avian3d::prelude::ColliderConstructor; use bevy::prelude::*; use serde::{Deserialize, Serialize}; /// Stable persisted identity for an actor. Bevy [`Entity`] IDs are runtime-only. #[derive( Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, )] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct ActorId(pub String); impl ActorId { pub fn new(id: impl Into) -> Self { Self(id.into()) } } /// User-facing actor display name, separate from Bevy's runtime [`Name`]. #[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct ActorName(pub String); impl ActorName { pub fn new(name: impl Into) -> Self { Self(name.into()) } } /// Stable identity for repeatable authoring items such as renderer slots. #[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct ComponentInstanceId(pub String); impl ComponentInstanceId { pub fn new(id: impl Into) -> Self { Self(id.into()) } pub fn is_empty(&self) -> bool { self.0.trim().is_empty() } } /// Editor-only component ordering metadata. Runtime systems must not depend on it. #[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct InspectorOrder { #[serde(default)] pub component_type_names: Vec, #[serde(default)] pub component_states: Vec, } /// Per-component inspector metadata that must survive scene save/load. #[derive(Reflect, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct InspectorComponentState { pub type_name: String, #[serde(default = "default_true")] pub active: bool, } impl Default for InspectorComponentState { fn default() -> Self { Self { type_name: String::new(), active: true, } } } impl InspectorOrder { pub fn is_component_active(&self, type_name: &str) -> bool { self.component_states .iter() .find(|state| state.type_name == type_name) .map(|state| state.active) .unwrap_or(true) } pub fn set_component_active(&mut self, type_name: impl Into, active: bool) { let type_name = type_name.into(); if let Some(state) = self .component_states .iter_mut() .find(|state| state.type_name == type_name) { state.active = active; } else { self.component_states .push(InspectorComponentState { type_name, active }); } } pub fn ensure_component_order(&mut self, present: &[&str]) { for type_name in present { if !self .component_type_names .iter() .any(|existing| existing == type_name) { self.component_type_names.push((*type_name).to_string()); } } } pub fn ordered_components<'a>(&self, present: &[&'a str]) -> Vec<&'a str> { let mut ordered = Vec::new(); for type_name in &self.component_type_names { if let Some(present_type) = present.iter().find(|present| **present == type_name) { ordered.push(*present_type); } } for type_name in present { if !ordered.iter().any(|ordered_type| ordered_type == type_name) { ordered.push(*type_name); } } ordered } pub fn move_component(&mut self, type_name: &str, offset: isize, present: &[&str]) -> bool { self.ensure_component_order(present); let Some(index) = self .component_type_names .iter() .position(|existing| existing == type_name) else { return false; }; let target = (index as isize + offset) .clamp(0, self.component_type_names.len() as isize - 1) as usize; if target == index { return false; } let item = self.component_type_names.remove(index); self.component_type_names.insert(target, item); true } } pub fn inspector_component_active(order: Option<&InspectorOrder>, type_name: &str) -> bool { order .map(|order| order.is_component_active(type_name)) .unwrap_or(true) } pub const COMPONENT_PRIMITIVE: &str = "shared::components::Primitive"; pub const COMPONENT_BRUSH_DESC: &str = "shared::components::BrushDesc"; pub const COMPONENT_STATIC_MESH_RENDERER: &str = "shared::components::StaticMeshRenderer"; pub const COMPONENT_MATERIAL_DESC: &str = "shared::components::MaterialDesc"; pub const COMPONENT_MATERIAL_OVERRIDE: &str = "shared::components::MaterialOverride"; pub const COMPONENT_LIGHT_DESC: &str = "shared::components::LightDesc"; pub const COMPONENT_RIGID_BODY_DESC: &str = "shared::components::RigidBodyDesc"; pub const COMPONENT_COLLIDER_DESC: &str = "shared::components::ColliderDesc"; pub const COMPONENT_PHYSICS_BODY: &str = "shared::components::PhysicsBody"; pub const COMPONENT_PLAYER_SPAWN: &str = "shared::components::PlayerSpawn"; pub const COMPONENT_WEAPON_SPAWN: &str = "shared::components::WeaponSpawn"; pub const COMPONENT_TRIGGER_VOLUME: &str = "shared::components::TriggerVolume"; pub const COMPONENT_TEAM_SPAWN: &str = "shared::components::TeamSpawn"; pub const COMPONENT_OBJECTIVE_MARKER: &str = "shared::components::ObjectiveMarker"; pub const COMPONENT_PREFAB_INSTANCE: &str = "shared::components::PrefabInstance"; pub const COMPONENT_POST_PROCESS_VOLUME: &str = "shared::components::PostProcessVolumeDesc"; pub const COMPONENT_PROJECT_SUN: &str = "shared::components::ProjectSun"; /// Reference to an imported content-browser asset or sub-asset. #[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct EditorAssetRef { /// Stable project asset registry UUID string. pub asset_id: String, /// Stable imported sub-asset ID inside the generated artifact. #[serde(default, alias = "mesh_label")] pub sub_asset_id: String, /// UI label cached for inspectors. The asset registry/artifact remains authoritative. #[serde(default)] pub label: String, } impl EditorAssetRef { pub fn new( asset_id: impl Into, sub_asset_id: impl Into, label: impl Into, ) -> Self { Self { asset_id: asset_id.into(), sub_asset_id: sub_asset_id.into(), label: label.into(), } } pub fn is_resolved(&self) -> bool { !self.asset_id.trim().is_empty() && !self.sub_asset_id.trim().is_empty() } } /// Marker for entities that belong to editable/savable level data. #[derive(Component, Reflect, Default, Debug, Clone, Copy, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct LevelObject; /// Runtime marker for helper meshes that must not enter Solari raytracing. #[derive(Component, Reflect, Default, Debug, Clone, Copy)] #[reflect(Component, Default, Debug)] pub struct RaytracingExcluded; /// Marks the player's spawn location for editor Play mode. /// /// Place on a level object and set its `Transform` to choose where Play starts. /// If none exists, the game default spawn is used. #[derive(Component, Reflect, Default, Debug, Clone, Copy, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct PlayerSpawn; /// Marks the runtime/project default directional sun. /// /// Scene-authored directional [`LightDesc`] entities can override this default /// so levels can own their lighting without duplicating sun contribution. #[derive(Component, Reflect, Default, Debug, Clone, Copy)] #[reflect(Component, Default, Debug)] pub struct ProjectSun; /// Simple primitive meshes that can be authored without external assets. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum PrimitiveShape { #[default] Box, Sphere, Ramp, } /// Reflectable authoring primitive. Hydration turns this into `Mesh3d`. #[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct Primitive { pub shape: PrimitiveShape, pub size: Vec3, } impl Primitive { pub fn cuboid(size: Vec3) -> Self { Self { shape: PrimitiveShape::Box, size, } } pub fn sphere(radius: f32) -> Self { Self { shape: PrimitiveShape::Sphere, size: Vec3::splat(radius * 2.0), } } pub fn ramp(size: Vec3) -> Self { Self { shape: PrimitiveShape::Ramp, size, } } } impl Default for Primitive { fn default() -> Self { Self::cuboid(Vec3::ONE) } } /// Brush boolean role. Subtractive brushes are authored now but CSG is applied /// by later tooling. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum BrushKind { #[default] Additive, SubtractiveMarker, } /// Serializable convex brush plane. Plane equation is `normal.dot(point) = distance`. #[derive(Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct BrushPlaneDesc { pub normal: Vec3, pub distance: f32, } impl Default for BrushPlaneDesc { fn default() -> Self { Self { normal: Vec3::Y, distance: 0.0, } } } /// One polygon face on a convex authoring brush. #[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct BrushFaceDesc { #[serde(default)] pub id: ComponentInstanceId, pub plane: BrushPlaneDesc, #[serde(default)] pub vertices: Vec, #[serde(default)] pub material: Option, #[serde(default)] pub texture: Option, #[serde(default)] pub uv_offset: Vec2, #[serde(default = "default_uv_scale")] pub uv_scale: Vec2, #[serde(default)] pub uv_rotation: f32, #[serde(default)] pub smoothing_group: u32, } impl Default for BrushFaceDesc { fn default() -> Self { Self { id: ComponentInstanceId::default(), plane: BrushPlaneDesc::default(), vertices: Vec::new(), material: None, texture: None, uv_offset: Vec2::ZERO, uv_scale: default_uv_scale(), uv_rotation: 0.0, smoothing_group: 0, } } } fn default_uv_scale() -> Vec2 { Vec2::ONE } /// Convex brush authoring data. Hydration turns this into generated mesh /// children while the scene stores only this component. #[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct BrushDesc { pub kind: BrushKind, #[serde(default)] pub faces: Vec, #[serde(default = "default_true")] pub cast_shadows: bool, #[serde(default = "default_true")] pub receive_shadows: bool, } impl BrushDesc { pub fn cuboid(size: Vec3) -> Self { let hx = size.x.max(0.001) * 0.5; let hy = size.y.max(0.001) * 0.5; let hz = size.z.max(0.001) * 0.5; let faces = vec![ brush_face( "face:+x", Vec3::X, vec![ Vec3::new(hx, -hy, -hz), Vec3::new(hx, hy, -hz), Vec3::new(hx, hy, hz), Vec3::new(hx, -hy, hz), ], ), brush_face( "face:-x", Vec3::NEG_X, vec![ Vec3::new(-hx, -hy, hz), Vec3::new(-hx, hy, hz), Vec3::new(-hx, hy, -hz), Vec3::new(-hx, -hy, -hz), ], ), brush_face( "face:+y", Vec3::Y, vec![ Vec3::new(-hx, hy, -hz), Vec3::new(-hx, hy, hz), Vec3::new(hx, hy, hz), Vec3::new(hx, hy, -hz), ], ), brush_face( "face:-y", Vec3::NEG_Y, vec![ Vec3::new(-hx, -hy, hz), Vec3::new(-hx, -hy, -hz), Vec3::new(hx, -hy, -hz), Vec3::new(hx, -hy, hz), ], ), brush_face( "face:+z", Vec3::Z, vec![ Vec3::new(hx, -hy, hz), Vec3::new(hx, hy, hz), Vec3::new(-hx, hy, hz), Vec3::new(-hx, -hy, hz), ], ), brush_face( "face:-z", Vec3::NEG_Z, vec![ Vec3::new(-hx, -hy, -hz), Vec3::new(-hx, hy, -hz), Vec3::new(hx, hy, -hz), Vec3::new(hx, -hy, -hz), ], ), ]; Self { kind: BrushKind::Additive, faces, cast_shadows: true, receive_shadows: true, } } } impl Default for BrushDesc { fn default() -> Self { Self::cuboid(Vec3::ONE) } } fn brush_face(id: &str, normal: Vec3, vertices: Vec) -> BrushFaceDesc { let distance = vertices .first() .map(|vertex| normal.dot(*vertex)) .unwrap_or(0.0); BrushFaceDesc { id: ComponentInstanceId::new(id), plane: BrushPlaneDesc { normal, distance }, vertices, ..Default::default() } } /// Legacy static mesh collision setting kept only for loading older scenes. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum StaticMeshColliderMode { #[default] None, /// Generate an Avian triangle mesh collider from the loaded render mesh. TrimeshFromMesh, } /// Backward-compatible alias for the previous static mesh reference name. pub type StaticMeshAssetRef = EditorAssetRef; /// One material slot on a static mesh renderer entry. #[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct StaticMeshMaterialSlot { pub slot_index: usize, pub name: String, /// Bevy sub-asset label for the source material, if the importer provided one. pub source_material_label: Option, /// Optional per-instance material override. pub override_material: Option, } /// One mesh draw slot owned by a [`StaticMeshRenderer`]. #[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct MeshRenderSlot { #[serde(default)] pub id: ComponentInstanceId, pub name: String, pub mesh: EditorAssetRef, #[serde(default)] pub material: Option, #[serde(default)] pub local_transform: Transform, #[serde(default = "default_true")] pub visible: bool, #[serde(default = "default_true")] pub cast_shadows: bool, #[serde(default = "default_true")] pub receive_shadows: bool, } impl Default for MeshRenderSlot { fn default() -> Self { Self { id: ComponentInstanceId::default(), name: "Mesh".into(), mesh: EditorAssetRef::default(), material: None, local_transform: Transform::default(), visible: true, cast_shadows: true, receive_shadows: true, } } } /// Backward-compatible alias for old code paths while scenes migrate to slots. pub type StaticMeshRendererEntry = MeshRenderSlot; /// Reflectable static mesh renderer. Hydration turns entries into child `Mesh3d` /// entities and source/default materials. #[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct StaticMeshRenderer { #[serde(default, alias = "entries")] pub slots: Vec, } impl StaticMeshRenderer { pub fn empty() -> Self { Self { slots: Vec::new() } } pub fn single(slot: MeshRenderSlot) -> Self { Self { slots: vec![slot] } } } impl Default for StaticMeshRenderer { fn default() -> Self { Self::empty() } } fn default_true() -> bool { true } /// How a material should be resolved at runtime. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum MaterialShaderKind { #[default] StandardLit, Unlit, Custom, } /// Reference to a built-in or custom shader schema. #[derive(Reflect, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct ShaderRefDesc { pub kind: MaterialShaderKind, #[serde(default)] pub schema_path: Option, #[serde(default)] pub shader_path: Option, } impl Default for ShaderRefDesc { fn default() -> Self { Self { kind: MaterialShaderKind::StandardLit, schema_path: None, shader_path: None, } } } /// Typed material parameter value exposed by a shader schema. #[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum MaterialParameterValue { Bool(bool), Float(f32), Vec2(Vec2), Vec3(Vec3), Color(ColorDesc), Enum(String), } impl Default for MaterialParameterValue { fn default() -> Self { Self::Float(0.0) } } #[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct MaterialParameter { pub name: String, pub value: MaterialParameterValue, } #[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct MaterialTextureBinding { pub name: String, pub texture: Option, } /// Per-render-slot material override stored on scene actors. #[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct MaterialSlotOverride { pub slot_id: ComponentInstanceId, #[serde(default)] pub base_material: Option, pub material: MaterialDesc, } /// Actor-level material overrides. Shared material assets are edited by asset /// inspectors, while actor inspectors write only to this component. #[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct MaterialOverride { #[serde(default)] pub slots: Vec, } /// Serializable color description. This avoids coupling saved scenes to any /// internal color representation details. #[derive(Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, Serialize, Deserialize)] pub struct ColorDesc { pub r: f32, pub g: f32, pub b: f32, pub a: f32, } impl ColorDesc { pub const fn srgb(r: f32, g: f32, b: f32) -> Self { Self { r, g, b, a: 1.0 } } pub fn to_color(self) -> Color { Color::srgba(self.r, self.g, self.b, self.a) } } impl Default for ColorDesc { fn default() -> Self { Self::srgb(0.8, 0.8, 0.8) } } /// Reflectable authoring material. Hydration turns this into `StandardMaterial`. #[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct MaterialDesc { #[serde(default)] pub shader: ShaderRefDesc, pub base_color: ColorDesc, pub metallic: f32, pub roughness: f32, /// Emissive radiance tint. Values are multiplied by `emissive_intensity`. #[serde(default = "default_emissive_color")] pub emissive_color: ColorDesc, /// Emissive luminance multiplier in nits. `0.0` means non-emissive. #[serde(default)] pub emissive_intensity: f32, /// Optional base color (albedo) texture, path relative to `assets/`. pub base_color_texture: Option, /// Optional emissive texture, path relative to `assets/`. #[serde(default)] pub emissive_texture: Option, /// Optional normal map texture, path relative to `assets/`. pub normal_map_texture: Option, /// Optional metallic/roughness texture, path relative to `assets/`. pub metallic_roughness_texture: Option, /// When set, references a [`crate::MaterialAsset`] RON under `assets/materials/`. #[serde(default)] pub material_asset_path: Option, /// Shader-schema-driven scalar/vector/color values. #[serde(default)] pub parameters: Vec, /// Shader-schema-driven texture references. #[serde(default)] pub textures: Vec, } impl MaterialDesc { pub fn new(base_color: ColorDesc, metallic: f32, roughness: f32) -> Self { Self { shader: ShaderRefDesc::default(), base_color, metallic, roughness, emissive_color: default_emissive_color(), emissive_intensity: 0.0, base_color_texture: None, emissive_texture: None, normal_map_texture: None, metallic_roughness_texture: None, material_asset_path: None, parameters: Vec::new(), textures: Vec::new(), } } } fn default_emissive_color() -> ColorDesc { ColorDesc::srgb(1.0, 1.0, 1.0) } impl Default for MaterialDesc { fn default() -> Self { Self::new(ColorDesc::default(), 0.0, 0.65) } } /// Reflectable reference to an imported 3D model scene (glTF/GLB or FBX). /// Hydration turns this into a `SceneRoot` on the entity. #[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct ModelRef { /// Asset path relative to `assets/`, e.g. `models/foo.glb` or `models/foo.fbx`. pub path: String, /// Scene index within the model file (`#SceneN` for FBX; glTF scene index otherwise). pub scene_index: usize, } impl ModelRef { pub fn new(path: impl Into) -> Self { Self { path: path.into(), scene_index: 0, } } /// Asset-server path for FBX `#SceneN` labels (see `bevy_ufbx`). pub fn fbx_scene_asset_path(path: &str, scene_index: usize) -> String { format!("{path}#Scene{scene_index}") } } /// Reflectable reference to a saved `.scn.ron` dynamic scene under `assets/`. /// Hydration turns this into a `DynamicSceneRoot`. #[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct PrefabRef { /// Asset path relative to `assets/`, e.g. "prefabs/foo.scn.ron". pub path: String, } impl PrefabRef { pub fn new(path: impl Into) -> Self { Self { path: path.into() } } } /// Marks a placed prefab instance with stable registry identity. #[derive(Component, Reflect, Debug, Clone, Default, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct PrefabInstance { /// Stable asset registry UUID string. pub asset_id: String, /// Source prefab path relative to `assets/`. pub source_path: String, /// Optional per-instance transform override serialized as RON (v1: unused). #[serde(default)] pub overrides_ron: Option, } impl PrefabInstance { pub fn new(asset_id: impl Into, source_path: impl Into) -> Self { Self { asset_id: asset_id.into(), source_path: source_path.into(), overrides_ron: None, } } } /// Weapon pickup / spawn pad for FPS gameplay authoring. #[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct WeaponSpawn { pub weapon_id: String, } /// Per-field post-process overrides (`None` = inherit project settings). #[derive(Reflect, Debug, Clone, Default, Serialize, Deserialize)] #[reflect(Default, Debug, Serialize, Deserialize)] pub struct PostProcessVolumeOverrides { pub exposure_ev100: Option, pub fog_color: Option, pub fog_density: Option, pub bloom: Option, pub taa: Option, pub ssao: Option, pub tonemapping_aces: Option, pub atmosphere: Option, pub hdr: Option, } impl PostProcessVolumeOverrides { pub fn override_count(&self) -> u32 { let mut count = 0u32; if self.exposure_ev100.is_some() { count += 1; } if self.fog_color.is_some() { count += 1; } if self.fog_density.is_some() { count += 1; } if self.bloom.is_some() { count += 1; } if self.taa.is_some() { count += 1; } if self.ssao.is_some() { count += 1; } if self.tonemapping_aces.is_some() { count += 1; } if self.atmosphere.is_some() { count += 1; } if self.hdr.is_some() { count += 1; } count } } /// Scene-authored post-process volume (axis-aligned box in local space). #[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct PostProcessVolumeDesc { pub half_extents: Vec3, pub priority: i32, pub blend_distance: f32, pub overrides: PostProcessVolumeOverrides, /// Path to `assets/post_fx/*.ron`, if any. pub fullscreen_effect: Option, /// Optional `assets/rendering_profiles/*.ron` preset seed. pub profile: Option, /// Editor-friendly label for HUD / Rendering panel. pub label: Option, } impl Default for PostProcessVolumeDesc { fn default() -> Self { Self { half_extents: Vec3::new(4.0, 2.0, 4.0), priority: 0, blend_distance: 2.0, overrides: PostProcessVolumeOverrides::default(), fullscreen_effect: None, profile: None, label: None, } } } /// Axis-aligned trigger volume for gameplay events. #[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct TriggerVolume { pub half_extents: Vec3, pub event_name: String, } impl Default for TriggerVolume { fn default() -> Self { Self { half_extents: Vec3::new(2.0, 2.0, 2.0), event_name: "trigger".into(), } } } /// Team spawn point marker. #[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct TeamSpawn { pub team_id: u8, } /// Objective / game mode marker. #[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct ObjectiveMarker { pub objective_id: String, } /// Stable sibling order among entities sharing the same parent in the hierarchy outliner. #[derive(Component, Reflect, Default, Debug, Clone, Copy, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct HierarchySiblingIndex(pub i32); /// Authoring visibility saved with the scene; hydration maps this to ECS [`Visibility`]. #[derive(Component, Reflect, Debug, Clone, Copy, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct EditorVisibility { pub visible: bool, } impl Default for EditorVisibility { fn default() -> Self { Self { visible: true } } } /// Declared role of a level object for inspector routing and save validation. #[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Debug, PartialEq, Serialize, Deserialize)] pub enum ActorKind { Empty, Brush, StaticMesh, ImportedModel, Light, PrefabAnchor, PlayerSpawn, WeaponSpawn, TriggerVolume, PostProcessVolume, TeamSpawn, Objective, } /// Inspector upper bound for point/spot [`LightDesc::intensity`] (lumens). /// /// Bevy's `PointLight` / `SpotLight` default is `light_consts::lumens::VERY_LARGE_CINEMA_LIGHT` /// (1M lumens) for outdoor exposure (~12–15 EV100). This cap is 2× that reference so authored /// lights can exceed the old 200k ceiling without unbounded sliders. pub const AUTHORING_POINT_SPOT_LUMENS_MAX: f32 = 2_000_000.0; /// Inspector upper bound for directional [`LightDesc::intensity`] (lux). pub const AUTHORING_DIRECTIONAL_LUX_MAX: f32 = 150_000.0; /// Serializable light kinds for editor-authored lighting. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum AuthoringLightKind { #[default] Point, Spot, Directional, } /// Reflectable authoring light. Hydration turns this into Bevy light components. #[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct LightDesc { pub kind: AuthoringLightKind, pub color: ColorDesc, /// Lumens (point/spot) or lux (directional). pub intensity: f32, /// Attenuation range in meters (point/spot). pub range: f32, pub shadows: bool, /// Spot inner cone half-angle in degrees. #[serde(default = "default_spot_inner_angle_deg")] pub inner_angle_deg: f32, /// Spot outer cone half-angle in degrees. #[serde(default = "default_spot_outer_angle_deg")] pub outer_angle_deg: f32, } fn default_spot_inner_angle_deg() -> f32 { 25.0 } fn default_spot_outer_angle_deg() -> f32 { 35.0 } impl Default for LightDesc { fn default() -> Self { Self::for_kind(AuthoringLightKind::Point) } } impl LightDesc { /// Sensible defaults when spawning a builtin light from the asset browser. pub fn for_kind(kind: AuthoringLightKind) -> Self { match kind { AuthoringLightKind::Point => Self { kind, color: ColorDesc::srgb(1.0, 0.95, 0.85), intensity: 25_000.0, range: 25.0, shadows: true, inner_angle_deg: default_spot_inner_angle_deg(), outer_angle_deg: default_spot_outer_angle_deg(), }, AuthoringLightKind::Spot => Self { kind, color: ColorDesc::srgb(1.0, 0.95, 0.85), intensity: 30_000.0, range: 35.0, shadows: true, inner_angle_deg: default_spot_inner_angle_deg(), outer_angle_deg: default_spot_outer_angle_deg(), }, AuthoringLightKind::Directional => Self { kind, color: ColorDesc::srgb(1.0, 0.95, 0.85), // Matches `ProjectSettings::rendering.sun_illuminance` default; spawn code // refreshes this from live project settings when placing in the scene. intensity: 100_000.0, range: 0.0, shadows: true, inner_angle_deg: default_spot_inner_angle_deg(), outer_angle_deg: default_spot_outer_angle_deg(), }, } } } /// Runtime body type in a serialized-friendly form. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum AuthoringRigidBody { #[default] Static, Kinematic, Dynamic, } /// Reflectable authoring rigid body separated from collider geometry. #[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct RigidBodyDesc { pub body: AuthoringRigidBody, } impl Default for RigidBodyDesc { fn default() -> Self { Self { body: AuthoringRigidBody::Static, } } } /// Mesh collider cooking options for imported static mesh assets. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum MeshColliderCooking { #[default] Default, } /// Serializable collider shape independent of rigid body settings. #[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum ColliderShapeDesc { Cuboid { x_length: f32, y_length: f32, z_length: f32, }, Sphere { radius: f32, }, Capsule { radius: f32, height: f32, }, StaticMesh { meshes: Vec, cooking: MeshColliderCooking, }, } impl Default for ColliderShapeDesc { fn default() -> Self { Self::Cuboid { x_length: 1.0, y_length: 1.0, z_length: 1.0, } } } impl ColliderShapeDesc { pub fn static_mesh(meshes: Vec) -> Self { Self::StaticMesh { meshes, cooking: MeshColliderCooking::Default, } } pub fn to_constructor(&self) -> Option { match self { ColliderShapeDesc::Cuboid { x_length, y_length, z_length, } => Some(ColliderConstructor::Cuboid { x_length: *x_length, y_length: *y_length, z_length: *z_length, }), ColliderShapeDesc::Sphere { radius } => { Some(ColliderConstructor::Sphere { radius: *radius }) } ColliderShapeDesc::Capsule { radius, height } => Some(ColliderConstructor::Capsule { radius: *radius, height: *height, }), ColliderShapeDesc::StaticMesh { .. } => None, } } } /// Reflectable authoring collider. Static mesh colliders are hydrated on /// generated mesh children so they can reuse Bevy's mesh-to-collider path. #[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct ColliderDesc { #[serde(default = "default_true")] pub enabled: bool, #[serde(default)] pub is_trigger: bool, pub shape: ColliderShapeDesc, } impl ColliderDesc { pub fn static_cuboid(size: Vec3) -> Self { Self { enabled: true, is_trigger: false, shape: ColliderShapeDesc::Cuboid { x_length: size.x, y_length: size.y, z_length: size.z, }, } } pub fn static_sphere(radius: f32) -> Self { Self { enabled: true, is_trigger: false, shape: ColliderShapeDesc::Sphere { radius }, } } pub fn static_mesh(meshes: Vec) -> Self { Self { enabled: true, is_trigger: false, shape: ColliderShapeDesc::static_mesh(meshes), } } } impl Default for ColliderDesc { fn default() -> Self { Self::static_cuboid(Vec3::ONE) } } /// Reflectable authoring physics. Hydration turns this into `RigidBody` and /// Avian's reflectable `ColliderConstructor` component. #[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct PhysicsBody { pub body: AuthoringRigidBody, pub collider: ColliderConstructor, } impl PhysicsBody { pub fn static_cuboid(size: Vec3) -> Self { Self { body: AuthoringRigidBody::Static, collider: ColliderConstructor::Cuboid { x_length: size.x, y_length: size.y, z_length: size.z, }, } } pub fn static_sphere(radius: f32) -> Self { Self { body: AuthoringRigidBody::Static, collider: ColliderConstructor::Sphere { radius }, } } } impl Default for PhysicsBody { fn default() -> Self { Self::static_cuboid(Vec3::ONE) } } #[cfg(test)] mod tests { use super::ModelRef; use bevy::light::light_consts; #[test] fn point_spot_lumen_max_covers_bevy_cinema_reference() { assert!( super::AUTHORING_POINT_SPOT_LUMENS_MAX >= light_consts::lumens::VERY_LARGE_CINEMA_LIGHT ); } #[test] fn fbx_scene_asset_path_format() { assert_eq!( ModelRef::fbx_scene_asset_path("models/foo.fbx", 0), "models/foo.fbx#Scene0" ); assert_eq!( ModelRef::fbx_scene_asset_path("models/foo.fbx", 2), "models/foo.fbx#Scene2" ); } }