// ▄████████▄ + ███ + ▄█████████ ███ + // ███▀ ▀███ + + ███ ███▀ + ███ + + // ███ + ███ ███ ███ █████████ ███ ███ ███ ███ // ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███ // ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███ // ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███ // ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████ // + + + ███ // + ▀████████████████████████████████████████████████████▀ // // This module populates the world with actors as defined in "defs.txt" extern crate regex; use crate::prelude::*; use bevy::pbr::{NotShadowCaster, NotShadowReceiver}; use bevy::prelude::*; use bevy_xpbd_3d::prelude::*; use regex::Regex; pub const ID_SPECIAL_PLAYERCAM: &str = "PLAYERCAMERA"; pub const ID_EARTH: &str = "earth"; pub const ID_SOL: &str = "sol"; pub const ID_JUPITER: &str = "jupiter"; pub struct CmdPlugin; impl Plugin for CmdPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, load_defs); app.add_systems( Update, handle_spawn_events .before(spawn_entities) .before(spawn_scenes), ); app.add_systems(Update, spawn_entities); app.add_systems(Update, spawn_scenes.after(spawn_entities)); app.add_systems(Update, process_mesh); app.add_systems( PreUpdate, hide_colliders.run_if(any_with_component::), ); app.add_event::(); app.add_event::(); app.add_event::(); } } #[derive(Component)] pub struct NeedsSceneColliderRemoved; #[derive(Event)] pub struct SpawnEvent(ParserState); #[derive(Event)] pub struct SpawnActorEvent(ParserState); #[derive(Event)] pub struct SpawnSceneEvent(ParserState); #[derive(PartialEq, Clone)] enum DefClass { Actor, Scene, None, } #[derive(Clone)] struct ParserState { class: DefClass, // Generic fields name: Option, chat: String, // Actor fields id: String, pos: DVec3, relative_to: Option, model: Option, model_scale: f32, rotation: Quat, axialtilt: f32, velocity: DVec3, angular_momentum: DVec3, pronoun: Option, is_sphere: bool, is_player: bool, is_lifeform: bool, is_alive: bool, is_suited: bool, is_vehicle: bool, is_clickable: bool, is_targeted_on_startup: bool, is_sun: bool, is_moon: bool, is_planet: bool, is_point_of_interest: bool, orbit_distance: Option, orbit_object_id: Option, orbit_phase: Option, has_physics: bool, has_ring: bool, wants_maxrotation: Option, wants_maxvelocity: Option, wants_tolookat_id: Option, wants_matchvelocity_id: Option, collider_is_mesh: bool, collider_is_one_mesh_of_scene: bool, thrust_forward: f32, thrust_sideways: f32, thrust_back: f32, reaction_wheels: f32, warmup_seconds: f32, engine_type: actor::EngineType, oxygen: f32, density: f64, collider: Collider, camdistance: f32, suit_integrity: f32, light_brightness: f32, light_color: Option, ar_model: Option, show_only_in_map_at_distance: Option<(f64, String)>, } impl Default for ParserState { fn default() -> Self { let default_actor = actor::Actor::default(); let default_engine = actor::Engine::default(); Self { class: DefClass::None, name: None, chat: "".to_string(), id: "".to_string(), pos: DVec3::new(0.0, 0.0, 0.0), relative_to: None, model: None, model_scale: 1.0, rotation: Quat::IDENTITY, axialtilt: 0.0, velocity: DVec3::splat(0.0), angular_momentum: DVec3::new(0.03, 0.3, 0.09), pronoun: None, is_sphere: false, is_player: false, is_lifeform: false, is_alive: false, is_suited: false, is_vehicle: false, is_clickable: true, is_targeted_on_startup: false, is_sun: false, is_moon: false, is_planet: false, is_point_of_interest: false, orbit_distance: None, orbit_object_id: None, orbit_phase: None, has_physics: true, has_ring: false, wants_maxrotation: None, wants_maxvelocity: None, wants_tolookat_id: None, wants_matchvelocity_id: None, collider_is_mesh: false, collider_is_one_mesh_of_scene: false, thrust_forward: default_engine.thrust_forward, thrust_sideways: default_engine.thrust_forward, thrust_back: default_engine.thrust_back, reaction_wheels: default_engine.reaction_wheels, warmup_seconds: default_engine.warmup_seconds, engine_type: default_engine.engine_type, oxygen: nature::OXY_D, density: 100.0, collider: Collider::sphere(1.0), camdistance: default_actor.camdistance, suit_integrity: 1.0, light_brightness: 0.0, light_color: None, ar_model: None, show_only_in_map_at_distance: None, } } } pub fn load_defs(mut ew_spawn: EventWriter) { let re1 = Regex::new(r"^\s*([a-z_-]+)\s+(.*)$").unwrap(); let re2 = Regex::new("\"([^\"]*)\"|(-?[0-9]+[0-9e-]*(?:\\.[0-9e-]+)?)|([a-zA-Z_-][a-zA-Z0-9_-]*)") .unwrap(); let defs_string = include_str!("data/defs.txt"); let mut lines = defs_string.lines(); let mut state = ParserState::default(); let mut command; let mut parameters; let mut line_nr = -1; while let Some(line) = lines.next() { line_nr += 1; let caps = re1.captures(line); if caps.is_none() { if line.trim() != "" { error!("Syntax Error in definitions line {}: `{}`", line_nr, line); } continue; } if let Some(caps) = caps { command = caps.get(1).unwrap().as_str(); parameters = caps.get(2).unwrap().as_str(); } else { error!( "Failed to read regex captures in line {}: `{}`", line_nr, line ); continue; } let mut parts: Vec<&str> = Vec::new(); parts.push(command); for caps in re2.captures_iter(parameters) { if let Some(part) = caps.get(1) { parts.push(&part.as_str()); } if let Some(part) = caps.get(2) { parts.push(&part.as_str()); } if let Some(part) = caps.get(3) { parts.push(&part.as_str()); } } match parts.as_slice() { ["name", name] => { debug!("Registering name: {}", name); state.name = Some(name.to_string()); } ["template", "person"] => { // command: collider handcrafted state.collider_is_one_mesh_of_scene = true; // command: alive yes state.is_alive = true; state.is_lifeform = true; state.is_suited = true; // command: oxygen 0.864 state.is_lifeform = true; state.is_suited = true; state.oxygen = nature::OXY_D; // command: engine monopropellant state.engine_type = actor::EngineType::Monopropellant; // command: thrust 1.2 1 1 14 1.5 state.thrust_forward = 1.2; state.thrust_back = 1.0; state.thrust_sideways = 1.0; state.reaction_wheels = 14.0; state.warmup_seconds = 1.5; // command: wants maxrotation 0 state.wants_maxrotation = Some(0.0); // command: wants maxvelocity 0 state.wants_maxvelocity = Some(0.0); // command: pointofinterest yes state.is_point_of_interest = true; // command: density 200 state.density = 200.0; // command: angularmomentum 0 0 0 state.angular_momentum = DVec3::ZERO; } ["template", "clippy"] => { // command: angularmomentum 0 0 0 state.angular_momentum = DVec3::ZERO; // command: wants maxrotation 0 state.wants_maxrotation = Some(0.0); // command: wants maxvelocity 0 state.wants_maxvelocity = Some(0.0); // command: thrust 15 6 3 400 0.5 state.thrust_forward = 15.0; state.thrust_back = 6.0; state.thrust_sideways = 3.0; state.reaction_wheels = 400.0; state.warmup_seconds = 0.5; // command: scale 3 state.model_scale = 3.0; // command: pronoun it state.pronoun = Some("it".to_string()); // command: pointofinterest yes state.is_point_of_interest = true; } ["template", "cruiser"] => { // command: actor ? ? ? cruiser state.class = DefClass::Actor; state.model = Some("cruiser".to_string()); // command: scale 5 state.model_scale = 5.0; // command: vehicle yes state.is_vehicle = true; // command: angularmomentum 0 0 0 state.angular_momentum = DVec3::ZERO; // command: collider handcrafted state.collider_is_one_mesh_of_scene = true; // command: thrust 16 16 8 100000 3 state.thrust_forward = 16.0; state.thrust_back = 16.0; state.thrust_sideways = 8.0; state.reaction_wheels = 100000.0; state.warmup_seconds = 3.0; // command: engine ion state.engine_type = actor::EngineType::Ion; // command: camdistance 50 state.camdistance = 50.0; // command: density 500 state.density = 500.0; // command: pointofinterest yes state.is_point_of_interest = true; } // Parsing actors ["actor", x, y, z, model] => { ew_spawn.send(SpawnEvent(state)); state = ParserState::default(); state.class = DefClass::Actor; state.model = Some(model.to_string()); if let (Ok(x_float), Ok(y_float), Ok(z_float)) = (x.parse::(), y.parse::(), z.parse::()) { state.pos = DVec3::new(x_float, y_float, z_float); } else { error!("Can't parse coordinates as floats in def: {line}"); state = ParserState::default(); continue; } } ["actor", x, y, z] => { ew_spawn.send(SpawnEvent(state)); state = ParserState::default(); state.class = DefClass::Actor; if let (Ok(x_float), Ok(y_float), Ok(z_float)) = (x.parse::(), y.parse::(), z.parse::()) { state.pos = DVec3::new(x_float, y_float, z_float); } else { error!("Can't parse coordinates as floats in def: {line}"); state = ParserState::default(); continue; } } ["scene", x, y, z, name] => { ew_spawn.send(SpawnEvent(state)); state = ParserState::default(); state.class = DefClass::Scene; state.name = Some(name.to_string()); if let (Ok(x_float), Ok(y_float), Ok(z_float)) = (x.parse::(), y.parse::(), z.parse::()) { state.pos = DVec3::new(x_float, y_float, z_float); } else { error!("Can't parse coordinates as floats in def: {line}"); state = ParserState::default(); continue; } } ["relativeto", id] => { // Offsets this actor's position by the pos. of the actor with the given id. state.relative_to = Some(id.to_string()); } ["orbitaround", object_id, radius_str] => { // Places the actor into an orbit around the object_id so that it's position // will be offset by the orbital distance (radius_str) and the phase will // be set dynamically based on the current day+time. // Unlike the "orbit" command, this position is not static, and there is no // parameter for a phase offset, though you can set that with the // "orbit_phase_offset" command. // Make sure to use this with "relativeto" with the object that's being orbited. if let Ok(r) = radius_str.parse::() { state.orbit_distance = Some(r); state.orbit_object_id = Some(object_id.to_string()); } else { error!("Can't parse float: {line}"); continue; } } ["orbit", radius_str, phase_str] => { // Offsets the actor's position by the given distance (radius_str) // in the direction as defined by the angle in phase_str. // Unlike "orbitaround", the position is static though and will not // change over time. // Make sure to use this with "relativeto" with the object that's being orbited. if let (Ok(r), Ok(phase)) = (radius_str.parse::(), phase_str.parse::()) { state.orbit_distance = Some(r); state.orbit_phase = Some(phase * PI * 2.0); } else { error!("Can't parse float: {line}"); continue; } } ["orbit_phase_offset", value] => { // When used in combination with "orbitaround", this command allows // you to move the actor ahead (or behind) in its orbit by the given offset. if let Ok(value_float) = value.parse::() { let offset_radians = 2.0 * PI * value_float; if let Some(phase_radians) = state.orbit_phase { state.orbit_phase = Some(phase_radians + offset_radians); } else { state.orbit_phase = Some(offset_radians); } } else { error!("Can't parse float: {line}"); continue; } } ["sphere", "yes"] => { state.is_sphere = true; } ["id", id] => { state.id = id.to_string(); } ["alive", "yes"] => { state.is_alive = true; state.is_lifeform = true; state.is_suited = true; } ["alive", "no"] => { state.is_alive = false; } ["vehicle", "yes"] => { state.is_vehicle = true; } ["clickable", "no"] => { state.is_clickable = false; } ["moon", "yes"] => { state.is_moon = true; } ["planet", "yes"] => { state.is_planet = true; } ["sun", "yes"] => { state.is_sun = true; } ["ring", "yes"] => { state.has_ring = true; } ["pointofinterest", "yes"] => { state.is_point_of_interest = true; } ["oxygen", amount] => { if let Ok(amount) = amount.parse::() { state.is_lifeform = true; state.is_suited = true; state.oxygen = amount; } else { error!("Can't parse float: {line}"); continue; } } ["pronoun", pronoun] => { state.pronoun = Some(pronoun.to_string()); } ["chatid", chat] => { state.chat = chat.to_string(); } ["scale", scale] => { if let Ok(scale_float) = scale.parse::() { state.model_scale = scale_float; } else { error!("Can't parse float: {line}"); continue; } } ["rotationx", rotation_x] => { if let Ok(rotation_x_float) = rotation_x.parse::() { state.rotation *= Quat::from_rotation_x(rotation_x_float.to_radians()); } else { error!("Can't parse float: {line}"); continue; } } ["rotationy", rotation_y] => { if let Ok(rotation_y_float) = rotation_y.parse::() { state.rotation *= Quat::from_rotation_y(rotation_y_float.to_radians()); } else { error!("Can't parse float: {line}"); continue; } } ["rotationz", rotation_z] => { if let Ok(rotation_z_float) = rotation_z.parse::() { state.rotation *= Quat::from_rotation_z(rotation_z_float.to_radians()); } else { error!("Can't parse float: {line}"); continue; } } ["axialtilt", rotation_y] => { if let Ok(rotation_y_float) = rotation_y.parse::() { state.rotation *= Quat::from_rotation_y(rotation_y_float.to_radians()); state.axialtilt = rotation_y_float; } else { error!("Can't parse float: {line}"); continue; } } ["velocity", x, y, z] => { if let (Ok(x_float), Ok(y_float), Ok(z_float)) = (x.parse::(), y.parse::(), z.parse::()) { state.velocity = DVec3::new(x_float, y_float, z_float); } else { error!("Can't parse float: {line}"); continue; } } ["angularmomentum", x, y, z] => { if let (Ok(x_float), Ok(y_float), Ok(z_float)) = (x.parse::(), y.parse::(), z.parse::()) { state.angular_momentum = DVec3::new(x_float, y_float, z_float); } else { error!("Can't parse float: {line}"); continue; } } ["thrust", forward, back, sideways, reaction_wheels, warmup_time] => { if let ( Ok(forward_float), Ok(back_float), Ok(sideways_float), Ok(reaction_wheels_float), Ok(warmup_time_float), ) = ( forward.parse::(), back.parse::(), sideways.parse::(), reaction_wheels.parse::(), warmup_time.parse::(), ) { state.thrust_forward = forward_float; state.thrust_back = back_float; state.thrust_sideways = sideways_float; state.reaction_wheels = reaction_wheels_float; state.warmup_seconds = warmup_time_float; } } ["engine", "ion"] => { state.engine_type = actor::EngineType::Ion; } ["engine", "monopropellant"] => { state.engine_type = actor::EngineType::Monopropellant; } ["health", value] => { if let Ok(value_float) = value.parse::() { state.suit_integrity = value_float; } else { error!("Can't parse float: {line}"); continue; } } ["density", value] => { if let Ok(value_float) = value.parse::() { state.density = value_float; } else { error!("Can't parse float: {line}"); continue; } } ["physics", "off"] => { state.has_physics = false; } ["collider", "sphere", radius] => { if let Ok(radius_float) = radius.parse::() { state.collider = Collider::sphere(radius_float); } else { error!("Can't parse float: {line}"); continue; } } ["collider", "capsule", height, radius] => { if let (Ok(height_float), Ok(radius_float)) = (height.parse::(), radius.parse::()) { state.collider = Collider::capsule(height_float, radius_float); } else { error!("Can't parse float: {line}"); continue; } } ["collider", "mesh"] => { state.collider_is_mesh = true; } ["collider", "handcrafted"] => { state.collider_is_one_mesh_of_scene = true; } ["player", "yes"] => { state.is_player = true; state.is_alive = true; } ["camdistance", value] => { if let Ok(value_float) = value.parse::() { state.camdistance = value_float; } else { error!("Can't parse float: {line}"); continue; } } ["light", color_hex, brightness] => { if let Ok(brightness_float) = brightness.parse::() { if let Ok(color) = Srgba::hex(color_hex) { state.light_color = Some(Color::Srgba(color)); state.light_brightness = brightness_float; } else { error!("Can't parse hexadecimal color code: {line}"); continue; } } else { error!("Can't parse float: {line}"); continue; } } ["wants", "maxrotation", "none"] => { state.wants_maxrotation = None; } ["wants", "maxrotation", value] => { // NOTE: requires an engine to slow down velocity if let Ok(value_float) = value.parse::() { state.wants_maxrotation = Some(value_float); } else { error!("Can't parse float: {line}"); continue; } } ["wants", "maxvelocity", "none"] => { state.wants_maxvelocity = None; } ["wants", "maxvelocity", value] => { // NOTE: requires an engine to slow down velocity if let Ok(value_float) = value.parse::() { state.wants_maxvelocity = Some(value_float); } else { error!("Can't parse float: {line}"); continue; } } ["wants", "lookat", id] => { // NOTE: Will not work if the actor has no engine state.wants_tolookat_id = Some(id.to_string()); } ["wants", "matchvelocitywith", id] => { // NOTE: Will not work if the actor has no engine state.wants_matchvelocity_id = Some(id.to_string()); } ["armodel", asset_name] => { state.ar_model = Some(asset_name.to_string()); } ["targeted", "yes"] => { state.is_targeted_on_startup = true; } ["only_in_map_at_dist", value, id] => { if let Ok(value_float) = value.parse::() { state.show_only_in_map_at_distance = Some((value_float, id.to_string())); } else { error!("Can't parse float: {line}"); continue; } } _ => { error!("No match for [{}]", parts.join(",")); } } } ew_spawn.send(SpawnEvent(state)); } fn handle_spawn_events( mut er_spawn: EventReader, mut ew_spawnscene: EventWriter, mut ew_spawnactor: EventWriter, ) { for state in er_spawn.read() { match state.0.class { DefClass::Actor => { ew_spawnactor.send(SpawnActorEvent(state.0.clone())); } DefClass::Scene => { ew_spawnscene.send(SpawnSceneEvent(state.0.clone())); } DefClass::None => {} } } } fn spawn_scenes( mut er_spawnscene: EventReader, mut ew_spawn: EventWriter, ) { for state_wrapper in er_spawnscene.read() { let root_state = &state_wrapper.0; let scene_defs = include!("data/scenes.in"); for (name, template, pos, rot) in scene_defs { if Some(name.to_string()) == root_state.name { match template { "cruiser" => { let mut state = ParserState::default(); state.class = DefClass::Actor; state.pos = DVec3::new( root_state.pos[0] + pos[0], root_state.pos[1] - pos[2], root_state.pos[2] + pos[1], ); state.model = Some("cruiser".to_string()); state.rotation = Quat::from_euler(EulerRot::XYZ, rot[0], rot[1], rot[2]); // command: relativeto ? state.relative_to = root_state.relative_to.clone(); // command: name Cruiser state.name = Some("Cruiser".to_string()); // command: scale 5 state.model_scale = 5.0; // command: vehicle yes state.is_vehicle = true; // command: angularmomentum 0 0 0 state.angular_momentum = DVec3::ZERO; // command: collider handcrafted state.collider_is_one_mesh_of_scene = true; // command: thrust 16 16 8 100000 3 state.thrust_forward = 16.0; state.thrust_back = 16.0; state.thrust_sideways = 8.0; state.reaction_wheels = 100000.0; state.warmup_seconds = 3.0; // command: engine ion state.engine_type = actor::EngineType::Ion; // command: camdistance 50 state.camdistance = 50.0; // command: density 500 state.density = 500.0; // command: pointofinterest yes state.is_point_of_interest = true; ew_spawn.send(SpawnEvent(state)); } "shippingcontainer" => { let mut state = ParserState::default(); state.class = DefClass::Actor; state.pos = DVec3::new( root_state.pos[0] + pos[0], root_state.pos[1] - pos[2], root_state.pos[2] + pos[1], ); state.model = Some("shippingcontainer".to_string()); state.rotation = Quat::from_euler(EulerRot::XYZ, rot[0], rot[1], rot[2]); // command: relativeto ? state.relative_to = root_state.relative_to.clone(); // command: name Cruiser state.name = Some("Shipping Container".to_string()); // command: scale 8 state.model_scale = 8.0; // command: angularmomentum 0 0 0 state.angular_momentum = DVec3::ZERO; // command: collider handcrafted state.collider_is_one_mesh_of_scene = true; // command: density 500 state.density = 500.0; // command: pointofinterest yes ew_spawn.send(SpawnEvent(state)); } _ => {} } } } } } fn spawn_entities( mut er_spawn: EventReader, mut commands: Commands, asset_server: Res, mut meshes: ResMut>, mut materials: ResMut>, mut materials_jupiter: ResMut>, mut id2pos: ResMut, mut achievement_tracker: ResMut, mut ew_updateavatar: EventWriter, settings: Res, ) { for state_wrapper in er_spawn.read() { let jupiter_pos: DVec3 = if let Some(jupiter_pos) = id2pos.0.get(ID_JUPITER) { *jupiter_pos } else { warn!("Could not determine Jupiter's position"); DVec3::ZERO }; let state = &state_wrapper.0; let mut rotation = state.rotation; // Preprocessing let mut absolute_pos = if let Some(id) = &state.relative_to { match id2pos.0.get(&id.to_string()) { Some(pos) => state.pos + *pos, None => { error!("Specified `relativeto` command but could not find id `{id}`"); continue; } } } else { state.pos }; if let Some(r) = state.orbit_distance { let mass: Option = if let Some(id) = &state.orbit_object_id { match id.as_str() { "jupiter" => Some(nature::JUPITER_MASS), "sol" => Some(nature::JUPITER_MASS), _ => { error!("Found no mass for object `{id}`"); continue; } } } else { None }; absolute_pos += nature::pos_offset_for_orbiting_body(r, mass, state.orbit_phase); } let scale = Vec3::splat( if state.is_sun { 5.0 } else if state.is_moon && settings.large_moons { 3.0 } else { 1.0 } * state.model_scale, ); let orbits_jupiter = state.id != ID_JUPITER; let velocity = if orbits_jupiter { let coords = absolute_pos - jupiter_pos; state.velocity + nature::orbital_velocity(coords, nature::JUPITER_MASS) } else { state.velocity }; // Spawn the actor let actor_entity; let mut actor = commands.spawn_empty(); actor.insert(actor::Actor { id: state.id.clone(), name: state.name.clone(), camdistance: state.camdistance, ..default() }); actor.insert(SleepingDisabled); if orbits_jupiter { actor.insert(actor::OrbitsJupiter); } actor.insert(world::DespawnOnPlayerDeath); actor.insert(actor::HitPoints::default()); actor.insert(Position::from(absolute_pos)); if state.is_sphere { let sphere_texture_handle = if let Some(model) = &state.model { Some(asset_server.load(format!("textures/{}.jpg", model))) } else { None }; rotation = Quat::from_rotation_x(-90f32.to_radians()) * rotation; let sphere_handle = meshes.add(Sphere::new(1.0).mesh().uv(128, 128)); let sphere_material_handle = materials.add(StandardMaterial { base_color_texture: sphere_texture_handle, perceptual_roughness: 1.0, metallic: 0.0, ..default() }); actor.insert(PbrBundle { mesh: sphere_handle, material: sphere_material_handle, transform: Transform::from_scale(scale), ..default() }); } else if let Some(model) = &state.model { actor.insert(SpatialBundle { transform: Transform::from_scale(scale), ..default() }); load_asset(model.as_str(), &mut actor, &*asset_server); } actor.insert(Rotation::from(rotation)); // Physics Parameters if state.has_physics { actor.insert(RigidBody::Dynamic); actor.insert(LinearVelocity(velocity)); actor.insert(AngularVelocity(state.angular_momentum)); actor.insert(ColliderDensity(state.density)); if state.collider_is_mesh { actor.insert(MassPropertiesBundle::new_computed( &Collider::sphere(0.5 * state.model_scale as f64), state.density, )); actor.insert(AsyncSceneCollider::new(Some( ComputedCollider::TriMesh, //ComputedCollider::ConvexDecomposition(VHACDParameters::default()) ))); } else if state.collider_is_one_mesh_of_scene { actor.insert(MassPropertiesBundle::new_computed( &Collider::sphere(0.5 * state.model_scale as f64), state.density, )); actor.insert( AsyncSceneCollider::new(None) .with_shape_for_name("Collider", ComputedCollider::TriMesh) .with_layers_for_name("Collider", CollisionLayers::ALL), //.with_density_for_name("Collider", state.density) ); actor.insert(NeedsSceneColliderRemoved); } else { actor.insert(state.collider.clone()); } } // TODO: angular velocity for objects without collisions, static objects // Optional Components if state.is_player { actor.insert(actor::Player); actor.insert(actor::PlayerCamera); actor.insert(hud::AugmentedRealityOverlayBroadcaster); ew_updateavatar.send(hud::UpdateAvatarEvent); } if state.is_sun { let (r, g, b) = nature::star_color_index_to_rgb(0.656); actor.insert(materials.add(StandardMaterial { base_color: Color::srgb(r * 13.0, g * 13.0, b * 13.0), unlit: true, ..default() })); actor.insert((NotShadowCaster, NotShadowReceiver)); } if state.is_targeted_on_startup { actor.insert(hud::IsTargeted); } if let Some((mindist, id)) = &state.show_only_in_map_at_distance { actor.insert(camera::ShowOnlyInMap { min_distance: *mindist, distance_to_id: id.clone(), }); } if state.is_player || state.is_vehicle { // used to apply mouse movement to actor rotation actor.insert(ExternalTorque::ZERO.with_persistence(false)); } if state.is_lifeform { actor.insert(actor::LifeForm::default()); actor.insert(actor::ExperiencesGForce::default()); actor.insert(actor::Suit { oxygen: state.oxygen, oxygen_max: nature::OXY_D, integrity: state.suit_integrity, ..default() }); actor.insert(actor::Battery::default()); } if state.is_clickable { actor.insert(hud::IsClickable { name: state.name.clone(), pronoun: state.pronoun.clone(), ..default() }); } if let Some(value) = state.wants_maxrotation { actor.insert(actor::WantsMaxRotation(value)); } if let Some(value) = state.wants_maxvelocity { actor.insert(actor::WantsMaxVelocity(value)); } if let Some(value) = &state.wants_tolookat_id { actor.insert(actor::WantsToLookAt(value.clone())); } if let Some(value) = &state.wants_matchvelocity_id { actor.insert(actor::WantsMatchVelocityWith(value.clone())); } if let Some(color) = state.light_color { actor.insert(( PointLight { intensity: state.light_brightness, color, range: 2000.0, shadows_enabled: settings.shadows_pointlights, ..default() }, bevy::pbr::CubemapVisibleEntities::default(), bevy::render::primitives::CubemapFrusta::default(), )); } if !state.id.is_empty() { actor.insert(actor::Identifier(state.id.clone())); id2pos.0.insert(state.id.clone(), absolute_pos); } if !state.chat.is_empty() { actor.insert(chat::Talker { actor_id: state.id.clone(), chat_name: state.chat.clone(), name: state.name.clone(), pronoun: state.pronoun.clone(), talking_speed: 1.0, }); if let Some(name) = &state.name { achievement_tracker.all_people.insert(name.clone()); } } if state.is_vehicle { actor.insert(actor::Vehicle::default()); if let Some(name) = &state.name { achievement_tracker.all_vehicles.insert(name.clone()); } } if state.is_vehicle || state.is_suited || state.thrust_forward > 0.0 || state.thrust_sideways > 0.0 || state.thrust_back > 0.0 || state.reaction_wheels > 0.0 { actor.insert(actor::Engine { thrust_forward: state.thrust_forward, thrust_back: state.thrust_back, thrust_sideways: state.thrust_sideways, reaction_wheels: state.reaction_wheels, warmup_seconds: state.warmup_seconds, engine_type: state.engine_type, ..default() }); } if let Some(_) = state.ar_model { actor.insert(hud::AugmentedRealityOverlayBroadcaster); } if state.is_player { actor.with_children(|builder| { builder.spawn(( world::DespawnOnPlayerDeath, actor::PlayersFlashLight, SpotLightBundle { transform: Transform { translation: Vec3::new(0.0, 0.0, 1.0), rotation: Quat::from_rotation_y(180f32.to_radians()), ..default() }, spot_light: SpotLight { intensity: 400_000_000.0, // lumens color: Color::WHITE, shadows_enabled: true, inner_angle: PI32 / 8.0 * 0.85, outer_angle: PI32 / 4.0, range: 3000.0, ..default() }, visibility: Visibility::Hidden, ..default() }, )); }); } actor_entity = actor.id(); if let Some(ar_asset_name) = &state.ar_model { let mut entitycmd = commands.spawn(( hud::AugmentedRealityOverlay { owner: actor_entity, scale: 1.0, }, world::DespawnOnPlayerDeath, SpatialBundle { visibility: Visibility::Hidden, ..default() }, NotShadowCaster, NotShadowReceiver, )); load_asset(ar_asset_name, &mut entitycmd, &*asset_server); } if state.is_point_of_interest || state.is_moon || state.is_planet { let mut entitycmd = commands.spawn(( hud::PointOfInterestMarker(actor_entity), world::DespawnOnPlayerDeath, hud::ToggleableHudElement, SpatialBundle { visibility: Visibility::Hidden, ..default() }, NotShadowCaster, NotShadowReceiver, )); let model = if state.is_point_of_interest { "point_of_interest" } else if state.is_planet { "marker_planets" } else { "marker_satellites" }; load_asset(model, &mut entitycmd, &*asset_server); } if state.has_ring { let ring_radius = state.model_scale * (nature::JUPITER_RING_RADIUS / nature::JUPITER_RADIUS) as f32; commands.spawn(( world::DespawnOnPlayerDeath, MaterialMeshBundle { mesh: meshes.add(Mesh::from(Cylinder::new(ring_radius, 1.0))), material: materials_jupiter.add(load::JupitersRing { alpha_mode: AlphaMode::Blend, ring_radius: nature::JUPITER_RING_RADIUS as f32, jupiter_radius: nature::JUPITER_RADIUS as f32, }), transform: Transform::from_translation(absolute_pos.as_vec3()), ..default() }, Position::new(absolute_pos), Rotation::from(Quat::from_rotation_z(-state.axialtilt.to_radians())), NotShadowCaster, NotShadowReceiver, )); } } } pub fn hide_colliders( mut q_mesh: Query<(&mut Visibility, &Name), (Added, With>)>, ) { for (mut visibility, name) in &mut q_mesh { if name.as_str() == "Collider" { *visibility = Visibility::Hidden; } } } pub fn process_mesh( mut commands: Commands, mut q_mesh: Query<(Entity, &Name, &Parent), Added>>, q_parents: Query<( Option<&Parent>, Option<&actor::Player>, Option<&NotShadowCaster>, Option<&NotShadowReceiver>, )>, ) { // Add "PlayerCollider" component to the player's collider mesh entity for (child_entity, child_name, child_parent) in &mut q_mesh { // get the root parent let mut get_parent = q_parents.get(child_parent.get()); while let Ok((parent_maybe, _, _, _)) = get_parent { if let Some(parent) = parent_maybe { get_parent = q_parents.get(parent.get()); } else { break; } } if let Ok((_, player, noshadowcast, noshadowrecv)) = get_parent { let childcmd = &mut commands.entity(child_entity); // If the root parent is the player, add PlayerCollider to the collider mesh if player.is_some() && child_name.as_str() == "Collider" { childcmd.insert(actor::PlayerCollider); } if noshadowcast.is_some() { childcmd.insert(NotShadowCaster); } if noshadowrecv.is_some() { childcmd.insert(NotShadowReceiver); } } } }