player can take damage

This commit is contained in:
2025-04-02 11:33:01 +08:00
parent f967b1b0da
commit 4652bc4563
8 changed files with 120 additions and 50 deletions

View File

@@ -4,7 +4,7 @@ mod thrown;
use crate::{ use crate::{
GameState, GameState,
heads::ActiveHeads, heads::ActiveHeads,
npc::Hit, hitpoints::Hit,
player::{Player, PlayerRig}, player::{Player, PlayerRig},
sounds::PlaySound, sounds::PlaySound,
tb_entities::EnemySpawn, tb_entities::EnemySpawn,

View File

@@ -3,8 +3,8 @@ use crate::{
GameState, GameState,
aim::AimState, aim::AimState,
billboards::Billboard, billboards::Billboard,
hitpoints::Hit,
loading_assets::GameAssets, loading_assets::GameAssets,
npc::Hit,
physics_layers::GameLayer, physics_layers::GameLayer,
player::{Player, PlayerRig}, player::{Player, PlayerRig},
sounds::PlaySound, sounds::PlaySound,

View File

@@ -1,10 +1,7 @@
use super::AimState; use super::AimState;
use crate::{ use crate::{
GameState, GameState, backpack::UiHeadState, heads::HeadsImages, hitpoints::Hitpoints,
backpack::UiHeadState, loading_assets::UIAssets, npc::NpcHead,
heads::HeadsImages,
loading_assets::UIAssets,
npc::{Hitpoints, NpcHead},
}; };
use bevy::prelude::*; use bevy::prelude::*;

View File

@@ -4,7 +4,8 @@ use crate::{
GameState, GameState,
abilities::HeadAbility, abilities::HeadAbility,
backpack::{BackbackSwapEvent, Backpack}, backpack::{BackbackSwapEvent, Backpack},
player::head_id_to_str, hitpoints::Hitpoints,
player::{Player, head_id_to_str},
sounds::PlaySound, sounds::PlaySound,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -92,6 +93,15 @@ impl ActiveHeads {
false false
} }
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");
return;
};
(head.health, head.health_max) = hp.get()
}
} }
#[derive(Event, Reflect)] #[derive(Event, Reflect)]
@@ -118,7 +128,10 @@ pub fn plugin(app: &mut App) {
}); });
app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(Update, reload.run_if(in_state(GameState::Playing))); app.add_systems(
Update,
(reload, sync_hp).run_if(in_state(GameState::Playing)),
);
app.add_observer(on_select_active_head); app.add_observer(on_select_active_head);
app.add_observer(on_swap_backpack); app.add_observer(on_swap_backpack);
@@ -133,6 +146,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.insert_resource(HeadsImages { heads }); 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 reload(mut commands: Commands, mut active: ResMut<ActiveHeads>, time: Res<Time>) { fn reload(mut commands: Commands, mut active: ResMut<ActiveHeads>, time: Res<Time>) {
if !active.reloading() { if !active.reloading() {
return; return;
@@ -154,6 +175,7 @@ fn on_select_active_head(
trigger: Trigger<SelectActiveHead>, trigger: Trigger<SelectActiveHead>,
mut commands: Commands, mut commands: Commands,
mut res: ResMut<ActiveHeads>, mut res: ResMut<ActiveHeads>,
mut query: Query<&mut Hitpoints, With<Player>>,
) { ) {
match trigger.event() { match trigger.event() {
SelectActiveHead::Right => { SelectActiveHead::Right => {
@@ -164,6 +186,12 @@ fn on_select_active_head(
} }
} }
// sync health back into Hitpoints
let Ok(mut hp) = query.get_single_mut() else {
return;
};
hp.set_health(res.current().unwrap().health);
commands.trigger(PlaySound::Selection); commands.trigger(PlaySound::Selection);
commands.trigger(HeadChanged(res.heads[res.current_slot].unwrap().head)); commands.trigger(HeadChanged(res.heads[res.current_slot].unwrap().head));
} }
@@ -173,6 +201,7 @@ fn on_swap_backpack(
mut commands: Commands, mut commands: Commands,
mut res: ResMut<ActiveHeads>, mut res: ResMut<ActiveHeads>,
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;
@@ -189,5 +218,11 @@ fn on_swap_backpack(
backpack.heads.remove(backpack_slot); 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);
commands.trigger(HeadChanged(res.heads[res.current_slot].unwrap().head)); commands.trigger(HeadChanged(res.heads[res.current_slot].unwrap().head));
} }

61
src/hitpoints.rs Normal file
View File

@@ -0,0 +1,61 @@
use bevy::prelude::*;
use crate::sounds::PlaySound;
#[derive(Event, Reflect)]
pub struct Kill;
#[derive(Event, Reflect)]
pub struct Hit {
pub damage: u32,
}
#[derive(Component, Reflect, Debug)]
pub struct Hitpoints {
max: u32,
current: u32,
}
impl Hitpoints {
pub fn new(v: u32) -> Self {
Self { max: v, current: v }
}
pub fn health(&self) -> f32 {
self.current as f32 / self.max as f32
}
pub fn set_health(&mut self, v: u32) {
self.current = v;
}
pub fn get(&self) -> (u32, u32) {
(self.current, self.max)
}
}
pub fn plugin(app: &mut App) {
app.add_systems(Update, on_hp_added);
}
fn on_hp_added(mut commands: Commands, query: Query<Entity, Added<Hitpoints>>) {
for e in query.iter() {
commands.entity(e).observe(on_hit);
}
}
fn on_hit(trigger: Trigger<Hit>, mut commands: Commands, mut query: Query<&mut Hitpoints>) {
let Hit { damage } = trigger.event();
let Ok(mut hp) = query.get_mut(trigger.entity()) else {
return;
};
commands.trigger(PlaySound::Hit);
hp.current = hp.current.saturating_sub(*damage);
if hp.current == 0 {
commands.trigger_targets(Kill, trigger.entity());
}
}

View File

@@ -8,6 +8,7 @@ mod control;
mod cutscene; mod cutscene;
mod gates; mod gates;
mod heads; mod heads;
mod hitpoints;
mod keys; mod keys;
mod loading_assets; mod loading_assets;
mod loading_map; mod loading_map;
@@ -116,6 +117,7 @@ fn main() {
app.add_plugins(sprite_3d_animation::plugin); app.add_plugins(sprite_3d_animation::plugin);
app.add_plugins(abilities::plugin); app.add_plugins(abilities::plugin);
app.add_plugins(heads::plugin); app.add_plugins(heads::plugin);
app.add_plugins(hitpoints::plugin);
app.init_state::<GameState>(); app.init_state::<GameState>();

View File

@@ -1,31 +1,14 @@
use crate::{ use crate::{
GameState, heads::HEAD_COUNT, keys::KeySpawn, player::head_id_to_str, sounds::PlaySound, GameState,
heads::HEAD_COUNT,
hitpoints::{Hitpoints, Kill},
keys::KeySpawn,
player::head_id_to_str,
tb_entities::EnemySpawn, tb_entities::EnemySpawn,
}; };
use bevy::prelude::*; use bevy::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Event, Reflect)]
pub struct Hit {
pub damage: u32,
}
#[derive(Component, Reflect)]
pub struct Hitpoints {
max: i32,
current: i32,
}
impl Hitpoints {
fn new(v: i32) -> Self {
Self { max: v, current: v }
}
pub fn health(&self) -> f32 {
self.current as f32 / self.max as f32
}
}
#[derive(Component)] #[derive(Component)]
pub struct NpcHead(pub usize); pub struct NpcHead(pub usize);
@@ -33,7 +16,7 @@ pub fn plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Playing), init); app.add_systems(OnEnter(GameState::Playing), init);
} }
fn init(mut commands: Commands, query: Query<(Entity, &EnemySpawn), Without<Hitpoints>>) { fn init(mut commands: Commands, query: Query<(Entity, &EnemySpawn)>) {
let mut names: HashMap<String, usize> = HashMap::default(); let mut names: HashMap<String, usize> = HashMap::default();
for i in 0..HEAD_COUNT { for i in 0..HEAD_COUNT {
names.insert(head_id_to_str(i).to_string(), i); names.insert(head_id_to_str(i).to_string(), i);
@@ -43,32 +26,22 @@ fn init(mut commands: Commands, query: Query<(Entity, &EnemySpawn), Without<Hitp
commands commands
.entity(e) .entity(e)
.insert((Hitpoints::new(100), NpcHead(id))) .insert((Hitpoints::new(100), NpcHead(id)))
.observe(on_hit); .observe(on_kill);
} }
} }
fn on_hit( fn on_kill(
trigger: Trigger<Hit>, trigger: Trigger<Kill>,
mut commands: Commands, mut commands: Commands,
mut query: Query<(&mut Hitpoints, &Transform, &EnemySpawn)>, query: Query<(&Transform, &EnemySpawn)>,
) { ) {
let Hit { damage } = trigger.event(); let Ok((transform, enemy)) = query.get(trigger.entity()) else {
let Ok((mut hp, transform, enemy)) = query.get_mut(trigger.entity()) else {
return; return;
}; };
commands.trigger(PlaySound::Hit);
hp.current = hp.current.saturating_sub(*damage as i32);
info!("npc hp changed: {} [{}]", hp.current, trigger.entity());
if hp.current <= 0 {
commands.entity(trigger.entity()).despawn_recursive(); commands.entity(trigger.entity()).despawn_recursive();
if !enemy.key.is_empty() { if !enemy.key.is_empty() {
commands.trigger(KeySpawn(transform.translation, enemy.key.clone())); commands.trigger(KeySpawn(transform.translation, enemy.key.clone()));
} }
}
} }

View File

@@ -8,6 +8,7 @@ use crate::{
controller::{CharacterControllerBundle, MovementBundle, PlayerMovement}, controller::{CharacterControllerBundle, MovementBundle, PlayerMovement},
}, },
heads::HeadChanged, heads::HeadChanged,
hitpoints::Hitpoints,
loading_assets::GameAssets, loading_assets::GameAssets,
physics_layers::GameLayer, physics_layers::GameLayer,
sounds::PlaySound, sounds::PlaySound,
@@ -95,6 +96,7 @@ fn spawn(
.spawn(( .spawn((
Name::from("player"), Name::from("player"),
Player(0), Player(0),
Hitpoints::new(100),
CameraTarget, CameraTarget,
transform, transform,
Visibility::default(), Visibility::default(),