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,
player_rot: Query<&Transform, With<PlayerBodyMesh>>,
player_query: Query<(&Transform, &AimTarget), With<Player>>,
mut active_heads: ResMut<ActiveHeads>,
mut active_heads: Query<&mut ActiveHeads, With<Player>>,
heads_db: Res<HeadsDatabase>,
time: Res<Time>,
) {
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 {
return;
};

View File

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

View File

@@ -1,7 +1,12 @@
use super::AimTarget;
use crate::{
GameState, backpack::UiHeadState, heads::HeadsImages, hitpoints::Hitpoints,
loading_assets::UIAssets, npc::Npc, player::Player,
GameState,
backpack::UiHeadState,
heads::{ActiveHeads, HeadsImages},
hitpoints::Hitpoints,
loading_assets::UIAssets,
npc::Npc,
player::Player,
};
use bevy::prelude::*;
@@ -142,11 +147,12 @@ fn update(
fn sync(
mut target: ResMut<TargetUi>,
player_target: Query<&AimTarget, With<Player>>,
target_data: Query<(&Hitpoints, &Npc)>,
target_data: Query<(&Hitpoints, &ActiveHeads), With<Npc>>,
) {
let mut new_state = None;
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 {
head: head.head,
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_ui_gradients::{AngularColorStop, BackgroundGradient, ConicGradient, Gradient, Position};
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>) {
if active.is_changed() || active.reloading() {
state.current_slot = active.slot();
fn sync(
active_heads: Query<Ref<ActiveHeads>, With<Player>>,
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 {
state.heads[i] = active
state.heads[i] = active_heads
.head(i)
.map(|state| UiHeadState::new(state, time.elapsed_secs()));
}

View File

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

View File

@@ -2,7 +2,7 @@ use crate::{
GameState,
ai::Ai,
head::ActiveHead,
heads::{HEAD_COUNT, HeadState},
heads::{ActiveHeads, HEAD_COUNT, HeadState},
heads_database::HeadsDatabase,
hitpoints::{Hitpoints, Kill},
keys::KeySpawn,
@@ -11,9 +11,9 @@ use crate::{
use bevy::prelude::*;
use std::collections::HashMap;
#[derive(Component, Reflect, Deref, DerefMut)]
#[derive(Component, Reflect)]
#[reflect(Component)]
pub struct Npc(HeadState);
pub struct Npc;
pub fn plugin(app: &mut App) {
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)
.insert((
Hitpoints::new(100),
Npc(HeadState::new(id, 10)),
Npc,
ActiveHead(id),
ActiveHeads::new([Some(HeadState::new(id, 10)), None, None, None, None]),
Ai,
))
.observe(on_kill);

View File

@@ -6,7 +6,7 @@ use crate::{
control::controller_common::{CharacterControllerBundle, PlayerMovement},
global_observer,
head::ActiveHead,
heads::HeadChanged,
heads::{ActiveHeads, HeadChanged, HeadState},
heads_database::{HeadControls, HeadsDatabase},
hitpoints::Hitpoints,
loading_assets::{AudioAssets, GameAssets},
@@ -75,6 +75,13 @@ fn spawn(
Name::from("player"),
Player,
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),
CameraTarget,
transform,