// ▄████████▄ + ███ + ▄█████████ ███ + // ███▀ ▀███ + + ███ ███▀ + ███ + + // ███ + ███ ███ ███ █████████ ███ ███ ███ ███ // ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███ // ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███ // ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███ // ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████ // + + + ███ // + ▀████████████████████████████████████████████████████▀ // // This module handles player input, and coordinates interplay between other modules use crate::prelude::*; use actor::PlayerCamera; use bevy::color::palettes::css; use bevy::prelude::*; use bevy::scene::SceneInstance; use bevy::window::{PrimaryWindow, Window, WindowMode}; use bevy_xpbd_3d::prelude::*; use std::collections::HashMap; pub const CHEAT_WARP_1: &str = "pizzeria"; pub const CHEAT_WARP_2: &str = "busstopclippy2"; pub const CHEAT_WARP_3: &str = "busstopclippy3"; pub const RACE_TARGET_RADIUS: f64 = 5.0; pub struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup); app.add_systems( Update, handle_cheats.run_if(game_running).run_if(in_control), ); app.add_systems(Update, debug.run_if(game_running)); app.add_systems(PostUpdate, handle_game_event); app.add_systems(Update, handle_window_focus); app.add_systems(PreUpdate, handle_player_death); app.add_systems( PostUpdate, update_id2pos .run_if(game_running) .in_set(bevy_xpbd_3d::plugins::sync::SyncSet::PositionToTransform), ); app.add_systems(PostUpdate, update_id2v); app.add_systems( Update, handle_achievement_event .run_if(game_running) .run_if(on_event::()), ); app.add_systems(Update, check_achievements.run_if(game_running)); app.add_systems(Update, handle_race.run_if(game_running)); app.insert_resource(Id2Pos(HashMap::new())); app.insert_resource(Id2V(HashMap::new())); app.insert_resource(JupiterPos(DVec3::ZERO)); app.insert_resource(var::AchievementTracker::default()); app.insert_resource(var::Settings::default()); app.insert_resource(var::GameVars::default()); app.insert_resource(RaceState::default()); app.insert_resource(AchievementCheckTimer(Timer::from_seconds( 1.0, TimerMode::Repeating, ))); app.add_event::(); app.add_event::(); app.add_event::(); } } #[derive(Event)] pub struct PlayerDiesEvent(pub actor::DamageType); #[derive(Resource)] pub struct Id2Pos(pub HashMap); #[derive(Resource)] pub struct Id2V(pub HashMap); #[derive(Resource)] pub struct JupiterPos(pub DVec3); #[derive(Resource)] pub struct AchievementCheckTimer(pub Timer); #[derive(Resource, Default)] pub struct RaceState { pub initialized: bool, pub started: bool, pub start_countdown: f64, pub timeout: f64, pub score: u64, } #[derive(Component)] pub struct RaceTarget; #[derive(Event)] pub enum AchievementEvent { RepairSuit, TalkTo(String), RideVehicle(String), DrinkPizza, InJupitersShadow, FindEarth, } #[derive(Event)] pub enum GameEvent { SetAR(Turn), SetMusic(Cycle), SetSound(Cycle), SetMap(Turn), SetFullscreen(Turn), SetMenu(Turn), SetThirdPerson(Turn), SetRotationStabilizer(Turn), SetShadows(Turn), UpdateFlashlight, Achievement(String), PhoneCall, } pub enum Turn { On, Off, Toggle, Unchanged, } impl Turn { pub fn to_bool(&self, current_state: bool) -> bool { match self { Turn::On => true, Turn::Off => false, Turn::Toggle => !current_state, Turn::Unchanged => current_state, } } } pub enum Cycle { First, Last, Next, Previous, Unchanged, } impl Cycle { pub fn to_index(&self, current_index: usize, vector: &Vec) -> Option { if vector.is_empty() { return None; } match self { Cycle::First => Some(0), Cycle::Last => Some(vector.len() - 1), Cycle::Next => { let index = current_index.saturating_add(1); if index >= vector.len() { Some(0) } else { Some(index) } } Cycle::Previous => { if current_index == 0 { Some(vector.len() - 1) } else { Some(current_index - 1) } } Cycle::Unchanged => Some(current_index), } } } pub fn setup( mut commands: Commands, mut settings: ResMut, prefs: ResMut, asset_server: Res, ) { settings.hud_active = prefs.augmented_reality; settings.radio_mode = prefs.radio_station; settings.set_noise_cancellation_mode(prefs.noise_cancellation_mode); settings.third_person = prefs.third_person; settings.shadows_sun = prefs.shadows_sun; settings.ar_avatar = prefs.avatar; // Setup Race let mut entitycmd = commands.spawn(( RaceTarget, actor::OrbitsJupiter, bevy::pbr::NotShadowCaster, bevy::pbr::NotShadowReceiver, Position::default(), actor::WantsToLookAt(cmd::ID_SPECIAL_PLAYERCAM.to_string()), LinearVelocity::default(), SpatialBundle { transform: Transform::from_scale(Vec3::splat(RACE_TARGET_RADIUS as f32)), visibility: Visibility::Hidden, ..default() }, )); load_asset("marker_race", &mut entitycmd, &*asset_server); } pub fn handle_game_event( mut settings: ResMut, mut er_game: EventReader, mut ew_conv: EventWriter, mut ew_sfx: EventWriter, mut ew_updateoverlays: EventWriter, mut ew_updatemenu: EventWriter, mut ew_togglemusic: EventWriter, mut q_window: Query<&mut Window, With>, mut q_light: Query<&mut DirectionalLight>, mut q_flashlight: Query<(&mut Visibility, &mut SpotLight), With>, mut mapcam: ResMut, mut log: ResMut, opt: Res, mut prefs: ResMut, ) { for event in er_game.read() { match event { GameEvent::SetAR(turn) => { settings.hud_active = turn.to_bool(settings.hud_active); ew_togglemusic.send(audio::ToggleMusicEvent()); ew_updateoverlays.send(hud::UpdateOverlayVisibility); prefs.augmented_reality = settings.hud_active; prefs.save(); } GameEvent::SetMusic(cycle) => { match cycle.to_index(settings.radio_mode, &settings.radio_modes) { Some(mode) => { settings.radio_mode = mode; } None => {} } ew_togglemusic.send(audio::ToggleMusicEvent()); prefs.radio_station = settings.radio_mode; prefs.save(); } GameEvent::SetSound(cycle) => { match cycle.to_index( settings.noise_cancellation_mode, &settings.noise_cancellation_modes, ) { Some(mode) => { settings.set_noise_cancellation_mode(mode); } None => {} } ew_togglemusic.send(audio::ToggleMusicEvent()); prefs.noise_cancellation_mode = settings.noise_cancellation_mode; prefs.save(); } GameEvent::SetMap(turn) => { settings.map_active = turn.to_bool(settings.map_active); if settings.map_active { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Woosh)); } *mapcam = camera::MapCam::default(); ew_updateoverlays.send(hud::UpdateOverlayVisibility); } GameEvent::SetFullscreen(turn) => { for mut window in &mut q_window { let current_state = window.mode != WindowMode::Windowed; prefs.fullscreen_on = turn.to_bool(current_state); window.mode = match prefs.fullscreen_on { true => opt.window_mode_fullscreen, false => WindowMode::Windowed, }; prefs.save(); } } GameEvent::SetMenu(turn) => { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click)); settings.menu_active = turn.to_bool(settings.menu_active); for mut window in &mut q_window { window.cursor.grab_mode = if settings.menu_active { bevy::window::CursorGrabMode::None } else { bevy::window::CursorGrabMode::Locked }; window.cursor.visible = settings.menu_active; } ew_updatemenu.send(menu::UpdateMenuEvent); } GameEvent::SetThirdPerson(turn) => { settings.third_person = turn.to_bool(settings.third_person); prefs.third_person = settings.third_person; prefs.save(); } GameEvent::SetRotationStabilizer(turn) => { settings.rotation_stabilizer_active = turn.to_bool(settings.rotation_stabilizer_active); } GameEvent::SetShadows(turn) => { settings.shadows_sun = turn.to_bool(settings.shadows_sun); for mut light in &mut q_light { light.shadows_enabled = settings.shadows_sun; } prefs.shadows_sun = settings.shadows_sun; prefs.save(); } GameEvent::Achievement(name) => { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Achieve)); log.add( format!("Achievement accomplished: {name}!"), "".to_string(), hud::LogLevel::Achievement, ); } GameEvent::UpdateFlashlight => { for (mut visibility, mut spotlight) in &mut q_flashlight { spotlight.intensity = actor::FLASHLIGHT_INTENSITY[prefs.flashlight_power]; *visibility = bool2vis(settings.flashlight_active); } } GameEvent::PhoneCall => { let talker = chat::Talker { chat_name: "phone".to_string(), actor_id: "".to_string(), name: Some("Phone".to_string()), counts_towards_achievement: false, pronoun: None, talking_speed: 0.0, }; ew_conv.send(chat::StartConversationEvent { talker: talker }); } } } } fn handle_player_death( mut cmd: Commands, mut er_playerdies: EventReader, q_scenes: Query<(Entity, &SceneInstance), With>, q_noscenes: Query, Without)>, mut scene_spawner: ResMut, mut active_asteroids: ResMut, mut ew_effect: EventWriter, mut ew_deathscreen: EventWriter, mut log: ResMut, mut gamevars: ResMut, mut settings: ResMut, ) { for death in er_playerdies.read() { if settings.god_mode { return; } settings.reset_player_settings(); gamevars.reset(); active_asteroids.0.clear(); for entity in &q_noscenes { cmd.entity(entity).despawn(); } for (entity, sceneinstance) in &q_scenes { cmd.entity(entity).despawn(); scene_spawner.despawn_instance(**sceneinstance); } log.clear(); match death.0 { actor::DamageType::Depressurization => { settings.death_cause = "Depressurization".to_string(); ew_effect.send(visual::SpawnEffectEvent { class: visual::Effects::FadeIn(Color::BLACK), duration: 4.0, }); } actor::DamageType::Mental => { settings.death_cause = "Brain Damage".to_string(); ew_effect.send(visual::SpawnEffectEvent { class: visual::Effects::FadeIn(Color::BLACK), duration: 4.0, }); } actor::DamageType::Asphyxiation => { settings.death_cause = "Suffocation".to_string(); ew_effect.send(visual::SpawnEffectEvent { class: visual::Effects::FadeIn(Color::BLACK), duration: 1.0, }); } actor::DamageType::Trauma => { settings.death_cause = "Trauma".to_string(); ew_effect.send(visual::SpawnEffectEvent { class: visual::Effects::FadeIn(css::MAROON.into()), duration: 1.0, }); } actor::DamageType::GForce => { settings.death_cause = "Trauma from excessive g forces".to_string(); ew_effect.send(visual::SpawnEffectEvent { class: visual::Effects::FadeIn(css::MAROON.into()), duration: 1.0, }); } actor::DamageType::Radiation => { settings.death_cause = "Acute radiation poisoning".to_string(); ew_effect.send(visual::SpawnEffectEvent { class: visual::Effects::FadeIn(Color::BLACK), duration: 4.0, }); } actor::DamageType::Unknown => { settings.death_cause = "Unknown".to_string(); ew_effect.send(visual::SpawnEffectEvent { class: visual::Effects::FadeIn(css::MAROON.into()), duration: 1.0, }); } } ew_deathscreen.send(menu::DeathScreenEvent::Show); return; } } fn handle_cheats( key_input: Res>, mut q_player: Query< (&Transform, &mut Position, &mut LinearVelocity), With, >, mut q_life: Query<(&mut actor::LifeForm, &mut actor::ExperiencesGForce), With>, q_target: Query< (&Transform, &Position, Option<&LinearVelocity>), (With, Without), >, mut ew_playerdies: EventWriter, mut settings: ResMut, jupiter_pos: Res, id2pos: Res, id2v: Res, mut ew_sfx: EventWriter, ) { if q_player.is_empty() || q_life.is_empty() { return; } let (trans, mut pos, mut v) = q_player.get_single_mut().unwrap(); let (mut lifeform, mut gforce) = q_life.get_single_mut().unwrap(); let boost = if key_input.pressed(KeyCode::ShiftLeft) { 1e6 } else { 1e3 }; if key_input.just_pressed(settings.key_cheat_god_mode) { settings.god_mode ^= true; if settings.god_mode { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Honk)); } else { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch)); } } if !settings.god_mode && !settings.dev_mode { return; } if key_input.just_pressed(settings.key_cheat_stop) { gforce.ignore_gforce_seconds = 1.0; v.0 = nature::orbital_velocity(pos.0 - jupiter_pos.0, nature::JUPITER_MASS); } if key_input.pressed(settings.key_cheat_speed) || key_input.pressed(settings.key_cheat_speed_backward) { gforce.ignore_gforce_seconds = 1.0; let sign = if key_input.pressed(settings.key_cheat_speed) { 1.0 } else { -1.0 }; let dv = DVec3::from(trans.rotation * Vec3::new(0.0, 0.0, sign * boost)); let current_speed = v.0.length(); let next_speed = (v.0 + dv).length(); let avg_speed = (current_speed + next_speed) / 2.0; let inv_lorentz = nature::inverse_lorentz_factor(avg_speed.clamp(0.0, nature::C)); v.0 = v.0 + inv_lorentz * dv; } if key_input.just_pressed(settings.key_cheat_teleport) { if let Ok((transform, target_pos, target_v)) = q_target.get_single() { gforce.ignore_gforce_seconds = 1.0; let offset: DVec3 = 4.0 * (**pos - **target_pos).normalize() * transform.scale.as_dvec3(); pos.0 = **target_pos + offset; if let Some(target_v) = target_v { *v = target_v.clone(); } } } if !settings.dev_mode { return; } if key_input.just_pressed(settings.key_cheat_pizza) { if let Some(target) = id2pos.0.get(&CHEAT_WARP_1.to_string()) { pos.0 = *target + DVec3::new(-60.0, 0.0, 0.0); gforce.ignore_gforce_seconds = 1.0; } if let Some(target) = id2v.0.get(&CHEAT_WARP_1.to_string()) { v.0 = *target; } } if key_input.just_pressed(settings.key_cheat_farview1) { if let Some(target) = id2pos.0.get(&CHEAT_WARP_2.to_string()) { pos.0 = *target + DVec3::new(0.0, -1000.0, 0.0); gforce.ignore_gforce_seconds = 1.0; } if let Some(target) = id2v.0.get(&CHEAT_WARP_2.to_string()) { v.0 = *target; } } if key_input.just_pressed(settings.key_cheat_farview2) { if let Some(target) = id2pos.0.get(&CHEAT_WARP_3.to_string()) { pos.0 = *target + DVec3::new(0.0, -1000.0, 0.0); gforce.ignore_gforce_seconds = 1.0; } if let Some(target) = id2v.0.get(&CHEAT_WARP_3.to_string()) { v.0 = *target; } } if key_input.pressed(settings.key_cheat_adrenaline_zero) { lifeform.adrenaline = 0.0; } if key_input.pressed(settings.key_cheat_adrenaline_mid) { lifeform.adrenaline = 0.5; } if key_input.pressed(settings.key_cheat_adrenaline_max) { lifeform.adrenaline = 1.0; } if key_input.just_pressed(settings.key_cheat_die) { settings.god_mode = false; ew_playerdies.send(PlayerDiesEvent(actor::DamageType::Trauma)); } } fn update_id2pos( mut id2pos: ResMut, mut jupiterpos: ResMut, q_id: Query<(&Position, &actor::Identifier)>, ) { id2pos.0.clear(); for (pos, id) in &q_id { id2pos.0.insert(id.0.clone(), pos.0); if id.0 == cmd::ID_JUPITER { jupiterpos.0 = pos.0; } } } fn update_id2v(mut id2v: ResMut, q_id: Query<(&LinearVelocity, &actor::Identifier)>) { id2v.0.clear(); for (v, id) in &q_id { id2v.0.insert(id.0.clone(), v.0); } } fn debug( settings: Res, keyboard_input: Res>, // mut commands: Commands, // mut extended_materials: ResMut< // Assets>, // >, mut achievement_tracker: ResMut, vars: Res, // materials: Query<(Entity, Option<&Name>, &Handle)>, ) { if settings.dev_mode && keyboard_input.just_pressed(KeyCode::KeyP) { // for (entity, _name, mesh) in &materials { // dbg!(mesh); // let mut entity = commands.entity(entity); // entity.remove::>(); // let material = extended_materials.add(load::AsteroidSurface::material()); // entity.insert(material); // } } if settings.dev_mode && keyboard_input.just_pressed(KeyCode::KeyN) { achievement_tracker.achieve_all(); dbg!(&vars); } } fn handle_achievement_event( mut er_achievement: EventReader, mut ew_game: EventWriter, mut tracker: ResMut, ) { for event in er_achievement.read() { match event { AchievementEvent::RepairSuit => { if !tracker.repair_suit { ew_game.send(GameEvent::Achievement("Repair Your Suit".into())); } tracker.repair_suit = true; } AchievementEvent::InJupitersShadow => { if !tracker.in_jupiters_shadow { ew_game.send(GameEvent::Achievement("Enter Jupiter's Shadow".into())); } tracker.in_jupiters_shadow = true; } AchievementEvent::DrinkPizza => { if !tracker.drink_a_pizza { ew_game.send(GameEvent::Achievement("Enjoy A Pizza".into())); } tracker.drink_a_pizza = true; } AchievementEvent::FindEarth => { if !tracker.find_earth { ew_game.send(GameEvent::Achievement("Find Earth".into())); } tracker.find_earth = true; } AchievementEvent::RideVehicle(name) => { tracker.vehicles_ridden.insert(name.clone()); let len = tracker.vehicles_ridden.len(); let total = tracker.all_vehicles.len(); if !tracker.ride_every_vehicle && len == total { tracker.ride_every_vehicle = true; ew_game.send(GameEvent::Achievement("Ride Every Vehicle".into())); } } AchievementEvent::TalkTo(name) => { tracker.people_talked_to.insert(name.clone()); let len = tracker.people_talked_to.len(); let total = tracker.all_people.len(); if !tracker.talk_to_everyone && len == total { tracker.talk_to_everyone = true; ew_game.send(GameEvent::Achievement("Talk To Everyone".into())); } } } } } fn check_achievements( time: Res