Wiring up animations (#47)

This commit is contained in:
PROMETHIA-27
2025-06-23 06:02:26 -04:00
committed by GitHub
parent 1aa83320b6
commit 3997beee03
21 changed files with 333 additions and 169 deletions

View File

@@ -1,9 +1,8 @@
use bevy::prelude::*;
use crate::{ use crate::{
GameState, global_observer, heads::ActiveHeads, heads_database::HeadsDatabase, GameState, global_observer, heads::ActiveHeads, heads_database::HeadsDatabase,
hitpoints::Hitpoints, loading_assets::AudioAssets, hitpoints::Hitpoints, loading_assets::AudioAssets,
}; };
use bevy::prelude::*;
#[derive(Component)] #[derive(Component)]
pub struct Healing(pub Entity); pub struct Healing(pub Entity);

View File

@@ -5,8 +5,6 @@ mod healing;
mod missile; mod missile;
mod thrown; mod thrown;
pub use healing::Healing;
use crate::{ use crate::{
GameState, GameState,
aim::AimTarget, aim::AimTarget,
@@ -19,6 +17,7 @@ use crate::{
sounds::PlaySound, sounds::PlaySound,
}; };
use bevy::prelude::*; use bevy::prelude::*;
pub use healing::Healing;
use healing::HealingStateChanged; use healing::HealingStateChanged;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@@ -1,5 +1,3 @@
use bevy::prelude::*;
use crate::{ use crate::{
GameState, GameState,
abilities::{HeadAbility, TriggerData, TriggerThrow}, abilities::{HeadAbility, TriggerData, TriggerThrow},
@@ -8,6 +6,7 @@ use crate::{
heads_database::HeadsDatabase, heads_database::HeadsDatabase,
player::Player, player::Player,
}; };
use bevy::prelude::*;
#[derive(Component, Reflect)] #[derive(Component, Reflect)]
#[reflect(Component)] #[reflect(Component)]

127
src/animation.rs Normal file
View File

@@ -0,0 +1,127 @@
use crate::{
GameState, character::CharacterAnimations, head::ActiveHead, heads_database::HeadsDatabase,
};
use bevy::{animation::RepeatAnimation, ecs::query::QueryData, prelude::*};
use std::time::Duration;
pub fn plugin(app: &mut App) {
app.register_type::<AnimationFlags>();
app.add_systems(
Update,
update_animation.run_if(in_state(GameState::Playing)),
);
}
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
pub struct AnimationFlags {
pub any_direction: bool,
pub jumping: bool,
pub just_jumped: bool,
pub shooting: bool,
pub hit: bool,
}
#[derive(QueryData)]
#[query_data(mutable)]
pub struct AnimationController {
pub transitions: &'static mut AnimationTransitions,
pub player: &'static mut AnimationPlayer,
}
impl AnimationController {
pub fn play_inner(
player: &mut AnimationPlayer,
transitions: &mut AnimationTransitions,
animation: AnimationNodeIndex,
transition: Duration,
repeat: RepeatAnimation,
) {
transitions
.play(player, animation, transition)
.set_repeat(repeat);
}
}
impl AnimationControllerItem<'_> {
pub fn play(
&mut self,
animation: AnimationNodeIndex,
transition: Duration,
repeat: RepeatAnimation,
) {
AnimationController::play_inner(
&mut self.player,
&mut self.transitions,
animation,
transition,
repeat,
);
}
pub fn is_playing(&self, index: AnimationNodeIndex) -> bool {
self.player.is_playing_animation(index)
}
}
const DEFAULT_TRANSITION_DURATION: Duration = Duration::from_millis(100);
fn update_animation(
mut animated: Query<(
AnimationController,
&CharacterAnimations,
&mut AnimationFlags,
)>,
) {
for (mut controller, anims, mut flags) in animated.iter_mut() {
if flags.shooting && flags.any_direction && anims.run_shoot.is_some() {
if !controller.is_playing(anims.run_shoot.unwrap()) {
controller.play(
anims.run_shoot.unwrap(),
DEFAULT_TRANSITION_DURATION,
RepeatAnimation::Forever,
);
}
} else if flags.shooting && anims.shoot.is_some() {
if !controller.is_playing(anims.shoot.unwrap()) {
controller.play(
anims.shoot.unwrap(),
DEFAULT_TRANSITION_DURATION,
RepeatAnimation::Forever,
);
}
} else if flags.hit {
if !controller.is_playing(anims.hit) {
controller.play(
anims.hit,
DEFAULT_TRANSITION_DURATION,
RepeatAnimation::Never,
);
}
} else if flags.jumping {
if !controller.is_playing(anims.jump) || flags.just_jumped {
controller.play(
anims.jump,
DEFAULT_TRANSITION_DURATION,
RepeatAnimation::Never,
);
flags.just_jumped = false;
}
} else if flags.any_direction {
if !controller.player.is_playing_animation(anims.run) {
controller.play(
anims.run,
DEFAULT_TRANSITION_DURATION,
RepeatAnimation::Forever,
);
}
} else if !controller.is_playing(anims.idle) {
controller.play(
anims.idle,
DEFAULT_TRANSITION_DURATION,
RepeatAnimation::Forever,
);
}
}
}

View File

@@ -5,9 +5,8 @@ use crate::{
cash::CashCollectEvent, global_observer, head_drop::HeadCollected, heads::HeadState, cash::CashCollectEvent, global_observer, head_drop::HeadCollected, heads::HeadState,
heads_database::HeadsDatabase, heads_database::HeadsDatabase,
}; };
use bevy::prelude::*;
pub use backpack_ui::BackpackAction; pub use backpack_ui::BackpackAction;
use bevy::prelude::*;
pub use ui_head_state::UiHeadState; pub use ui_head_state::UiHeadState;
#[derive(Resource, Default)] #[derive(Resource, Default)]

View File

@@ -1,9 +1,8 @@
use avian3d::prelude::*;
use bevy::prelude::*;
use crate::{ use crate::{
GameState, control::ControlState, loading_assets::UIAssets, physics_layers::GameLayer, GameState, control::ControlState, loading_assets::UIAssets, physics_layers::GameLayer,
}; };
use avian3d::prelude::*;
use bevy::prelude::*;
#[derive(Component, Reflect, Debug)] #[derive(Component, Reflect, Debug)]
pub struct CameraTarget; pub struct CameraTarget;

View File

@@ -1,8 +1,14 @@
use crate::{ use crate::{
GameState, heads_database::HeadsDatabase, loading_assets::GameAssets, utils::trail::Trail, GameState,
animation::{AnimationController, AnimationFlags},
heads_database::HeadsDatabase,
hitpoints::Hitpoints,
loading_assets::GameAssets,
utils::trail::Trail,
}; };
use bevy::{ use bevy::{
ecs::system::SystemParam, platform::collections::HashMap, prelude::*, scene::SceneInstanceReady, animation::RepeatAnimation, ecs::system::SystemParam, platform::collections::HashMap,
prelude::*, scene::SceneInstanceReady,
}; };
use std::{f32::consts::PI, time::Duration}; use std::{f32::consts::PI, time::Duration};
@@ -39,14 +45,31 @@ impl CharacterHierarchy<'_, '_> {
#[derive(Component, Debug, Reflect)] #[derive(Component, Debug, Reflect)]
#[reflect(Component)] #[reflect(Component)]
#[relationship(relationship_target = HasCharacterAnimations)]
#[require(AnimationFlags)]
pub struct CharacterAnimations { pub struct CharacterAnimations {
#[relationship]
pub of_character: Entity,
pub idle: AnimationNodeIndex, pub idle: AnimationNodeIndex,
pub run: AnimationNodeIndex, pub run: AnimationNodeIndex,
pub jump: AnimationNodeIndex, pub jump: AnimationNodeIndex,
pub shooting: Option<AnimationNodeIndex>, pub shoot: Option<AnimationNodeIndex>,
pub run_shoot: Option<AnimationNodeIndex>,
pub hit: AnimationNodeIndex,
pub graph: Handle<AnimationGraph>, pub graph: Handle<AnimationGraph>,
} }
const ANIM_IDLE: &str = "idle";
const ANIM_RUN: &str = "run";
const ANIM_JUMP: &str = "jump";
const ANIM_SHOOT: &str = "shoot";
const ANIM_RUN_SHOOT: &str = "run_shoot";
const ANIM_HIT: &str = "hit";
#[derive(Component)]
#[relationship_target(relationship = CharacterAnimations)]
pub struct HasCharacterAnimations(Entity);
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_systems( app.add_systems(
Update, Update,
@@ -142,18 +165,25 @@ fn setup_once_loaded(
mut query: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>, mut query: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
parent: Query<&ChildOf>, parent: Query<&ChildOf>,
animated_character: Query<(&AnimatedCharacter, &AnimatedCharacterAsset)>, animated_character: Query<(&AnimatedCharacter, &AnimatedCharacterAsset)>,
characters: Query<Entity, With<Hitpoints>>,
gltf_assets: Res<Assets<Gltf>>, gltf_assets: Res<Assets<Gltf>>,
mut graphs: ResMut<Assets<AnimationGraph>>, mut graphs: ResMut<Assets<AnimationGraph>>,
) { ) {
for (entity, mut player) in &mut query { for (entity, mut player) in query.iter_mut() {
let Some((_character, asset)) = parent let Some((_, asset)) = parent
.iter_ancestors(entity) .iter_ancestors(entity)
.find_map(|ancestor| animated_character.get(ancestor).ok()) .find_map(|ancestor| animated_character.get(ancestor).ok())
else { else {
continue; continue;
}; };
let Some(character) = parent
.iter_ancestors(entity)
.find_map(|ancestor| characters.get(ancestor).ok())
else {
continue;
};
let asset = gltf_assets.get(asset.0.id()).unwrap(); let asset = gltf_assets.get(asset.0.id()).unwrap();
let animations = asset let animations = asset
@@ -162,41 +192,45 @@ fn setup_once_loaded(
.map(|(name, animation)| (name.to_string(), animation.clone())) .map(|(name, animation)| (name.to_string(), animation.clone()))
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
let mut clips = vec![ let mut graph = AnimationGraph::new();
animations["idle"].clone(), let root = graph.root;
animations["run"].clone(), let idle = graph.add_clip(animations[ANIM_IDLE].clone(), 1.0, root);
animations["jump"].clone(), let run = graph.add_clip(animations[ANIM_RUN].clone(), 1.0, root);
]; let jump = graph.add_clip(animations[ANIM_JUMP].clone(), 1.0, root);
let shoot = animations
if let Some(shooting_animation) = animations.get("shoot") { .get(ANIM_SHOOT)
clips.push(shooting_animation.clone()); .map(|it| graph.add_clip(it.clone(), 1.0, root));
} let run_shoot = animations
.get(ANIM_RUN_SHOOT)
let (graph, node_indices) = AnimationGraph::from_clips(clips); .map(|it| graph.add_clip(it.clone(), 1.0, root));
let hit = graph.add_clip(animations[ANIM_HIT].clone(), 1.0, root);
// Insert a resource with the current scene information // Insert a resource with the current scene information
let graph_handle = graphs.add(graph); let graph_handle = graphs.add(graph);
let animations = CharacterAnimations { let animations = CharacterAnimations {
idle: node_indices[0], of_character: character,
run: node_indices[1], idle,
jump: node_indices[2], run,
shooting: if node_indices.len() == 4 { jump,
Some(node_indices[3]) shoot,
} else { run_shoot,
None hit,
},
graph: graph_handle.clone(), graph: graph_handle.clone(),
}; };
let mut transitions = AnimationTransitions::new(); let mut transitions = AnimationTransitions::new();
transitions AnimationController::play_inner(
.play(&mut player, animations.idle, Duration::ZERO) &mut player,
.repeat(); &mut transitions,
commands animations.idle,
.entity(entity) Duration::ZERO,
.insert(AnimationGraphHandle(animations.graph.clone())) RepeatAnimation::Forever,
.insert(transitions) );
.insert(animations); commands.entity(entity).insert((
AnimationGraphHandle(animations.graph.clone()),
transitions,
animations,
));
} }
} }

View File

@@ -1,27 +1,26 @@
use super::{ControllerSet, ControllerSwitchEvent};
use crate::{
GameState,
control::{SelectedController, controls::ControllerSettings},
heads_database::HeadControls,
player::PlayerBodyMesh,
};
use avian3d::{math::*, prelude::*}; use avian3d::{math::*, prelude::*};
use bevy::prelude::*; use bevy::prelude::*;
use happy_feet::KinematicVelocity; use happy_feet::{
use happy_feet::ground::{Grounding, GroundingConfig}; KinematicVelocity,
use happy_feet::prelude::{ ground::{Grounding, GroundingConfig},
Character, CharacterDrag, CharacterFriction, CharacterGravity, CharacterMovement, prelude::{
CharacterPlugin, MoveInput, SteppingBehaviour, SteppingConfig, Character, CharacterDrag, CharacterFriction, CharacterGravity, CharacterMovement,
CharacterPlugin, MoveInput, SteppingBehaviour, SteppingConfig,
},
}; };
use crate::GameState;
use crate::control::SelectedController;
use crate::control::controls::ControllerSettings;
use crate::heads_database::HeadControls;
use crate::player::PlayerBodyMesh;
use super::{ControllerSet, ControllerSwitchEvent};
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_plugins(CharacterPlugin::default()); app.add_plugins(CharacterPlugin::default());
app.register_type::<MovementSpeedFactor>(); app.register_type::<MovementSpeedFactor>();
app.init_resource::<PlayerMovement>();
app.add_systems( app.add_systems(
PreUpdate, PreUpdate,
reset_upon_switch reset_upon_switch
@@ -78,7 +77,7 @@ fn decelerate(
&ControllerSettings, &ControllerSettings,
)>, )>,
) { ) {
for (mut velocity, input, grounding, settings) in &mut character { for (mut velocity, input, grounding, settings) in character.iter_mut() {
let direction = input.value.normalize(); let direction = input.value.normalize();
let ground_normal = grounding let ground_normal = grounding
.and_then(|it| it.normal()) .and_then(|it| it.normal())
@@ -105,12 +104,6 @@ fn decelerate(
} }
} }
#[derive(Resource, Default)]
pub struct PlayerMovement {
pub any_direction: bool,
pub shooting: bool,
}
#[derive(Component, Reflect)] #[derive(Component, Reflect)]
#[reflect(Component)] #[reflect(Component)]
pub struct MovementSpeedFactor(pub f32); pub struct MovementSpeedFactor(pub f32);

View File

@@ -1,13 +1,8 @@
use std::f32::consts::PI; use super::{ControlState, ControllerSet};
use crate::{GameState, control::controller_common::MovementSpeedFactor, player::PlayerBodyMesh};
use bevy::prelude::*; use bevy::prelude::*;
use happy_feet::prelude::MoveInput; use happy_feet::prelude::MoveInput;
use std::f32::consts::PI;
use crate::GameState;
use crate::control::controller_common::MovementSpeedFactor;
use crate::player::PlayerBodyMesh;
use super::{ControlState, ControllerSet};
pub struct CharacterControllerPlugin; pub struct CharacterControllerPlugin;

View File

@@ -1,19 +1,22 @@
use super::{ControlState, ControllerSet, Controls}; use super::{ControlState, ControllerSet, Controls};
use crate::control::controller_common::MovementSpeedFactor; use crate::{
use crate::control::controls::ControllerSettings; GameState,
use crate::{GameState, abilities::TriggerStateRes, player::PlayerBodyMesh}; abilities::TriggerStateRes,
animation::AnimationFlags,
character::HasCharacterAnimations,
control::{controller_common::MovementSpeedFactor, controls::ControllerSettings},
player::{Player, PlayerBodyMesh},
};
use bevy::prelude::*; use bevy::prelude::*;
use happy_feet::prelude::{Grounding, KinematicVelocity, MoveInput}; use happy_feet::prelude::{Grounding, KinematicVelocity, MoveInput};
use super::controller_common::PlayerMovement;
pub struct CharacterControllerPlugin; pub struct CharacterControllerPlugin;
impl Plugin for CharacterControllerPlugin { impl Plugin for CharacterControllerPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems( app.add_systems(
PreUpdate, PreUpdate,
(set_movement_flag, rotate_view, apply_controls) (set_animation_flags, rotate_view, apply_controls)
.chain() .chain()
.in_set(ControllerSet::ApplyControlsRun) .in_set(ControllerSet::ApplyControlsRun)
.run_if(in_state(GameState::Playing)), .run_if(in_state(GameState::Playing)),
@@ -22,28 +25,38 @@ impl Plugin for CharacterControllerPlugin {
} }
/// Sets the movement flag, which is an indicator for the rig animation and the braking system. /// Sets the movement flag, which is an indicator for the rig animation and the braking system.
fn set_movement_flag( fn set_animation_flags(
mut player_movement: ResMut<PlayerMovement>, mut flags: Query<&mut AnimationFlags>,
controls: Res<Controls>, controls: Res<Controls>,
trigger: Res<TriggerStateRes>, trigger: Res<TriggerStateRes>,
player: Single<(&Grounding, &HasCharacterAnimations), With<Player>>,
) { ) {
let mut direction = controls.keyboard_state.move_dir; let mut direction = controls.keyboard_state.move_dir;
let deadzone = 0.2; let deadzone = 0.2;
let (grounding, has_anims) = *player;
let mut flags = flags.get_mut(*has_anims.collection()).unwrap();
if let Some(gamepad) = controls.gamepad_state { if let Some(gamepad) = controls.gamepad_state {
direction += gamepad.move_dir; direction += gamepad.move_dir;
} }
if player_movement.any_direction { if flags.any_direction {
if direction.length_squared() < deadzone { if direction.length_squared() < deadzone {
player_movement.any_direction = false; flags.any_direction = false;
} }
} else if direction.length_squared() > deadzone { } else if direction.length_squared() > deadzone {
player_movement.any_direction = true; flags.any_direction = true;
} }
if player_movement.shooting != trigger.is_active() { if flags.shooting != trigger.is_active() {
player_movement.shooting = trigger.is_active(); flags.shooting = trigger.is_active();
}
// `apply_controls` sets the jump flag when the player actually jumps.
// Unset the flag on hitting the ground
if grounding.is_grounded() {
flags.jumping = false;
} }
} }
@@ -55,7 +68,7 @@ fn rotate_view(
return; return;
} }
for mut tr in &mut player { for mut tr in player.iter_mut() {
tr.rotate_y(controls.look_dir.x * -0.001); tr.rotate_y(controls.look_dir.x * -0.001);
} }
} }
@@ -68,15 +81,19 @@ fn apply_controls(
&mut KinematicVelocity, &mut KinematicVelocity,
&ControllerSettings, &ControllerSettings,
&MovementSpeedFactor, &MovementSpeedFactor,
&HasCharacterAnimations,
)>, )>,
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>, rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
mut anim_flags: Query<&mut AnimationFlags>,
) { ) {
let Ok((mut move_input, mut grounding, mut velocity, settings, move_factor)) = let Ok((mut move_input, mut grounding, mut velocity, settings, move_factor, has_anims)) =
character.single_mut() character.single_mut()
else { else {
return; return;
}; };
let mut flags = anim_flags.get_mut(*has_anims.collection()).unwrap();
let mut direction = -controls.move_dir.extend(0.0).xzy(); let mut direction = -controls.move_dir.extend(0.0).xzy();
if let Some(ref rig_transform) = rig_transform_q { if let Some(ref rig_transform) = rig_transform_q {
@@ -92,6 +109,8 @@ fn apply_controls(
move_input.set(direction * move_factor.0); move_input.set(direction * move_factor.0);
if controls.jump && grounding.is_grounded() { if controls.jump && grounding.is_grounded() {
flags.jumping = true;
flags.just_jumped = true;
happy_feet::movement::jump(settings.jump_force, &mut velocity, &mut grounding, Dir3::Y) happy_feet::movement::jump(settings.jump_force, &mut velocity, &mut grounding, Dir3::Y)
} }
} }

View File

@@ -1,8 +1,9 @@
use crate::control::ControllerSet; use super::{ControlState, Controls};
use crate::{ use crate::{
GameState, GameState,
abilities::{TriggerCashHeal, TriggerState}, abilities::{TriggerCashHeal, TriggerState},
backpack::BackpackAction, backpack::BackpackAction,
control::ControllerSet,
heads::SelectActiveHead, heads::SelectActiveHead,
}; };
use bevy::{ use bevy::{
@@ -14,8 +15,6 @@ use bevy::{
prelude::*, prelude::*,
}; };
use super::{ControlState, Controls};
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.init_resource::<Controls>(); app.init_resource::<Controls>();

View File

@@ -1,10 +1,9 @@
use bevy::prelude::*;
use crate::{ use crate::{
GameState, GameState,
head::ActiveHead, head::ActiveHead,
heads_database::{HeadControls, HeadsDatabase}, heads_database::{HeadControls, HeadsDatabase},
}; };
use bevy::prelude::*;
pub mod controller_common; pub mod controller_common;
pub mod controller_flying; pub mod controller_flying;

View File

@@ -1,10 +1,9 @@
use super::{ActiveHeads, HEAD_SLOTS, HeadsImages};
use crate::{GameState, backpack::UiHeadState, loading_assets::UIAssets, player::Player}; use crate::{GameState, backpack::UiHeadState, loading_assets::UIAssets, player::Player};
use bevy::{ecs::spawn::SpawnIter, prelude::*}; use bevy::{ecs::spawn::SpawnIter, 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;
use super::{ActiveHeads, HEAD_SLOTS, HeadsImages};
#[derive(Component, Reflect, Default)] #[derive(Component, Reflect, Default)]
#[reflect(Component)] #[reflect(Component)]
struct HeadSelector(pub usize); struct HeadSelector(pub usize);

View File

@@ -202,7 +202,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, heads: Res<Head
fn sync_hp(mut query: Query<(&mut ActiveHeads, &Hitpoints)>) { fn sync_hp(mut query: Query<(&mut ActiveHeads, &Hitpoints)>) {
for (mut active_heads, hp) in query.iter_mut() { for (mut active_heads, hp) in query.iter_mut() {
if active_heads.hp() != *hp { if active_heads.hp().get() != hp.get() {
active_heads.set_hitpoint(hp); active_heads.set_hitpoint(hp);
} }
} }

View File

@@ -1,7 +1,11 @@
use crate::{
GameState,
animation::AnimationFlags,
character::{CharacterAnimations, HasCharacterAnimations},
sounds::PlaySound,
};
use bevy::prelude::*; use bevy::prelude::*;
use crate::sounds::PlaySound;
#[derive(Event, Reflect)] #[derive(Event, Reflect)]
pub struct Kill; pub struct Kill;
@@ -10,15 +14,20 @@ pub struct Hit {
pub damage: u32, pub damage: u32,
} }
#[derive(Component, Reflect, Debug, PartialEq, Eq, Clone, Copy)] #[derive(Component, Reflect, Debug, Clone, Copy)]
pub struct Hitpoints { pub struct Hitpoints {
max: u32, max: u32,
current: u32, current: u32,
last_hit_timestamp: f32,
} }
impl Hitpoints { impl Hitpoints {
pub fn new(v: u32) -> Self { pub fn new(v: u32) -> Self {
Self { max: v, current: v } Self {
max: v,
current: v,
last_hit_timestamp: f32::NEG_INFINITY,
}
} }
pub fn with_health(mut self, v: u32) -> Self { pub fn with_health(mut self, v: u32) -> Self {
@@ -45,10 +54,17 @@ impl Hitpoints {
pub fn max(&self) -> bool { pub fn max(&self) -> bool {
self.current == self.max self.current == self.max
} }
pub fn time_since_hit(&self, time: &Time) -> f32 {
time.elapsed_secs() - self.last_hit_timestamp
}
} }
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_systems(Update, on_hp_added); app.add_systems(Update, on_hp_added).add_systems(
PreUpdate,
reset_hit_animation_flag.run_if(in_state(GameState::Playing)),
);
} }
fn on_hp_added(mut commands: Commands, query: Query<Entity, Added<Hitpoints>>) { fn on_hp_added(mut commands: Commands, query: Query<Entity, Added<Hitpoints>>) {
@@ -57,18 +73,52 @@ fn on_hp_added(mut commands: Commands, query: Query<Entity, Added<Hitpoints>>) {
} }
} }
fn on_hit(trigger: Trigger<Hit>, mut commands: Commands, mut query: Query<&mut Hitpoints>) { fn on_hit(
trigger: Trigger<Hit>,
mut commands: Commands,
mut query: Query<(&mut Hitpoints, Option<&HasCharacterAnimations>)>,
mut anim_flags: Query<&mut AnimationFlags>,
time: Res<Time>,
) {
let Hit { damage } = trigger.event(); let Hit { damage } = trigger.event();
let Ok(mut hp) = query.get_mut(trigger.target()) else { let Ok((mut hp, has_anims)) = query.get_mut(trigger.target()) else {
return; return;
}; };
commands.trigger(PlaySound::Hit); commands.trigger(PlaySound::Hit);
if let Some(has_anims) = has_anims {
let mut flags = anim_flags.get_mut(*has_anims.collection()).unwrap();
flags.hit = true;
}
hp.current = hp.current.saturating_sub(*damage); hp.current = hp.current.saturating_sub(*damage);
hp.last_hit_timestamp = time.elapsed_secs();
if hp.current == 0 { if hp.current == 0 {
commands.trigger_targets(Kill, trigger.target()); commands.trigger_targets(Kill, trigger.target());
} }
} }
fn reset_hit_animation_flag(
query: Query<(&Hitpoints, &HasCharacterAnimations)>,
mut animations: Query<(
&AnimationGraphHandle,
&CharacterAnimations,
&mut AnimationFlags,
)>,
graphs: Res<Assets<AnimationGraph>>,
clips: Res<Assets<AnimationClip>>,
time: Res<Time>,
) {
for (hp, anims) in query.iter() {
let (graph_handle, anims, mut flags) = animations.get_mut(*anims.collection()).unwrap();
let graph = graphs.get(graph_handle.id()).unwrap();
let hit_anim = match graph.get(anims.hit).unwrap().node_type {
AnimationNodeType::Clip(ref handle) => clips.get(handle.id()).unwrap(),
_ => unreachable!(),
};
flags.hit = hp.time_since_hit(&time) < hit_anim.duration();
}
}

View File

@@ -1,6 +1,7 @@
mod abilities; mod abilities;
mod ai; mod ai;
mod aim; mod aim;
mod animation;
mod backpack; mod backpack;
mod camera; mod camera;
mod cash; mod cash;
@@ -29,6 +30,7 @@ mod tb_entities;
mod utils; mod utils;
mod water; mod water;
use crate::utils::{auto_rotate, explosions};
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::{ use bevy::{
audio::{PlaybackMode, Volume}, audio::{PlaybackMode, Volume},
@@ -47,8 +49,6 @@ use loading_assets::AudioAssets;
use std::io::{Read, Write}; use std::io::{Read, Write};
use utils::{billboards, sprite_3d_animation, squish_animation, trail}; use utils::{billboards, sprite_3d_animation, squish_animation, trail};
use crate::utils::{auto_rotate, explosions};
#[derive(Resource, Reflect, Debug)] #[derive(Resource, Reflect, Debug)]
#[reflect(Resource)] #[reflect(Resource)]
struct DebugVisuals { struct DebugVisuals {
@@ -143,6 +143,7 @@ fn main() {
} }
app.add_plugins(ai::plugin); app.add_plugins(ai::plugin);
app.add_plugins(animation::plugin);
app.add_plugins(character::plugin); app.add_plugins(character::plugin);
app.add_plugins(cash::plugin); app.add_plugins(cash::plugin);
app.add_plugins(player::plugin); app.add_plugins(player::plugin);

View File

@@ -2,8 +2,8 @@ use crate::{
GameState, GameState,
camera::{CameraArmRotation, CameraTarget}, camera::{CameraArmRotation, CameraTarget},
cash::{Cash, CashCollectEvent}, cash::{Cash, CashCollectEvent},
character::{AnimatedCharacter, CharacterAnimations}, character::AnimatedCharacter,
control::controller_common::{CharacterControllerBundle, PlayerMovement}, control::controller_common::CharacterControllerBundle,
global_observer, global_observer,
head::ActiveHead, head::ActiveHead,
head_drop::HeadDrops, head_drop::HeadDrops,
@@ -22,14 +22,10 @@ use bevy::{
prelude::*, prelude::*,
window::{CursorGrabMode, PrimaryWindow}, window::{CursorGrabMode, PrimaryWindow},
}; };
use std::time::Duration;
#[derive(Component, Default)] #[derive(Component, Default)]
pub struct Player; pub struct Player;
#[derive(Component, Default)]
struct PlayerAnimations;
#[derive(Component, Default)] #[derive(Component, Default)]
#[require(Transform, Visibility)] #[require(Transform, Visibility)]
pub struct PlayerBodyMesh; pub struct PlayerBodyMesh;
@@ -41,7 +37,6 @@ pub fn plugin(app: &mut App) {
Update, Update,
( (
collect_cash, collect_cash,
toggle_animation,
setup_animations_marker_for_player, setup_animations_marker_for_player,
toggle_cursor_system.run_if(input_just_pressed(KeyCode::Escape)), toggle_cursor_system.run_if(input_just_pressed(KeyCode::Escape)),
) )
@@ -172,57 +167,19 @@ fn collect_cash(
fn setup_animations_marker_for_player( fn setup_animations_marker_for_player(
mut commands: Commands, mut commands: Commands,
animation_handles: Query<Entity, Added<AnimationGraphHandle>>, animation_handles: Query<Entity, Added<AnimationGraphHandle>>,
children: Query<&ChildOf>, child_of: Query<&ChildOf>,
player: Query<&PlayerBodyMesh>, player_rig: Query<&ChildOf, With<PlayerBodyMesh>>,
) { ) {
for entity in animation_handles.iter() { for animation_rig in animation_handles.iter() {
for ancestor in children.iter_ancestors(entity) { for ancestor in child_of.iter_ancestors(animation_rig) {
if player.contains(ancestor) { if let Ok(rig_child_of) = player_rig.get(ancestor) {
commands.entity(entity).insert(PlayerAnimations); commands.entity(rig_child_of.parent());
return; return;
} }
} }
} }
} }
fn toggle_animation(
mut transitions: Query<
(
&mut AnimationTransitions,
&mut AnimationPlayer,
&CharacterAnimations,
),
With<PlayerAnimations>,
>,
movement: Res<PlayerMovement>,
) {
if movement.is_changed() {
for (mut transition, mut player, animations) in &mut transitions {
let index = if movement.any_direction {
animations.run
} else {
animations.idle
};
let (mut index, mut duration) = (index, Duration::from_millis(100));
if movement.shooting {
if let Some(shoot_anim) = animations.shooting {
(index, duration) = (shoot_anim, Duration::from_millis(10));
}
}
if transition
.get_main_animation()
.map(|playing| playing != index)
.unwrap_or_default()
{
transition.play(&mut player, index, duration).repeat();
}
}
}
}
fn on_update_head_mesh( fn on_update_head_mesh(
trigger: Trigger<HeadChanged>, trigger: Trigger<HeadChanged>,
mut commands: Commands, mut commands: Commands,

View File

@@ -1,5 +1,4 @@
use std::f32::consts::PI; use crate::{cash::Cash, loading_assets::GameAssets, physics_layers::GameLayer};
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::{ use bevy::{
ecs::{component::HookContext, world::DeferredWorld}, ecs::{component::HookContext, world::DeferredWorld},
@@ -8,10 +7,7 @@ use bevy::{
}; };
use bevy_trenchbroom::prelude::*; use bevy_trenchbroom::prelude::*;
use happy_feet::prelude::PhysicsMover; use happy_feet::prelude::PhysicsMover;
use std::f32::consts::PI;
use crate::cash::Cash;
use crate::loading_assets::GameAssets;
use crate::physics_layers::GameLayer;
#[derive(PointClass, Component, Reflect, Default)] #[derive(PointClass, Component, Reflect, Default)]
#[reflect(QuakeClass, Component)] #[reflect(QuakeClass, Component)]

View File

@@ -1,8 +1,7 @@
use crate::{global_observer, hitpoints::Hit, physics_layers::GameLayer};
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::prelude::*; use bevy::prelude::*;
use crate::{global_observer, hitpoints::Hit, physics_layers::GameLayer};
#[derive(Event, Debug)] #[derive(Event, Debug)]
pub struct Explosion { pub struct Explosion {
pub position: Vec3, pub position: Vec3,

View File

@@ -1,6 +1,5 @@
use bevy::prelude::*;
use crate::GameState; use crate::GameState;
use bevy::prelude::*;
#[derive(Component)] #[derive(Component)]
pub struct Trail { pub struct Trail {

View File

@@ -1,5 +1,7 @@
use crate::control::controller_common::MovementSpeedFactor; use crate::{
use crate::{GameState, global_observer, player::Player, tb_entities::Water}; GameState, control::controller_common::MovementSpeedFactor, global_observer, player::Player,
tb_entities::Water,
};
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::prelude::*; use bevy::prelude::*;