diff --git a/art/foxtrot.blend b/art/foxtrot.blend index e2903996..0c466982 100644 Binary files a/art/foxtrot.blend and b/art/foxtrot.blend differ diff --git a/assets/blueprints/Box.glb b/assets/blueprints/Box.glb index 8ac2f809..3d9f34bd 100644 Binary files a/assets/blueprints/Box.glb and b/assets/blueprints/Box.glb differ diff --git a/assets/levels/World.glb b/assets/levels/World.glb index c08f13fd..aa553e8c 100644 Binary files a/assets/levels/World.glb and b/assets/levels/World.glb differ diff --git a/assets/levels/World.meta.ron b/assets/levels/World.meta.ron index 804f8076..e1020a07 100644 --- a/assets/levels/World.meta.ron +++ b/assets/levels/World.meta.ron @@ -19,65 +19,65 @@ ("citybits_texture", File ( path: "materials/citybits_texture.glb" )), ("Car Blue", File ( path: "blueprints/Car Blue.glb" )), ("TailLights", File ( path: "materials/TailLights.glb" )), + ("Blue", File ( path: "materials/Blue.glb" )), ("Black", File ( path: "materials/Black.glb" )), + ("Windows", File ( path: "materials/Windows.glb" )), ("Grey", File ( path: "materials/Grey.glb" )), ("Headlights", File ( path: "materials/Headlights.glb" )), - ("Blue", File ( path: "materials/Blue.glb" )), - ("Windows", File ( path: "materials/Windows.glb" )), ("Building Red", File ( path: "blueprints/Building Red.glb" )), - ("brick_shade1", File ( path: "materials/brick_shade1.glb" )), - ("brick_shade2", File ( path: "materials/brick_shade2.glb" )), ("concrete", File ( path: "materials/concrete.glb" )), - ("wood", File ( path: "materials/wood.glb" )), - ("window_glass", File ( path: "materials/window_glass.glb" )), - ("sandstone", File ( path: "materials/sandstone.glb" )), ("window_frame", File ( path: "materials/window_frame.glb" )), - ("Building Red Corner", File ( path: "blueprints/Building Red Corner.glb" )), - ("brick_shade1", File ( path: "materials/brick_shade1.glb" )), ("brick_shade2", File ( path: "materials/brick_shade2.glb" )), + ("brick_shade1", File ( path: "materials/brick_shade1.glb" )), + ("window_glass", File ( path: "materials/window_glass.glb" )), ("wood", File ( path: "materials/wood.glb" )), + ("sandstone", File ( path: "materials/sandstone.glb" )), + ("Building Red Corner", File ( path: "blueprints/Building Red Corner.glb" )), ("concrete", File ( path: "materials/concrete.glb" )), + ("window_frame", File ( path: "materials/window_frame.glb" )), + ("brick_shade2", File ( path: "materials/brick_shade2.glb" )), + ("brick_shade1", File ( path: "materials/brick_shade1.glb" )), ("window_glass", File ( path: "materials/window_glass.glb" )), + ("wood", File ( path: "materials/wood.glb" )), ("sandstone", File ( path: "materials/sandstone.glb" )), - ("window_frame", File ( path: "materials/window_frame.glb" )), ("Building Beige", File ( path: "blueprints/Building Beige.glb" )), ("concrete", File ( path: "materials/concrete.glb" )), - ("window_glass", File ( path: "materials/window_glass.glb" )), + ("window_frame", File ( path: "materials/window_frame.glb" )), ("door_white", File ( path: "materials/door_white.glb" )), - ("brick_shadeGreen", File ( path: "materials/brick_shadeGreen.glb" )), ("brick_shadeGreen2", File ( path: "materials/brick_shadeGreen2.glb" )), + ("window_glass", File ( path: "materials/window_glass.glb" )), + ("brick_shadeGreen", File ( path: "materials/brick_shadeGreen.glb" )), ("sandstone", File ( path: "materials/sandstone.glb" )), - ("window_frame", File ( path: "materials/window_frame.glb" )), ("Building Beige Corner Pizza", File ( path: "blueprints/Building Beige Corner Pizza.glb" )), ("concrete", File ( path: "materials/concrete.glb" )), - ("window_glass", File ( path: "materials/window_glass.glb" )), + ("window_frame", File ( path: "materials/window_frame.glb" )), ("door_white", File ( path: "materials/door_white.glb" )), - ("brick_shadeGreen", File ( path: "materials/brick_shadeGreen.glb" )), - ("White", File ( path: "materials/White.glb" )), ("red", File ( path: "materials/red.glb" )), + ("White", File ( path: "materials/White.glb" )), ("brick_shadeGreen2", File ( path: "materials/brick_shadeGreen2.glb" )), + ("window_glass", File ( path: "materials/window_glass.glb" )), + ("brick_shadeGreen", File ( path: "materials/brick_shadeGreen.glb" )), ("sandstone", File ( path: "materials/sandstone.glb" )), - ("window_frame", File ( path: "materials/window_frame.glb" )), ("Building Black", File ( path: "blueprints/Building Black.glb" )), - ("brick_shade3", File ( path: "materials/brick_shade3.glb" )), ("concrete", File ( path: "materials/concrete.glb" )), + ("window_frame", File ( path: "materials/window_frame.glb" )), ("window_glass", File ( path: "materials/window_glass.glb" )), + ("brick_shade3", File ( path: "materials/brick_shade3.glb" )), ("sandstone", File ( path: "materials/sandstone.glb" )), - ("window_frame", File ( path: "materials/window_frame.glb" )), ("Building Red Barren", File ( path: "blueprints/Building Red Barren.glb" )), - ("brick_shade1", File ( path: "materials/brick_shade1.glb" )), - ("brick_shade2", File ( path: "materials/brick_shade2.glb" )), ("concrete", File ( path: "materials/concrete.glb" )), + ("window_frame", File ( path: "materials/window_frame.glb" )), + ("brick_shade2", File ( path: "materials/brick_shade2.glb" )), + ("brick_shade1", File ( path: "materials/brick_shade1.glb" )), ("window_glass", File ( path: "materials/window_glass.glb" )), ("sandstone", File ( path: "materials/sandstone.glb" )), - ("window_frame", File ( path: "materials/window_frame.glb" )), ("Buliding Big", File ( path: "blueprints/Buliding Big.glb" )), ("LightYellow", File ( path: "materials/LightYellow.glb" )), + ("DarkGrey", File ( path: "materials/DarkGrey.glb" )), + ("Beige", File ( path: "materials/Beige.glb" )), + ("Brown", File ( path: "materials/Brown.glb" )), ("Grey", File ( path: "materials/Grey.glb" )), ("BrickRed", File ( path: "materials/BrickRed.glb" )), - ("Brown", File ( path: "materials/Brown.glb" )), - ("Beige", File ( path: "materials/Beige.glb" )), - ("DarkGrey", File ( path: "materials/DarkGrey.glb" )), ("Air Conditioner", File ( path: "blueprints/Air Conditioner.glb" )), ("Mat", File ( path: "materials/Mat.glb" )), ("Fire Exit", File ( path: "blueprints/Fire Exit.glb" )), @@ -89,43 +89,43 @@ ("Traffic Light", File ( path: "blueprints/Traffic Light.glb" )), ("Atlas.052", File ( path: "materials/Atlas.052.glb" )), ("Mailbox", File ( path: "blueprints/Mailbox.glb" )), - ("sign_shade2", File ( path: "materials/sign_shade2.glb" )), ("mailbox", File ( path: "materials/mailbox.glb" )), + ("sign_shade2", File ( path: "materials/sign_shade2.glb" )), ("sign", File ( path: "materials/sign.glb" )), ("Trash Container", File ( path: "blueprints/Trash Container.glb" )), - ("Grey", File ( path: "materials/Grey.glb" )), ("DarkGrey", File ( path: "materials/DarkGrey.glb" )), ("Green", File ( path: "materials/Green.glb" )), + ("Grey", File ( path: "materials/Grey.glb" )), ("Cone", File ( path: "blueprints/Cone.glb" )), - ("Dark_gray", File ( path: "materials/Dark_gray.glb" )), ("Black", File ( path: "materials/Black.glb" )), ("Orange", File ( path: "materials/Orange.glb" )), ("White", File ( path: "materials/White.glb" )), + ("Dark_gray", File ( path: "materials/Dark_gray.glb" )), ("Trash Bag", File ( path: "blueprints/Trash Bag.glb" )), ("mat17", File ( path: "materials/mat17.glb" )), ("Bus Stop", File ( path: "blueprints/Bus Stop.glb" )), ("LightBlue_BusStop", File ( path: "materials/LightBlue_BusStop.glb" )), ("BlueBusStop", File ( path: "materials/BlueBusStop.glb" )), ("Bus Stop Sign", File ( path: "blueprints/Bus Stop Sign.glb" )), - ("Black", File ( path: "materials/Black.glb" )), ("Orang", File ( path: "materials/Orang.glb" )), - ("Grey", File ( path: "materials/Grey.glb" )), - ("White", File ( path: "materials/White.glb" )), ("Silver", File ( path: "materials/Silver.glb" )), + ("White", File ( path: "materials/White.glb" )), + ("Black", File ( path: "materials/Black.glb" )), + ("Grey", File ( path: "materials/Grey.glb" )), ("Box", File ( path: "blueprints/Box.glb" )), ("citybits_texture", File ( path: "materials/citybits_texture.glb" )), ("Fence Piece", File ( path: "blueprints/Fence Piece.glb" )), - ("brick_shade1", File ( path: "materials/brick_shade1.glb" )), - ("sandstone_light", File ( path: "materials/sandstone_light.glb" )), ("brick_shade2", File ( path: "materials/brick_shade2.glb" )), + ("brick_shade1", File ( path: "materials/brick_shade1.glb" )), ("metal_fence", File ( path: "materials/metal_fence.glb" )), + ("sandstone_light", File ( path: "materials/sandstone_light.glb" )), ("Grass", File ( path: "blueprints/Grass.glb" )), ("Grass", File ( path: "materials/Grass.glb" )), ("Fence End", File ( path: "blueprints/Fence End.glb" )), - ("brick_shade1", File ( path: "materials/brick_shade1.glb" )), - ("sandstone_light", File ( path: "materials/sandstone_light.glb" )), ("brick_shade2", File ( path: "materials/brick_shade2.glb" )), + ("brick_shade1", File ( path: "materials/brick_shade1.glb" )), ("metal_fence", File ( path: "materials/metal_fence.glb" )), + ("sandstone_light", File ( path: "materials/sandstone_light.glb" )), ("Bench", File ( path: "blueprints/Bench.glb" )), ("mat19", File ( path: "materials/mat19.glb" )), ("mat20", File ( path: "materials/mat20.glb" )), @@ -136,20 +136,20 @@ ("Metal Fence", File ( path: "blueprints/Metal Fence.glb" )), ("Silver", File ( path: "materials/Silver.glb" )), ("Overpass Tunnel", File ( path: "blueprints/Overpass Tunnel.glb" )), - ("citybits_texture", File ( path: "materials/citybits_texture.glb" )), ("Black", File ( path: "materials/Black.glb" )), ("concrete", File ( path: "materials/concrete.glb" )), - ("Overpass Block", File ( path: "blueprints/Overpass Block.glb" )), ("citybits_texture", File ( path: "materials/citybits_texture.glb" )), + ("Overpass Block", File ( path: "blueprints/Overpass Block.glb" )), ("concrete", File ( path: "materials/concrete.glb" )), + ("citybits_texture", File ( path: "materials/citybits_texture.glb" )), ("Npc Pizza", File ( path: "blueprints/Npc Pizza.glb" )), ("Woman", File ( path: "materials/Woman.glb" )), ("Npc Mail", File ( path: "blueprints/Npc Mail.glb" )), - ("Pants", File ( path: "materials/Pants.glb" )), ("Eyes", File ( path: "materials/Eyes.glb" )), - ("Skin", File ( path: "materials/Skin.glb" )), + ("Pants", File ( path: "materials/Pants.glb" )), ("Hair", File ( path: "materials/Hair.glb" )), - ("Shirt", File ( path: "materials/Shirt.glb" )), ("Socks", File ( path: "materials/Socks.glb" )), + ("Shirt", File ( path: "materials/Shirt.glb" )), + ("Skin", File ( path: "materials/Skin.glb" )), ] ) \ No newline at end of file diff --git a/src/opportunities/available_opportunities.rs b/src/opportunities/available_opportunities.rs index f6c6fe9d..3775fd47 100644 --- a/src/opportunities/available_opportunities.rs +++ b/src/opportunities/available_opportunities.rs @@ -4,23 +4,24 @@ use crate::{ player::{camera::PlayerCamera, Player}, }; use avian3d::prelude::*; -use bevy::prelude::*; +use bevy::{ + ecs::component::{ComponentHooks, StorageType}, + prelude::*, +}; use super::OpportunitySystem; pub(super) fn plugin(app: &mut App) { - app.add_systems( - Update, - update_available_opportunities - .run_if(not(dialog_running)) - .in_set(OpportunitySystem::UpdateAvailableOpportunities), - ); - app.register_type::(); + app.register_type::<( + PlayerInteractionParameters, + AvailablePlayerInteraction, + PlayerInteraction, + )>(); } #[derive(Debug, Component, PartialEq, Eq, Clone, Default, Deref, DerefMut, Reflect)] #[reflect(Component, PartialEq, Default)] -pub struct ActiveInteractable(pub Option); +pub struct AvailablePlayerInteraction(pub Option); /// The general idea is as follows: /// This component sits on a collider for an interactable object, e.g. a door or a character. @@ -29,57 +30,42 @@ pub struct ActiveInteractable(pub Option); /// If so, we have an interaction opportunity. #[derive(Debug, Component, PartialEq, Clone, Reflect)] #[reflect(Component, PartialEq)] -pub struct PlayerInteractable { +pub struct PlayerInteractionParameters { /// The prompt to display when the opportunity is available. pub prompt: String, - /// The opportunity to activate when the player chooses to interact after the prompt is shown. - pub interaction: PlayerInteraction, /// The maximum distance from the camera at which the opportunity can be interacted with. pub max_distance: f32, } -#[derive(Debug, Clone, Component, PartialEq, Eq, Reflect)] -#[reflect(Component, PartialEq)] +impl PlayerInteractionParameters { + pub fn default(player_interaction: &PlayerInteraction) -> Self { + match player_interaction { + PlayerInteraction::Dialog(node) => Self { + prompt: "Talk".to_string(), + max_distance: 10.0, + }, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Reflect)] +#[reflect(PartialEq, Component)] pub enum PlayerInteraction { /// A dialog opportunity with a Yarn Spinner dialogue node. Dialog(String), } -fn update_available_opportunities( - q_interactable: Query<&PlayerInteractable>, - mut q_player: Query<(Entity, &mut ActiveInteractable), With>, - q_camera: Query<&Transform, With>, - spatial_query: SpatialQuery, -) { - let Ok((player_entity, mut active_interactable)) = q_player.get_single_mut() else { - return; - }; - let Ok(camera_transform) = q_camera.get_single() else { - return; - }; - - let origin = camera_transform.translation; - let direction = camera_transform.forward(); - // Not relevant because we the maximum distance is determined by the object hit by the raycast. - let max_distance = f32::INFINITY; - // Little bit more efficient than `false`, as we don't care about the actual hit result, - // only if we hit anything at all. - let solid = true; - // Layers that either contain interactable objects or those able to block line of sight with interactable objects. - let query_filter = SpatialQueryFilter::from_mask([ - CollisionLayer::Character, - CollisionLayer::Prop, - CollisionLayer::Terrain, - ]) - .with_excluded_entities([player_entity]); - - let interactable = spatial_query - .cast_ray(origin, direction, max_distance, solid, &query_filter) - .map(|hit| hit.entity) - .filter(|entity| q_interactable.contains(*entity)); - let new_interactable = ActiveInteractable(interactable); +impl Component for PlayerInteraction { + const STORAGE_TYPE: StorageType = StorageType::Table; - if active_interactable.as_ref() != &new_interactable { - *active_interactable = new_interactable; + fn register_component_hooks(hooks: &mut ComponentHooks) { + hooks.on_add(|mut world, entity, _component_id| { + if world.get::(entity).is_some() { + return; + } + let interaction = world.get::(entity).unwrap(); + let parameters = PlayerInteractionParameters::default(interaction); + world.commands().entity(entity).insert(parameters); + }); } } diff --git a/src/opportunities/interact.rs b/src/opportunities/interact.rs index ccc85fce..6f43252d 100644 --- a/src/opportunities/interact.rs +++ b/src/opportunities/interact.rs @@ -5,7 +5,9 @@ use leafwing_input_manager::prelude::*; use crate::{character::action::CharacterAction, dialog::StartDialog, player::Player}; use super::{ - available_opportunities::{ActiveInteractable, PlayerInteractable, PlayerInteraction}, + available_opportunities::{ + AvailablePlayerInteraction, PlayerInteraction, PlayerInteractionParameters, + }, OpportunitySystem, }; @@ -16,15 +18,21 @@ pub(super) fn plugin(app: &mut App) { /// Triggers dialog opportunity events when the player has an interaction opportunity and presses /// the interact button. The target entity is the entity that has the [`RigidBody`] component. fn usher_interact( - mut q_player: Query<(&ActionState, &mut ActiveInteractable), With>, - q_interactable: Query<(&PlayerInteractable, &ColliderParent)>, + mut q_player: Query< + ( + &ActionState, + &mut AvailablePlayerInteraction, + ), + With, + >, + q_interactable: Query<(Entity, &PlayerInteractionParameters)>, mut commands: Commands, ) { for (action_state, mut active_interactable) in &mut q_player { if active_interactable.is_none() || !action_state.just_pressed(&CharacterAction::Interact) { continue; } - let Some((interactable, rigid_body)) = active_interactable + let Some((entity, interactable)) = active_interactable // Clear the current interactable so that it won't show up if we end up in a dialog .take() .and_then(|e| q_interactable.get(e).ok()) @@ -33,7 +41,7 @@ fn usher_interact( }; match &interactable.interaction { PlayerInteraction::Dialog(node) => { - commands.trigger_targets(StartDialog(node.clone()), rigid_body.get()); + commands.trigger_targets(StartDialog(node.clone()), entity); } } } diff --git a/src/opportunities/mod.rs b/src/opportunities/mod.rs index d8693017..7039b590 100644 --- a/src/opportunities/mod.rs +++ b/src/opportunities/mod.rs @@ -4,12 +4,14 @@ use bevy::prelude::*; pub mod available_opportunities; mod interact; mod prompt; +mod update_available; pub(super) fn plugin(app: &mut App) { app.add_plugins(( available_opportunities::plugin, prompt::plugin, interact::plugin, + update_available::plugin, )); app.configure_sets( Update, diff --git a/src/opportunities/prompt.rs b/src/opportunities/prompt.rs index 98697057..e56e40f0 100644 --- a/src/opportunities/prompt.rs +++ b/src/opportunities/prompt.rs @@ -3,7 +3,7 @@ use bevy::ui::Val::*; use sickle_ui::{prelude::*, ui_commands::SetTextExt as _}; use super::{ - available_opportunities::{ActiveInteractable, PlayerInteractable}, + available_opportunities::{AvailablePlayerInteraction, PlayerInteractionParameters}, OpportunitySystem, }; @@ -13,8 +13,8 @@ pub(super) fn plugin(app: &mut App) { } fn show_prompt( - q_active_interactable: Query<&ActiveInteractable, Changed>, - q_interactable: Query<&PlayerInteractable>, + q_active_interactable: Query<&AvailablePlayerInteraction, Changed>, + q_interactable: Query<&PlayerInteractionParameters>, mut q_prompt_text_node: Query<&mut Text, With>, mut q_prompt_visibility: Query<&mut Visibility, With>, ) { @@ -40,6 +40,7 @@ fn show_prompt( return; }; + *prompt_visibility = Visibility::Inherited; prompt_text_node.sections[0].value = interactable.prompt.clone(); } diff --git a/src/opportunities/update_available.rs b/src/opportunities/update_available.rs new file mode 100644 index 00000000..6ea55482 --- /dev/null +++ b/src/opportunities/update_available.rs @@ -0,0 +1,64 @@ +use crate::{ + collision_layer::CollisionLayer, + dialog::conditions::dialog_running, + player::{camera::PlayerCamera, Player}, +}; +use avian3d::prelude::*; +use bevy::prelude::*; + +use super::{ + available_opportunities::{ + AvailablePlayerInteraction, PlayerInteraction, PlayerInteractionParameters, + }, + OpportunitySystem, +}; + +pub(super) fn plugin(app: &mut App) { + app.add_systems( + Update, + update_available_opportunities + .run_if(not(dialog_running)) + .in_set(OpportunitySystem::UpdateAvailableOpportunities), + ); +} + +fn update_available_opportunities( + q_interaction: Query<&PlayerInteractionParameters, With>, + mut q_player: Query<(Entity, &mut AvailablePlayerInteraction), With>, + q_camera: Query<&Transform, With>, + q_collider_parent: Query<&ColliderParent>, + spatial_query: SpatialQuery, +) { + let Ok((player_entity, mut active_interactable)) = q_player.get_single_mut() else { + return; + }; + let Ok(camera_transform) = q_camera.get_single() else { + return; + }; + + let origin = camera_transform.translation; + let direction = camera_transform.forward(); + // Not relevant because we the maximum distance is determined by the object hit by the raycast. + let max_distance = f32::INFINITY; + // Little bit more efficient than `false`, as we don't care about the actual hit result, + // only if we hit anything at all. + let solid = true; + // Layers that either contain interactable objects or those able to block line of sight with interactable objects. + let query_filter = SpatialQueryFilter::from_mask([ + CollisionLayer::Character, + CollisionLayer::Prop, + CollisionLayer::Terrain, + ]) + .with_excluded_entities([player_entity]); + + let interactable = spatial_query + .cast_ray(origin, direction, max_distance, solid, &query_filter) + .and_then(|hit| q_collider_parent.get(hit.entity).ok()) + .map(|collider_parent| collider_parent.get()) + .filter(|entity| q_interactable.contains(*entity)); + let new_interactable = ActiveInteractable(interactable); + + if active_interactable.as_ref() != &new_interactable { + *active_interactable = new_interactable; + } +} diff --git a/src/player/camera/first_person.rs b/src/player/camera/first_person.rs index 91fd9d0f..cf89b447 100644 --- a/src/player/camera/first_person.rs +++ b/src/player/camera/first_person.rs @@ -1,6 +1,6 @@ use std::f32::consts::FRAC_PI_2; -use bevy::prelude::*; +use bevy::{app::RunFixedMainLoop, prelude::*, time::run_fixed_main_schedule}; use leafwing_input_manager::prelude::*; use crate::player::Player; @@ -10,7 +10,11 @@ use super::PlayerCamera; pub(super) fn plugin(app: &mut App) { app.register_type::(); app.add_plugins((InputManagerPlugin::::default(),)); - app.add_systems(Update, (rotate_camera, follow_player).chain()); + app.add_systems( + RunFixedMainLoop, + rotate_camera.before(run_fixed_main_schedule), + ); + app.add_systems(Update, follow_player); } #[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)] diff --git a/src/player/initialize.rs b/src/player/initialize.rs index e611759d..72265576 100644 --- a/src/player/initialize.rs +++ b/src/player/initialize.rs @@ -6,7 +6,7 @@ use leafwing_input_manager::InputManagerBundle; use crate::{ asset_tracking::LoadResource as _, - character::{action::CharacterAction, controller::OverrideForwardDirection}, opportunities::available_opportunities::ActiveInteractable, + character::{action::CharacterAction, controller::OverrideForwardDirection}, opportunities::available_opportunities::AvailablePlayerInteraction, }; use super::{camera::PlayerCamera, Player}; @@ -27,7 +27,7 @@ fn add_player_components( commands.entity(trigger.entity()).insert(( InputManagerBundle::with_map(CharacterAction::default_input_map()), OverrideForwardDirection(camera), - ActiveInteractable::default(), + AvailablePlayerInteraction::default(), )); }