extern crate regex; use crate::{actor, nature}; use regex::Regex; use bevy::prelude::*; //use bevy::core_pipeline::Skybox; //use bevy::asset::LoadState; //use bevy::render::render_resource::{TextureViewDescriptor, TextureViewDimension}; use bevy::pbr::CascadeShadowConfigBuilder; use bevy_xpbd_3d::prelude::*; use std::f32::consts::PI; const ASTEROID_SIZE: f32 = 100.0; const STARS_MAX_MAGNITUDE: f32 = 5.5; //const SKYBOX_BRIGHTNESS: f32 = 300.0; //const SKYBOX_BRIGHTNESS_AR: f32 = 100.0; //const ASSET_CUBEMAP: &str = "textures/cubemap-fs8.png"; //const ASSET_CUBEMAP_AR: &str = "textures/out.png"; const ASSET_ASTEROID1: &str = "models/asteroid.glb#Scene0"; const ASSET_ASTEROID2: &str = "models/asteroid2.glb#Scene0"; fn asset_name_to_path(name: &str) -> &'static str { match name { "suit" => "models/suit.glb#Scene0", "jupiter" => "models/jupiter.glb#Scene0", "asteroid1" => ASSET_ASTEROID1, "asteroid2" => ASSET_ASTEROID2, "moonlet" => "models/moonlet.glb#Scene0", "monolith" => "models/monolith_neon.glb#Scene0", "MeteorAceGT" => "models/MeteorAceGT.glb#Scene0", "pizzeria" => "models/pizzeria2.glb#Scene0", "pizzasign" => "models/pizzasign.glb#Scene0", _ => "models/error.glb#Scene0", } } pub struct WorldPlugin; impl Plugin for WorldPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, (setup, load_defs)); //app.add_systems(Update, asset_loaded.after(load_cubemap_asset)); //app.add_systems(Update, swap_world_on_ar_toggle); app.add_plugins(PhysicsPlugins::default()); //app.add_plugins(PhysicsDebugPlugin::default()); app.insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0))); app.insert_resource(Gravity(Vec3::splat(0.0))); } } #[derive(Component)] pub struct Star; //#[derive(Resource)] //pub struct WorldState { // is_loaded: bool, // entities_viewn_through_ar: bool, // cubemap_handle: Handle, // cubemap_ar_handle: Handle, //} //impl WorldState { // pub fn get_cubemap_handle(&self) -> Handle { // if self.entities_viewn_through_ar { // self.cubemap_ar_handle.clone() // } else { // self.cubemap_handle.clone() // } // } //} pub fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, asset_server: Res, ) { // let cubemap_handle = asset_server.load(ASSET_CUBEMAP); // commands.insert_resource(WorldState { // is_loaded: false, // entities_viewn_through_ar: settings.hud_active, // cubemap_handle: asset_server.load(ASSET_CUBEMAP), // cubemap_ar_handle: asset_server.load(ASSET_CUBEMAP_AR), // }); // Generate a bunch of asteriods let maxdist = 10; for i in -maxdist..maxdist { for j in -maxdist..maxdist { for k in -maxdist..maxdist { let offset = 500.0; let dist = 8e3; let wobble = dist/2.0; let (i, j, k) = (i as f32, j as f32, k as f32); let asset = match ((i+j+k) as i32) % 2 { 0 => ASSET_ASTEROID1, _ => ASSET_ASTEROID2, }; commands.spawn(( actor::Actor { v: Vec3::new(-0.00, 0.0, 0.35), angular_momentum: Quat::from_euler(EulerRot::XYZ, 0.001, 0.001, 0.0003), ..default() }, SceneBundle { transform: Transform { translation: Vec3::new( offset + dist * i + wobble * (j+k/PI).sin() * (k+j/PI).cos(), offset + dist * j + wobble * (k+i/PI).sin() * (i+k/PI).cos(), offset + dist * k + wobble * (i+j/PI).sin() * (j+i/PI).cos(), ), rotation: Quat::from_rotation_y(-PI / 3.), scale: Vec3::splat(ASTEROID_SIZE), }, scene: asset_server.load(asset), ..default() }, )); } } } // Generate starmap let sphere_handle = meshes.add(Sphere::new(1.0)); let mut starcount = 0; for star in nature::STARS { let mag = star[3]; if mag > STARS_MAX_MAGNITUDE { continue; } let scale_color = {|color: f32| if mag < -20.0 { color * 13.0f32 // Sun } else { color * (0.0659663 * mag * mag - 1.09862 * mag + 4.3) } }; let scale_size = {|mag: f32| if mag < -20.0 { 40000.0f32 // Sun } else { 1000.0 * (0.230299 * mag * mag - 3.09013 * mag + 15.1782) } }; let (r, g, b) = nature::star_color_index_to_rgb(star[4]); let star_color_handle = materials.add(StandardMaterial { base_color: Color::rgb(scale_color(r), scale_color(g), scale_color(b)), unlit: true, ..default() }); let dist = 1e7; commands.spawn(( Star, PbrBundle { mesh: sphere_handle.clone(), material: star_color_handle.clone(), transform: Transform::from_xyz( dist * star[0], dist * star[1], dist * star[2], ) .with_scale(Vec3::splat(scale_size(mag))), ..default() } )); starcount += 1; } info!("Generated {starcount} stars"); // Add Light from the Sun commands.spawn(DirectionalLightBundle { directional_light: DirectionalLight { illuminance: 1000.0, shadows_enabled: false, ..default() }, transform: Transform::from_rotation(Quat::from_rotation_y(PI/2.0)), cascade_shadow_config: CascadeShadowConfigBuilder { first_cascade_far_bound: 7.0, maximum_distance: 25.0, ..default() } .into(), ..default() }); } enum DefClass { Actor, Chat, None, } struct ParserState { class: DefClass, // Generic fields name: String, chat: String, // Actor fields id: String, pos: Vec3, model: String, model_scale: f32, rotation: Quat, angular_momentum: Quat, pronoun: String, is_player: bool, is_lifeform: bool, is_alive: bool, is_suited: bool, is_vehicle: bool, thrust_forward: f32, thrust_sideways: f32, thrust_back: f32, reaction_wheels: f32, warmup_seconds: f32, engine_type: actor::EngineType, oxygen: f32, mass: f32, collider: Collider, camdistance: f32, // Chat fields delay: f64, text: String, level: String, label: String, goto: String, is_choice: bool, stores_item: bool, script: String, script_parameter: String, script_parameter2: 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: "NONAME".to_string(), chat: "".to_string(), id: "".to_string(), pos: Vec3::new(0.0, 0.0, 0.0), model: "".to_string(), model_scale: 1.0, rotation: Quat::IDENTITY, angular_momentum: default_actor.angular_momentum, pronoun: "they/them".to_string(), is_player: false, is_lifeform: false, is_alive: false, is_suited: false, is_vehicle: 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, mass: 1.0, collider: Collider::sphere(1.0), camdistance: default_actor.camdistance, delay: 0.0, text: "".to_string(), level: "chat".to_string(), label: "".to_string(), goto: "".to_string(), is_choice: false, stores_item: false, script: "".to_string(), script_parameter: "".to_string(), script_parameter2: "".to_string(), } } } impl ParserState { fn reset_message(&mut self) { let default = ParserState::default(); self.label = default.label; self.delay = default.delay; self.goto = default.goto; self.level = default.level; self.text = default.text; self.is_choice = default.is_choice; } fn reset_chat(&mut self) { let default = ParserState::default(); self.reset_message(); self.stores_item = default.stores_item; } fn reset(&mut self) { *self = Self::default(); } fn as_chatbranch(&self) -> actor::ChatBranch { return actor::ChatBranch { id: self.chat.clone(), name: self.name.clone(), label: self.label.clone(), delay: self.delay.clone(), sound: if self.is_choice { "".to_string() } else { "chat".to_string() }, level: self.level.clone(), reply: if self.is_choice { "".to_string() } else { self.text.clone() }, choice: if self.is_choice { self.text.clone() } else { "".to_string() }, goto: self.goto.clone(), script: self.script.clone(), script_parameter: self.script_parameter.clone(), script_parameter2: self.script_parameter2.clone(), } } fn spawn_chat(&mut self, commands: &mut Commands) { if self.stores_item { debug!("{:#?}", self.as_chatbranch()); commands.spawn(self.as_chatbranch()); } self.reset_message(); } fn spawn_actor(&mut self, commands: &mut Commands, asset_server: &Res) { let mut actor = commands.spawn_empty(); actor.insert(actor::Actor { angular_momentum: self.angular_momentum, id: self.id.clone(), camdistance: self.camdistance, ..default() }); actor.insert(SceneBundle { transform: Transform { translation: self.pos, scale: Vec3::splat(self.model_scale), rotation: self.rotation, }, scene: asset_server.load(asset_name_to_path(self.model.as_str())), ..default() }); // Physics Parameters let fix_scale = 1.0 / self.model_scale.powf(3.0); actor.insert(RigidBody::Dynamic); actor.insert(self.collider.clone()); actor.insert(ColliderDensity(self.mass * fix_scale)); // Optional Components if self.is_player { actor.insert(actor::Player); actor.insert(actor::PlayerCamera); } if self.is_lifeform { actor.insert(actor::LifeForm::default()); actor.insert(actor::Suit { oxygen: self.oxygen, oxygen_max: nature::OXY_D, ..default() }); } if !self.chat.is_empty() { actor.insert(actor::Talker { conv_id: self.chat.clone(), ..default() }); } if self.is_vehicle { actor.insert(actor::Vehicle); } if self.is_vehicle || self.is_suited { actor.insert(actor::Engine { thrust_forward: self.thrust_forward, thrust_back: self.thrust_back, thrust_sideways: self.thrust_sideways, reaction_wheels: self.reaction_wheels, warmup_seconds: self.warmup_seconds, engine_type: self.engine_type, ..default() }); } //info!("Spawning actor {} with model {} at {}/{}/{}", // self.name, self.model, self.pos.x, self.pos.y, self.pos.z); self.reset(); } fn spawn_entities(&mut self, commands: &mut Commands, asset_server: &Res) { match self.class { DefClass::Actor => { self.spawn_actor(commands, asset_server); } DefClass::Chat => { self.spawn_chat(commands); } DefClass::None => {} } } } pub fn load_defs( mut commands: Commands, asset_server: Res, ) { let re1 = Regex::new(r"^\s*([a-z_-]+)\s+(.*)$").unwrap(); let re2 = Regex::new("\"([^\"]*)\"|(-?[0-9]+(?:\\.[0-9]+)?)|([a-zA-Z_-][a-zA-Z0-9_-]*)").unwrap(); let defs_string = include_str!("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() { // Parsing actors ["actor", x, y, z, model] => { state.spawn_entities(&mut commands, &asset_server); state.reset(); state.class = DefClass::Actor; state.model = model.to_string(); if let (Ok(x_float), Ok(y_float), Ok(z_float)) = (x.parse::(), y.parse::(), z.parse::()) { state.pos = Vec3::new(x_float, y_float, z_float); } else { error!("Can't parse coordinates as floats in def: {line}"); state.reset(); continue; } } ["id", id] => { state.id = id.to_string(); } ["alive", "yes"] => { state.is_alive = true; state.is_lifeform = true; state.is_suited = true; } ["vehicle", "yes"] => { state.is_vehicle = 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 = 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(PI * rotation_x_float); } 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(PI * rotation_y_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 = Quat::from_euler(EulerRot::XYZ, 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", "rocket"] => { state.engine_type = actor::EngineType::Rocket; } ["engine", "ion"] => { state.engine_type = actor::EngineType::Ion; } ["mass", value] => { if let Ok(value_float) = value.parse::() { state.mass = value_float; } else { error!("Can't parse float: {line}"); continue; } } ["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; } } ["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; } } // Parsing chats ["chat", chat_name] => { debug!("Registering chat: {}", chat_name); state.spawn_entities(&mut commands, &asset_server); state.reset_chat(); state.class = DefClass::Chat; state.chat = chat_name.to_string(); } ["name", name] => { debug!("Registering name: {}", name); state.name = name.to_string(); } ["msg", sleep, text] => { debug!("Registering message (sleep={}): {}", sleep, text); state.spawn_entities(&mut commands, &asset_server); if let Ok(sleep_float) = sleep.parse::() { state.delay = sleep_float; state.text = text.to_string(); state.stores_item = true; state.is_choice = false; } else { error!("The 'sleep' value for this message is not a float: {}", line); continue; } } ["msg", sleep, label, goto, text] => { debug!("Registering message (sleep={}): {}", sleep, text); state.spawn_entities(&mut commands, &asset_server); if let Ok(sleep_float) = sleep.parse::() { state.delay = sleep_float; state.text = text.to_string(); state.stores_item = true; state.is_choice = false; state.goto = goto.to_string(); state.label = label.to_string(); } else { error!("The 'sleep' value for this message is not a float: {}", line); continue; } } ["choice", sleep, text] => { debug!("Registering choice (sleep={}): {}", sleep, text); state.spawn_entities(&mut commands, &asset_server); if let Ok(sleep_float) = sleep.parse::() { state.delay = sleep_float; state.text = text.to_string(); state.stores_item = true; state.is_choice = true; } else { error!("The 'sleep' value for this message is not a float: {}", line); continue; } } ["choice", sleep, label, goto, text] => { debug!("Registering choice (sleep={}): {}", sleep, text); state.spawn_entities(&mut commands, &asset_server); if let Ok(sleep_float) = sleep.parse::() { state.delay = sleep_float; state.text = text.to_string(); state.stores_item = true; state.is_choice = true; state.goto = goto.to_string(); state.label = label.to_string(); } else { error!("The 'sleep' value for this message is not a float: {}", line); continue; } } ["goto", label] => { debug!("Registering goto: {}", label); state.goto = label.to_string(); } ["label", label] => { debug!("Registering label: {}", label); state.label = label.to_string(); } ["lvl", level] => { debug!("Registering level: {}", level); state.level = level.to_string(); } ["script", scriptname, parameter] => { state.script = scriptname.to_string(); state.script_parameter = parameter.to_string(); state.script_parameter2 = "".to_string(); } ["script", scriptname, parameter, parameter2] => { state.script = scriptname.to_string(); state.script_parameter = parameter.to_string(); state.script_parameter2 = parameter2.to_string(); } _ => { error!("No match for [{}]", parts.join(",")); } } } state.spawn_entities(&mut commands, &asset_server); } //pub fn swap_world_on_ar_toggle( // asset_server: Res, // mut images: ResMut>, // mut worldstate: ResMut, // mut skyboxes: Query<&mut Skybox>, // settings: Res, //) { // if settings.hud_active != worldstate.entities_viewn_through_ar { // worldstate.is_loaded = false; // worldstate.entities_viewn_through_ar = settings.hud_active; // } // if !worldstate.is_loaded && asset_server.load_state(&worldstate.get_cubemap_handle()) == LoadState::Loaded { // let cubemap_handle = &worldstate.get_cubemap_handle(); // let image = images.get_mut(cubemap_handle).unwrap(); // if image.texture_descriptor.array_layer_count() == 1 { // image.reinterpret_stacked_2d_as_array(image.height() / image.width()); // image.texture_view_descriptor = Some(TextureViewDescriptor { // dimension: Some(TextureViewDimension::Cube), // ..default() // }); // } // // for mut skybox in &mut skyboxes { // skybox.image = cubemap_handle.clone(); // skybox.brightness = if settings.hud_active { // SKYBOX_BRIGHTNESS_AR // } else { // SKYBOX_BRIGHTNESS // } // } // // worldstate.is_loaded = true; // } //}