diff --git a/Cargo.lock b/Cargo.lock index 95b41d4..40d6b8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -560,6 +560,29 @@ dependencies = [ "web-sys", ] +[[package]] +name = "bevy_asset_loader" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d806c255faca43ace03fe99889dd322e295a55ed4dd478a5d8ea6efe523158fe" +dependencies = [ + "anyhow", + "bevy", + "bevy_asset_loader_derive", + "path-slash", +] + +[[package]] +name = "bevy_asset_loader_derive" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b758b06fa9ec729c925f1fc256b503ca438f1ea345636af362b5fae71f5d8868" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bevy_asset_macros" version = "0.15.3" @@ -2779,6 +2802,7 @@ dependencies = [ "bevy-inspector-egui", "bevy-tnua", "bevy-tnua-avian3d", + "bevy_asset_loader", "bevy_dolly", "bevy_trenchbroom", "nil", @@ -3823,6 +3847,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "path-slash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" + [[package]] name = "percent-encoding" version = "2.3.1" diff --git a/Cargo.toml b/Cargo.toml index 9c69280..3e22f01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ bevy-inspector-egui = "0.29.1" bevy-tnua = "0.21.0" bevy-tnua-avian3d = "0.2.0" bevy_dolly = { version = "0.0.5", default-features = false } +bevy_asset_loader = "0.22.0" [patch.crates-io] bevy_trenchbroom = { git = "https://github.com/Noxmore/bevy_trenchbroom.git", rev = "3b79f1b" } diff --git a/assets/models/heads/commando.glb b/assets/models/heads/commando.glb index 19ff8f6..ccce5bb 100644 Binary files a/assets/models/heads/commando.glb and b/assets/models/heads/commando.glb differ diff --git a/assets/models/heads/french legionaire.glb b/assets/models/heads/french legionaire.glb index 68337a1..a09e8ec 100644 Binary files a/assets/models/heads/french legionaire.glb and b/assets/models/heads/french legionaire.glb differ diff --git a/assets/models/heads/goblin.glb b/assets/models/heads/goblin.glb index 45a474c..f625a25 100644 Binary files a/assets/models/heads/goblin.glb and b/assets/models/heads/goblin.glb differ diff --git a/assets/models/heads/highland hammer thrower.glb b/assets/models/heads/highland hammer thrower.glb index a6cb591..55b973b 100644 Binary files a/assets/models/heads/highland hammer thrower.glb and b/assets/models/heads/highland hammer thrower.glb differ diff --git a/assets/sfx/heads/french legion.ogg b/assets/sfx/heads/french legionaire.ogg similarity index 100% rename from assets/sfx/heads/french legion.ogg rename to assets/sfx/heads/french legionaire.ogg diff --git a/assets/ui/head_bg.png b/assets/ui/head_bg.png new file mode 100644 index 0000000..eccf4e8 Binary files /dev/null and b/assets/ui/head_bg.png differ diff --git a/assets/ui/head_regular.png b/assets/ui/head_regular.png new file mode 100644 index 0000000..e0ef387 Binary files /dev/null and b/assets/ui/head_regular.png differ diff --git a/assets/textures/icons/angry demonstrator.png b/assets/ui/heads/angry demonstrator.png similarity index 100% rename from assets/textures/icons/angry demonstrator.png rename to assets/ui/heads/angry demonstrator.png diff --git a/assets/textures/icons/commando.png b/assets/ui/heads/commando.png similarity index 100% rename from assets/textures/icons/commando.png rename to assets/ui/heads/commando.png diff --git a/assets/textures/icons/french legion.png b/assets/ui/heads/french legionaire.png similarity index 100% rename from assets/textures/icons/french legion.png rename to assets/ui/heads/french legionaire.png diff --git a/assets/textures/icons/goblin_upscayl.png b/assets/ui/heads/goblin.png similarity index 100% rename from assets/textures/icons/goblin_upscayl.png rename to assets/ui/heads/goblin.png diff --git a/assets/textures/icons/highland hammer thrower.png b/assets/ui/heads/highland hammer thrower.png similarity index 100% rename from assets/textures/icons/highland hammer thrower.png rename to assets/ui/heads/highland hammer thrower.png diff --git a/assets/ui/selector.png b/assets/ui/selector.png new file mode 100644 index 0000000..445eb10 Binary files /dev/null and b/assets/ui/selector.png differ diff --git a/src/alien.rs b/src/alien.rs index cec64b8..adf1d72 100644 --- a/src/alien.rs +++ b/src/alien.rs @@ -63,7 +63,7 @@ fn toggle_animation( keys: Res>, mut animation_index: Local, ) { - if keys.just_pressed(KeyCode::KeyE) { + if keys.just_pressed(KeyCode::KeyT) { for (mut transition, mut player) in &mut transitions { transition .play( diff --git a/src/heads_ui.rs b/src/heads_ui.rs new file mode 100644 index 0000000..64d6b6b --- /dev/null +++ b/src/heads_ui.rs @@ -0,0 +1,163 @@ +use bevy::prelude::*; + +#[derive(Component, Default)] +struct HeadSelector(pub usize); + +#[derive(Component, Default)] +struct HeadImage(pub usize); + +#[derive(Resource, Default)] +struct HeadsImages { + heads: Vec>, +} + +#[derive(Resource, Default)] +struct ActiveHeads { + heads: [Option; 5], + current_slot: usize, +} + +#[derive(Event)] +pub struct HeadChanged(pub usize); + +pub fn plugin(app: &mut App) { + app.add_systems(Startup, setup); + app.add_systems(Update, (update, toggle_heads)); +} + +fn setup(mut commands: Commands, asset_server: Res) { + let bg = asset_server.load("ui/head_bg.png"); + let regular = asset_server.load("ui/head_regular.png"); + let selector = asset_server.load("ui/selector.png"); + + commands + .spawn(Node { + position_type: PositionType::Absolute, + bottom: Val::Px(20.0), + right: Val::Px(100.0), + height: Val::Px(74.0), + ..default() + }) + .with_children(|parent| { + spawn_head_ui(parent, bg.clone(), regular.clone(), selector.clone(), 0); + spawn_head_ui(parent, bg.clone(), regular.clone(), selector.clone(), 1); + spawn_head_ui(parent, bg.clone(), regular.clone(), selector.clone(), 2); + spawn_head_ui(parent, bg.clone(), regular.clone(), selector.clone(), 3); + spawn_head_ui(parent, bg.clone(), regular.clone(), selector.clone(), 4); + }); + + let head_01 = asset_server.load("ui/heads/angry demonstrator.png"); + let head_02 = asset_server.load("ui/heads/commando.png"); + let head_03 = asset_server.load("ui/heads/goblin.png"); + let head_04 = asset_server.load("ui/heads/highland hammer thrower.png"); + let head_05 = asset_server.load("ui/heads/french legionaire.png"); + + commands.insert_resource(HeadsImages { + heads: vec![head_01, head_02, head_03, head_04, head_05], + }); + + commands.insert_resource(ActiveHeads { + heads: [Some(0), Some(1), Some(2), Some(3), Some(4)], + current_slot: 0, + }); +} + +fn spawn_head_ui( + parent: &mut ChildBuilder, + bg: Handle, + regular: Handle, + selector: Handle, + head: usize, +) { + parent + .spawn((Node { + position_type: PositionType::Relative, + justify_content: JustifyContent::Center, + width: Val::Px(74.0), + ..default() + },)) + .with_children(|parent| { + parent.spawn(( + Node { + position_type: PositionType::Absolute, + top: Val::Px(-20.0), + ..default() + }, + Visibility::Hidden, + ImageNode::new(selector), + HeadSelector(head), + )); + parent.spawn(( + Node { + position_type: PositionType::Absolute, + ..default() + }, + ImageNode::new(bg), + )); + parent.spawn(( + Node { + position_type: PositionType::Absolute, + left: Val::Px(2.0), + right: Val::Px(2.0), + top: Val::Px(2.0), + bottom: Val::Px(2.0), + ..default() + }, + ImageNode::default(), + Visibility::Hidden, + HeadImage(head), + )); + parent.spawn(( + Node { + position_type: PositionType::Absolute, + ..default() + }, + ImageNode::new(regular), + )); + }); +} + +fn update( + res: Res, + heads_images: Res, + mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without>, + mut head_selector: Query<(&HeadSelector, &mut Visibility), Without>, +) { + if res.is_changed() { + for (HeadImage(head), mut vis, mut image) in head_image.iter_mut() { + if let Some(head) = res.heads[*head] { + *vis = Visibility::Visible; + image.image = heads_images.heads[head].clone(); + } else { + *vis = Visibility::Hidden; + } + } + for (HeadSelector(head), mut vis) in head_selector.iter_mut() { + *vis = if *head == res.current_slot { + Visibility::Visible + } else { + Visibility::Hidden + }; + } + } +} + +fn toggle_heads( + mut commands: Commands, + mut res: ResMut, + keys: Res>, +) { + let changed = if keys.just_pressed(KeyCode::KeyE) { + res.current_slot = (res.current_slot + 1) % 5; + true + } else if keys.just_pressed(KeyCode::KeyQ) { + res.current_slot = (res.current_slot + 4) % 5; + true + } else { + false + }; + + if changed { + commands.trigger(HeadChanged(res.heads[res.current_slot].unwrap())); + } +} diff --git a/src/main.rs b/src/main.rs index b6b974d..64a7f0a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod alien; mod camera; mod cash; +mod heads_ui; mod player; mod tb_entities; @@ -22,6 +23,7 @@ struct DebugVisuals { pub tonemapping: Tonemapping, pub exposure: f32, pub shadows: bool, + pub cam_follow: bool, } #[derive(Component, Debug)] @@ -36,6 +38,7 @@ fn main() { tonemapping: Tonemapping::None, exposure: 1., shadows: true, + cam_follow: true, }); app.add_plugins(DefaultPlugins.set(ImagePlugin { @@ -43,7 +46,7 @@ fn main() { })); app.add_plugins(PhysicsPlugins::default()); - app.add_plugins(PhysicsDebugPlugin::default()); + // app.add_plugins(PhysicsDebugPlugin::default()); app.add_plugins(( TnuaControllerPlugin::new(FixedUpdate), TnuaAvian3dPlugin::new(FixedUpdate), @@ -54,6 +57,7 @@ fn main() { app.add_plugins(alien::plugin); app.add_plugins(cash::plugin); app.add_plugins(player::plugin); + app.add_plugins(heads_ui::plugin); app.insert_resource(AmbientLight { color: Color::WHITE, diff --git a/src/player.rs b/src/player.rs index 10adbac..677d72d 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,9 +1,11 @@ use std::time::Duration; use crate::{ + DebugVisuals, alien::{ALIEN_ASSET_PATH, Animations}, camera::GameCameraRig, cash::{Cash, CashCollectEvent}, + heads_ui::HeadChanged, tb_entities::SpawnPoint, }; use avian3d::prelude::*; @@ -22,6 +24,9 @@ pub struct Player; #[derive(Component, Default)] struct PlayerAnimations; +#[derive(Component, Default)] +struct PlayerHead; + #[derive(Resource, Default)] struct PlayerSpawned { spawned: bool, @@ -45,6 +50,8 @@ pub fn plugin(app: &mut App) { FixedUpdate, apply_controls.in_set(TnuaUserControlsSystemSet), ); + + app.add_observer(updaate_head); } fn spawn( @@ -77,10 +84,11 @@ fn spawn( Collider::capsule(1.2, 1.5), LockedAxes::ROTATION_LOCKED, TnuaController::default(), - TnuaAvian3dSensorShape(Collider::cylinder(1.0, 0.0)), + TnuaAvian3dSensorShape(Collider::cylinder(0.8, 0.0)), )) .with_child(( Name::from("head"), + PlayerHead, Transform::from_translation(Vec3::new(0., -0.5, 0.)) .with_rotation(Quat::from_rotation_y(std::f32::consts::PI)), SceneRoot(mesh), @@ -93,8 +101,9 @@ fn spawn( SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(ALIEN_ASSET_PATH))), )); - commands.spawn(AudioPlayer::new( - asset_server.load("sfx/heads/angry demonstrator.ogg"), + commands.spawn(( + AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")), + PlaybackSettings::DESPAWN, )); player_spawned.spawned = true; @@ -164,7 +173,7 @@ fn apply_controls( controller.basis(TnuaBuiltinWalk { // The `desired_velocity` determines how the character will move. - desired_velocity: direction.normalize_or_zero() * 10.0, + desired_velocity: direction.normalize_or_zero() * 8.0, // The `float_height` must be greater (even if by little) from the distance between the // character's center and the lowest point of its collider. float_height: 3.0, @@ -185,11 +194,19 @@ fn apply_controls( } } -fn update_camera(player: Query<&Transform, With>, mut rig: Single<&mut Rig>) { +fn update_camera( + player: Query<&Transform, With>, + mut rig: Single<&mut Rig>, + res: Res, +) { let Some(player) = player.iter().next() else { return; }; + if !res.cam_follow { + return; + } + rig.driver_mut::() .set_position_target(player.translation, player.rotation); } @@ -263,3 +280,32 @@ fn toggle_animation( } } } + +fn updaate_head( + trigger: Trigger, + mut commands: Commands, + asset_server: Res, + head: Query>, +) { + let Ok(head) = head.get_single() else { + return; + }; + + let head_str = match trigger.0 { + 0 => "angry demonstrator", + 1 => "commando", + 2 => "goblin", + 3 => "highland hammer thrower", + _ => "french legionaire", + }; + + commands.spawn(( + AudioPlayer::new(asset_server.load(format!("sfx/heads/{}.ogg", head_str))), + PlaybackSettings::DESPAWN, + )); + + let mesh = asset_server + .load(GltfAssetLabel::Scene(0).from_asset(format!("models/heads/{}.glb", head_str))); + + commands.entity(head).insert(SceneRoot(mesh)); +} diff --git a/src/tb_entities.rs b/src/tb_entities.rs index 667076a..cd3f22b 100644 --- a/src/tb_entities.rs +++ b/src/tb_entities.rs @@ -68,7 +68,7 @@ impl EnemySpawn { Name::from("Enemy"), )) .with_child(( - Transform::from_translation(Vec3::new(0., -0.6, 0.)), + Transform::from_translation(Vec3::new(0., -0.6, 0.)).with_scale(Vec3::splat(1.5)), SceneRoot(mesh), )); }