Head select (#4)

This commit is contained in:
extrawurst
2025-03-07 13:22:11 +01:00
committed by GitHub
parent 323e644ff4
commit 76e565e567
20 changed files with 252 additions and 8 deletions

30
Cargo.lock generated
View File

@@ -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"

View File

@@ -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" }

Binary file not shown.

Binary file not shown.

BIN
assets/ui/head_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

BIN
assets/ui/head_regular.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
assets/ui/selector.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -63,7 +63,7 @@ fn toggle_animation(
keys: Res<ButtonInput<KeyCode>>,
mut animation_index: Local<u32>,
) {
if keys.just_pressed(KeyCode::KeyE) {
if keys.just_pressed(KeyCode::KeyT) {
for (mut transition, mut player) in &mut transitions {
transition
.play(

163
src/heads_ui.rs Normal file
View File

@@ -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<Handle<Image>>,
}
#[derive(Resource, Default)]
struct ActiveHeads {
heads: [Option<usize>; 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<AssetServer>) {
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<Image>,
regular: Handle<Image>,
selector: Handle<Image>,
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<ActiveHeads>,
heads_images: Res<HeadsImages>,
mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without<HeadSelector>>,
mut head_selector: Query<(&HeadSelector, &mut Visibility), Without<HeadImage>>,
) {
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<ActiveHeads>,
keys: Res<ButtonInput<KeyCode>>,
) {
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()));
}
}

View File

@@ -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,

View File

@@ -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<Player>>, mut rig: Single<&mut Rig>) {
fn update_camera(
player: Query<&Transform, With<Player>>,
mut rig: Single<&mut Rig>,
res: Res<DebugVisuals>,
) {
let Some(player) = player.iter().next() else {
return;
};
if !res.cam_follow {
return;
}
rig.driver_mut::<GameCameraRig>()
.set_position_target(player.translation, player.rotation);
}
@@ -263,3 +280,32 @@ fn toggle_animation(
}
}
}
fn updaate_head(
trigger: Trigger<HeadChanged>,
mut commands: Commands,
asset_server: Res<AssetServer>,
head: Query<Entity, With<PlayerHead>>,
) {
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));
}

View File

@@ -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),
));
}