Head select (#4)
30
Cargo.lock
generated
@@ -560,6 +560,29 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "bevy_asset_macros"
|
name = "bevy_asset_macros"
|
||||||
version = "0.15.3"
|
version = "0.15.3"
|
||||||
@@ -2779,6 +2802,7 @@ dependencies = [
|
|||||||
"bevy-inspector-egui",
|
"bevy-inspector-egui",
|
||||||
"bevy-tnua",
|
"bevy-tnua",
|
||||||
"bevy-tnua-avian3d",
|
"bevy-tnua-avian3d",
|
||||||
|
"bevy_asset_loader",
|
||||||
"bevy_dolly",
|
"bevy_dolly",
|
||||||
"bevy_trenchbroom",
|
"bevy_trenchbroom",
|
||||||
"nil",
|
"nil",
|
||||||
@@ -3823,6 +3847,12 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "path-slash"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ bevy-inspector-egui = "0.29.1"
|
|||||||
bevy-tnua = "0.21.0"
|
bevy-tnua = "0.21.0"
|
||||||
bevy-tnua-avian3d = "0.2.0"
|
bevy-tnua-avian3d = "0.2.0"
|
||||||
bevy_dolly = { version = "0.0.5", default-features = false }
|
bevy_dolly = { version = "0.0.5", default-features = false }
|
||||||
|
bevy_asset_loader = "0.22.0"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
bevy_trenchbroom = { git = "https://github.com/Noxmore/bevy_trenchbroom.git", rev = "3b79f1b" }
|
bevy_trenchbroom = { git = "https://github.com/Noxmore/bevy_trenchbroom.git", rev = "3b79f1b" }
|
||||||
|
|||||||
BIN
assets/ui/head_bg.png
Normal file
|
After Width: | Height: | Size: 782 B |
BIN
assets/ui/head_regular.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
BIN
assets/ui/selector.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
@@ -63,7 +63,7 @@ fn toggle_animation(
|
|||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
mut animation_index: Local<u32>,
|
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 {
|
for (mut transition, mut player) in &mut transitions {
|
||||||
transition
|
transition
|
||||||
.play(
|
.play(
|
||||||
|
|||||||
163
src/heads_ui.rs
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
mod alien;
|
mod alien;
|
||||||
mod camera;
|
mod camera;
|
||||||
mod cash;
|
mod cash;
|
||||||
|
mod heads_ui;
|
||||||
mod player;
|
mod player;
|
||||||
mod tb_entities;
|
mod tb_entities;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ struct DebugVisuals {
|
|||||||
pub tonemapping: Tonemapping,
|
pub tonemapping: Tonemapping,
|
||||||
pub exposure: f32,
|
pub exposure: f32,
|
||||||
pub shadows: bool,
|
pub shadows: bool,
|
||||||
|
pub cam_follow: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
@@ -36,6 +38,7 @@ fn main() {
|
|||||||
tonemapping: Tonemapping::None,
|
tonemapping: Tonemapping::None,
|
||||||
exposure: 1.,
|
exposure: 1.,
|
||||||
shadows: true,
|
shadows: true,
|
||||||
|
cam_follow: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.add_plugins(DefaultPlugins.set(ImagePlugin {
|
app.add_plugins(DefaultPlugins.set(ImagePlugin {
|
||||||
@@ -43,7 +46,7 @@ fn main() {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
app.add_plugins(PhysicsPlugins::default());
|
app.add_plugins(PhysicsPlugins::default());
|
||||||
app.add_plugins(PhysicsDebugPlugin::default());
|
// app.add_plugins(PhysicsDebugPlugin::default());
|
||||||
app.add_plugins((
|
app.add_plugins((
|
||||||
TnuaControllerPlugin::new(FixedUpdate),
|
TnuaControllerPlugin::new(FixedUpdate),
|
||||||
TnuaAvian3dPlugin::new(FixedUpdate),
|
TnuaAvian3dPlugin::new(FixedUpdate),
|
||||||
@@ -54,6 +57,7 @@ fn main() {
|
|||||||
app.add_plugins(alien::plugin);
|
app.add_plugins(alien::plugin);
|
||||||
app.add_plugins(cash::plugin);
|
app.add_plugins(cash::plugin);
|
||||||
app.add_plugins(player::plugin);
|
app.add_plugins(player::plugin);
|
||||||
|
app.add_plugins(heads_ui::plugin);
|
||||||
|
|
||||||
app.insert_resource(AmbientLight {
|
app.insert_resource(AmbientLight {
|
||||||
color: Color::WHITE,
|
color: Color::WHITE,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
DebugVisuals,
|
||||||
alien::{ALIEN_ASSET_PATH, Animations},
|
alien::{ALIEN_ASSET_PATH, Animations},
|
||||||
camera::GameCameraRig,
|
camera::GameCameraRig,
|
||||||
cash::{Cash, CashCollectEvent},
|
cash::{Cash, CashCollectEvent},
|
||||||
|
heads_ui::HeadChanged,
|
||||||
tb_entities::SpawnPoint,
|
tb_entities::SpawnPoint,
|
||||||
};
|
};
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
@@ -22,6 +24,9 @@ pub struct Player;
|
|||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
struct PlayerAnimations;
|
struct PlayerAnimations;
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
struct PlayerHead;
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
struct PlayerSpawned {
|
struct PlayerSpawned {
|
||||||
spawned: bool,
|
spawned: bool,
|
||||||
@@ -45,6 +50,8 @@ pub fn plugin(app: &mut App) {
|
|||||||
FixedUpdate,
|
FixedUpdate,
|
||||||
apply_controls.in_set(TnuaUserControlsSystemSet),
|
apply_controls.in_set(TnuaUserControlsSystemSet),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.add_observer(updaate_head);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn(
|
fn spawn(
|
||||||
@@ -77,10 +84,11 @@ fn spawn(
|
|||||||
Collider::capsule(1.2, 1.5),
|
Collider::capsule(1.2, 1.5),
|
||||||
LockedAxes::ROTATION_LOCKED,
|
LockedAxes::ROTATION_LOCKED,
|
||||||
TnuaController::default(),
|
TnuaController::default(),
|
||||||
TnuaAvian3dSensorShape(Collider::cylinder(1.0, 0.0)),
|
TnuaAvian3dSensorShape(Collider::cylinder(0.8, 0.0)),
|
||||||
))
|
))
|
||||||
.with_child((
|
.with_child((
|
||||||
Name::from("head"),
|
Name::from("head"),
|
||||||
|
PlayerHead,
|
||||||
Transform::from_translation(Vec3::new(0., -0.5, 0.))
|
Transform::from_translation(Vec3::new(0., -0.5, 0.))
|
||||||
.with_rotation(Quat::from_rotation_y(std::f32::consts::PI)),
|
.with_rotation(Quat::from_rotation_y(std::f32::consts::PI)),
|
||||||
SceneRoot(mesh),
|
SceneRoot(mesh),
|
||||||
@@ -93,8 +101,9 @@ fn spawn(
|
|||||||
SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(ALIEN_ASSET_PATH))),
|
SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(ALIEN_ASSET_PATH))),
|
||||||
));
|
));
|
||||||
|
|
||||||
commands.spawn(AudioPlayer::new(
|
commands.spawn((
|
||||||
asset_server.load("sfx/heads/angry demonstrator.ogg"),
|
AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")),
|
||||||
|
PlaybackSettings::DESPAWN,
|
||||||
));
|
));
|
||||||
|
|
||||||
player_spawned.spawned = true;
|
player_spawned.spawned = true;
|
||||||
@@ -164,7 +173,7 @@ fn apply_controls(
|
|||||||
|
|
||||||
controller.basis(TnuaBuiltinWalk {
|
controller.basis(TnuaBuiltinWalk {
|
||||||
// The `desired_velocity` determines how the character will move.
|
// 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
|
// 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.
|
// character's center and the lowest point of its collider.
|
||||||
float_height: 3.0,
|
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 {
|
let Some(player) = player.iter().next() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !res.cam_follow {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
rig.driver_mut::<GameCameraRig>()
|
rig.driver_mut::<GameCameraRig>()
|
||||||
.set_position_target(player.translation, player.rotation);
|
.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));
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ impl EnemySpawn {
|
|||||||
Name::from("Enemy"),
|
Name::from("Enemy"),
|
||||||
))
|
))
|
||||||
.with_child((
|
.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),
|
SceneRoot(mesh),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||