npc reloading (#28)

This commit is contained in:
extrawurst
2025-04-15 08:30:41 +02:00
committed by GitHub
parent 2e5ec8f3d8
commit 248f92be27
8 changed files with 123 additions and 76 deletions

View File

@@ -114,11 +114,15 @@ fn on_trigger_state(
mut commands: Commands, mut commands: Commands,
player_rot: Query<&Transform, With<PlayerBodyMesh>>, player_rot: Query<&Transform, With<PlayerBodyMesh>>,
player_query: Query<(&Transform, &AimTarget), With<Player>>, player_query: Query<(&Transform, &AimTarget), With<Player>>,
mut active_heads: ResMut<ActiveHeads>, mut active_heads: Query<&mut ActiveHeads, With<Player>>,
heads_db: Res<HeadsDatabase>, heads_db: Res<HeadsDatabase>,
time: Res<Time>, time: Res<Time>,
) { ) {
if matches!(trigger.event(), TriggerState::Active) { if matches!(trigger.event(), TriggerState::Active) {
let Ok(mut active_heads) = active_heads.get_single_mut() else {
return;
};
let Some(state) = active_heads.current() else { let Some(state) = active_heads.current() else {
return; return;
}; };

View File

@@ -4,8 +4,8 @@ use crate::{
GameState, GameState,
abilities::{HeadAbility, TriggerData, TriggerThrow}, abilities::{HeadAbility, TriggerData, TriggerThrow},
aim::AimTarget, aim::AimTarget,
heads::ActiveHeads,
heads_database::HeadsDatabase, heads_database::HeadsDatabase,
npc::Npc,
}; };
#[derive(Component, Reflect)] #[derive(Component, Reflect)]
@@ -18,7 +18,7 @@ pub fn plugin(app: &mut App) {
fn update( fn update(
mut commands: Commands, mut commands: Commands,
mut query: Query<(&mut Npc, &AimTarget, &Transform), With<Ai>>, mut query: Query<(&mut ActiveHeads, &AimTarget, &Transform), With<Ai>>,
time: Res<Time>, time: Res<Time>,
heads_db: Res<HeadsDatabase>, heads_db: Res<HeadsDatabase>,
) { ) {
@@ -27,18 +27,21 @@ fn update(
continue; continue;
} }
let ability = heads_db.head_stats(npc.head).ability; let Some(npc_head) = npc.current() else {
continue;
};
let ability = heads_db.head_stats(npc_head.head).ability;
//TODO: support other abilities //TODO: support other abilities
if ability != HeadAbility::Thrown { if ability != HeadAbility::Thrown {
continue; continue;
} }
let can_shoot_again = npc.last_use + 1. < time.elapsed_secs(); let can_shoot_again = npc_head.last_use + 1. < time.elapsed_secs();
if can_shoot_again && npc.has_ammo() { if can_shoot_again && npc_head.has_ammo() {
npc.last_use = time.elapsed_secs(); npc.use_ammo(time.elapsed_secs());
npc.ammo -= 1;
let dir = t.forward(); let dir = t.forward();
@@ -48,7 +51,7 @@ fn update(
t.rotation, t.rotation,
t.translation, t.translation,
crate::physics_layers::GameLayer::Player, crate::physics_layers::GameLayer::Player,
npc.head, npc_head.head,
))); )));
} }
} }

View File

@@ -1,7 +1,12 @@
use super::AimTarget; use super::AimTarget;
use crate::{ use crate::{
GameState, backpack::UiHeadState, heads::HeadsImages, hitpoints::Hitpoints, GameState,
loading_assets::UIAssets, npc::Npc, player::Player, backpack::UiHeadState,
heads::{ActiveHeads, HeadsImages},
hitpoints::Hitpoints,
loading_assets::UIAssets,
npc::Npc,
player::Player,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -142,11 +147,12 @@ fn update(
fn sync( fn sync(
mut target: ResMut<TargetUi>, mut target: ResMut<TargetUi>,
player_target: Query<&AimTarget, With<Player>>, player_target: Query<&AimTarget, With<Player>>,
target_data: Query<(&Hitpoints, &Npc)>, target_data: Query<(&Hitpoints, &ActiveHeads), With<Npc>>,
) { ) {
let mut new_state = None; let mut new_state = None;
if let Some(e) = player_target.iter().next().and_then(|target| target.0) { if let Some(e) = player_target.iter().next().and_then(|target| target.0) {
if let Ok((hp, head)) = target_data.get(e) { if let Ok((hp, heads)) = target_data.get(e) {
let head = heads.current().expect("target must have a head on");
new_state = Some(UiHeadState { new_state = Some(UiHeadState {
head: head.head, head: head.head,
health: hp.health(), health: hp.health(),

View File

@@ -1,4 +1,4 @@
use crate::{GameState, backpack::UiHeadState, loading_assets::UIAssets}; use crate::{GameState, backpack::UiHeadState, loading_assets::UIAssets, player::Player};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_ui_gradients::{AngularColorStop, BackgroundGradient, ConicGradient, Gradient, Position}; use bevy_ui_gradients::{AngularColorStop, BackgroundGradient, ConicGradient, Gradient, Position};
use std::f32::consts::PI; use std::f32::consts::PI;
@@ -213,11 +213,19 @@ fn update_health(res: Res<UiActiveHeads>, mut query: Query<(&mut Node, &HeadDama
} }
} }
fn sync(active: Res<ActiveHeads>, mut state: ResMut<UiActiveHeads>, time: Res<Time>) { fn sync(
if active.is_changed() || active.reloading() { active_heads: Query<Ref<ActiveHeads>, With<Player>>,
state.current_slot = active.slot(); mut state: ResMut<UiActiveHeads>,
time: Res<Time>,
) {
let Ok(active_heads) = active_heads.get_single() else {
return;
};
if active_heads.is_changed() || active_heads.reloading() {
state.current_slot = active_heads.slot();
for i in 0..HEAD_SLOTS { for i in 0..HEAD_SLOTS {
state.heads[i] = active state.heads[i] = active_heads
.head(i) .head(i)
.map(|state| UiHeadState::new(state, time.elapsed_secs())); .map(|state| UiHeadState::new(state, time.elapsed_secs()));
} }

View File

@@ -48,14 +48,21 @@ impl HeadState {
} }
} }
#[derive(Resource, Default, Reflect)] #[derive(Component, Default, Reflect, Debug)]
#[reflect(Resource)] #[reflect(Component)]
pub struct ActiveHeads { pub struct ActiveHeads {
heads: [Option<HeadState>; 5], heads: [Option<HeadState>; 5],
current_slot: usize, current_slot: usize,
} }
impl ActiveHeads { impl ActiveHeads {
pub fn new(heads: [Option<HeadState>; 5]) -> Self {
Self {
heads,
current_slot: 0,
}
}
pub fn current(&self) -> Option<HeadState> { pub fn current(&self) -> Option<HeadState> {
self.heads[self.current_slot] self.heads[self.current_slot]
} }
@@ -80,7 +87,11 @@ impl ActiveHeads {
pub fn reloading(&self) -> bool { pub fn reloading(&self) -> bool {
for head in self.heads { for head in self.heads {
if head.map(|head| head.ammo == 0).unwrap_or(false) { let Some(head) = head else {
continue;
};
if head.ammo == 0 {
return true; return true;
} }
} }
@@ -88,6 +99,14 @@ impl ActiveHeads {
false false
} }
pub fn hp(&self) -> Hitpoints {
if let Some(head) = &self.heads[self.current_slot] {
Hitpoints::new(head.health_max).with_health(head.health)
} else {
Hitpoints::new(0)
}
}
pub fn set_hitpoint(&mut self, hp: &Hitpoints) { pub fn set_hitpoint(&mut self, hp: &Hitpoints) {
let Some(head) = &mut self.heads[self.current_slot] else { let Some(head) = &mut self.heads[self.current_slot] else {
error!("cannot use ammo of empty head"); error!("cannot use ammo of empty head");
@@ -110,17 +129,6 @@ pub struct HeadChanged(pub usize);
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_plugins(heads_ui::plugin); app.add_plugins(heads_ui::plugin);
app.insert_resource(ActiveHeads {
heads: [
Some(HeadState::new(0, 10)),
Some(HeadState::new(3, 10)),
Some(HeadState::new(6, 10)),
Some(HeadState::new(8, 10)),
Some(HeadState::new(9, 10)),
],
current_slot: 0,
});
app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems( app.add_systems(
Update, Update,
@@ -140,17 +148,18 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, heads: Res<Head
commands.insert_resource(HeadsImages { heads }); commands.insert_resource(HeadsImages { heads });
} }
fn sync_hp(mut active: ResMut<ActiveHeads>, query: Query<&Hitpoints, With<Player>>) { fn sync_hp(mut query: Query<(&mut ActiveHeads, &Hitpoints)>) {
let Ok(hp) = query.get_single() else { for (mut active_heads, hp) in query.iter_mut() {
return; if active_heads.hp() != *hp {
}; active_heads.set_hitpoint(hp);
}
active.set_hitpoint(hp); }
} }
fn reload(mut commands: Commands, mut active: ResMut<ActiveHeads>, time: Res<Time>) { fn reload(mut commands: Commands, mut active: Query<&mut ActiveHeads>, time: Res<Time>) {
for mut active in active.iter_mut() {
if !active.reloading() { if !active.reloading() {
return; continue;
} }
for head in active.heads.iter_mut() { for head in active.heads.iter_mut() {
@@ -159,52 +168,58 @@ fn reload(mut commands: Commands, mut active: ResMut<ActiveHeads>, time: Res<Tim
}; };
if !head.has_ammo() && (head.last_use + head.reload_duration <= time.elapsed_secs()) { if !head.has_ammo() && (head.last_use + head.reload_duration <= time.elapsed_secs()) {
// only for player?
commands.trigger(PlaySound::Reloaded); commands.trigger(PlaySound::Reloaded);
head.ammo = head.ammo_max; head.ammo = head.ammo_max;
} }
} }
} }
}
fn on_select_active_head( fn on_select_active_head(
trigger: Trigger<SelectActiveHead>, trigger: Trigger<SelectActiveHead>,
mut commands: Commands, mut commands: Commands,
mut res: ResMut<ActiveHeads>, mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>,
mut query: Query<&mut Hitpoints, With<Player>>,
) { ) {
match trigger.event() { let Ok((mut active_heads, mut hp)) = query.get_single_mut() else {
SelectActiveHead::Right => {
res.current_slot = (res.current_slot + 1) % HEAD_SLOTS;
}
SelectActiveHead::Left => {
res.current_slot = (res.current_slot + (HEAD_SLOTS - 1)) % HEAD_SLOTS;
}
}
// sync health back into Hitpoints
let Ok(mut hp) = query.get_single_mut() else {
return; return;
}; };
hp.set_health(res.current().unwrap().health);
match trigger.event() {
SelectActiveHead::Right => {
active_heads.current_slot = (active_heads.current_slot + 1) % HEAD_SLOTS;
}
SelectActiveHead::Left => {
active_heads.current_slot = (active_heads.current_slot + (HEAD_SLOTS - 1)) % HEAD_SLOTS;
}
}
hp.set_health(active_heads.current().unwrap().health);
commands.trigger(PlaySound::Selection); commands.trigger(PlaySound::Selection);
commands.trigger(HeadChanged(res.heads[res.current_slot].unwrap().head)); commands.trigger(HeadChanged(
active_heads.heads[active_heads.current_slot].unwrap().head,
));
} }
fn on_swap_backpack( fn on_swap_backpack(
trigger: Trigger<BackbackSwapEvent>, trigger: Trigger<BackbackSwapEvent>,
mut commands: Commands, mut commands: Commands,
mut res: ResMut<ActiveHeads>, mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>,
mut backpack: ResMut<Backpack>, mut backpack: ResMut<Backpack>,
mut query: Query<&mut Hitpoints, With<Player>>,
) { ) {
let backpack_slot = trigger.event().0; let backpack_slot = trigger.event().0;
let head = backpack.heads.get(backpack_slot).unwrap(); let head = backpack.heads.get(backpack_slot).unwrap();
let current_active_slot = res.current_slot; let Ok((mut active_heads, mut hp)) = query.get_single_mut() else {
return;
};
let current_active_head = res.heads[current_active_slot]; let current_active_slot = active_heads.current_slot;
res.heads[current_active_slot] = Some(*head);
let current_active_head = active_heads.heads[current_active_slot];
active_heads.heads[current_active_slot] = Some(*head);
if let Some(old_active) = current_active_head { if let Some(old_active) = current_active_head {
backpack.heads[backpack_slot] = old_active; backpack.heads[backpack_slot] = old_active;
@@ -212,11 +227,9 @@ fn on_swap_backpack(
backpack.heads.remove(backpack_slot); backpack.heads.remove(backpack_slot);
} }
// sync health back into Hitpoints hp.set_health(active_heads.current().unwrap().health);
let Ok(mut hp) = query.get_single_mut() else {
return;
};
hp.set_health(res.current().unwrap().health);
commands.trigger(HeadChanged(res.heads[res.current_slot].unwrap().head)); commands.trigger(HeadChanged(
active_heads.heads[active_heads.current_slot].unwrap().head,
));
} }

View File

@@ -10,7 +10,7 @@ pub struct Hit {
pub damage: u32, pub damage: u32,
} }
#[derive(Component, Reflect, Debug)] #[derive(Component, Reflect, Debug, PartialEq, Eq, Clone, Copy)]
pub struct Hitpoints { pub struct Hitpoints {
max: u32, max: u32,
current: u32, current: u32,
@@ -21,6 +21,11 @@ impl Hitpoints {
Self { max: v, current: v } Self { max: v, current: v }
} }
pub fn with_health(mut self, v: u32) -> Self {
self.current = v;
self
}
pub fn health(&self) -> f32 { pub fn health(&self) -> f32 {
self.current as f32 / self.max as f32 self.current as f32 / self.max as f32
} }

View File

@@ -2,7 +2,7 @@ use crate::{
GameState, GameState,
ai::Ai, ai::Ai,
head::ActiveHead, head::ActiveHead,
heads::{HEAD_COUNT, HeadState}, heads::{ActiveHeads, HEAD_COUNT, HeadState},
heads_database::HeadsDatabase, heads_database::HeadsDatabase,
hitpoints::{Hitpoints, Kill}, hitpoints::{Hitpoints, Kill},
keys::KeySpawn, keys::KeySpawn,
@@ -11,9 +11,9 @@ use crate::{
use bevy::prelude::*; use bevy::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Component, Reflect, Deref, DerefMut)] #[derive(Component, Reflect)]
#[reflect(Component)] #[reflect(Component)]
pub struct Npc(HeadState); pub struct Npc;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Playing), init); app.add_systems(OnEnter(GameState::Playing), init);
@@ -32,8 +32,9 @@ fn init(mut commands: Commands, query: Query<(Entity, &EnemySpawn)>, heads_db: R
.entity(e) .entity(e)
.insert(( .insert((
Hitpoints::new(100), Hitpoints::new(100),
Npc(HeadState::new(id, 10)), Npc,
ActiveHead(id), ActiveHead(id),
ActiveHeads::new([Some(HeadState::new(id, 10)), None, None, None, None]),
Ai, Ai,
)) ))
.observe(on_kill); .observe(on_kill);

View File

@@ -6,7 +6,7 @@ use crate::{
control::controller_common::{CharacterControllerBundle, PlayerMovement}, control::controller_common::{CharacterControllerBundle, PlayerMovement},
global_observer, global_observer,
head::ActiveHead, head::ActiveHead,
heads::HeadChanged, heads::{ActiveHeads, HeadChanged, HeadState},
heads_database::{HeadControls, HeadsDatabase}, heads_database::{HeadControls, HeadsDatabase},
hitpoints::Hitpoints, hitpoints::Hitpoints,
loading_assets::{AudioAssets, GameAssets}, loading_assets::{AudioAssets, GameAssets},
@@ -75,6 +75,13 @@ fn spawn(
Name::from("player"), Name::from("player"),
Player, Player,
ActiveHead(0), ActiveHead(0),
ActiveHeads::new([
Some(HeadState::new(0, 10)),
Some(HeadState::new(3, 10)),
Some(HeadState::new(6, 10)),
Some(HeadState::new(8, 10)),
Some(HeadState::new(9, 10)),
]),
Hitpoints::new(100), Hitpoints::new(100),
CameraTarget, CameraTarget,
transform, transform,