diff --git a/assets/beach/beach.level.yaml b/assets/beach/beach.level.yaml index 6de48a45..32a8f2ec 100644 --- a/assets/beach/beach.level.yaml +++ b/assets/beach/beach.level.yaml @@ -69,10 +69,20 @@ enemies: location: [450, 20, 0] trip_point_x: 300 - fighter: /fighters/big_bass/big_bass.fighter.yaml - location: [1000, 20, 0] + location: [600, 20, 0] trip_point_x: 700 boss: true + - fighter: &brute /fighters/brute/brute.fighter.yaml + location: [400, -30, 0] + trip_point_x: -1 + - fighter: *brute + location: [450, 20, 0] + trip_point_x: 300 + - fighter: *brute + location: [1000, 20, 0] + trip_point_x: 700 + stop_points: [500, 1000] items: diff --git a/assets/fighters/bandit/bandit.fighter.yaml b/assets/fighters/bandit/bandit.fighter.yaml index e0031743..651474be 100644 --- a/assets/fighters/bandit/bandit.fighter.yaml +++ b/assets/fighters/bandit/bandit.fighter.yaml @@ -36,6 +36,15 @@ spritesheet: attacking: frames: [14, 17] +attack: + name: "punch" + frames: + startup: 1 + active: 2 + recovery: 3 + hitbox: [12, 12] + hitbox_offset: [24, 0] + audio: effects: attacking: diff --git a/assets/fighters/big_bass/big_bass.fighter.yaml b/assets/fighters/big_bass/big_bass.fighter.yaml index 5a4a011b..60ccc428 100644 --- a/assets/fighters/big_bass/big_bass.fighter.yaml +++ b/assets/fighters/big_bass/big_bass.fighter.yaml @@ -36,6 +36,15 @@ spritesheet: attacking: frames: [30, 44] +attack: + name: "ground_slam" + frames: + startup: 5 + active: 9 + recovery: 14 + hitbox: [48, 48] + hitbox_offset: [0, 0] + audio: effects: attacking: diff --git a/assets/fighters/brute/brute.fighter.yaml b/assets/fighters/brute/brute.fighter.yaml index e9905051..7fe00f0b 100644 --- a/assets/fighters/brute/brute.fighter.yaml +++ b/assets/fighters/brute/brute.fighter.yaml @@ -36,5 +36,14 @@ spritesheet: attacking: frames: [16, 23] +attack: + name: "punch" + frames: + startup: 2 + active: 3 + recovery: 7 + hitbox: [12, 12] + hitbox_offset: [24, 0] + audio: effects: {} diff --git a/assets/fighters/fishy/fishy.fighter.yaml b/assets/fighters/fishy/fishy.fighter.yaml index b351ebde..916f0911 100644 --- a/assets/fighters/fishy/fishy.fighter.yaml +++ b/assets/fighters/fishy/fishy.fighter.yaml @@ -30,9 +30,19 @@ spritesheet: frames: [71, 76] dying: frames: [71, 76] + #spritesheet does not contain unique attack animation attacking: frames: [85, 90] +attack: + name: "punch" + frames: + startup: 1 + active: 2 + recovery: 4 + hitbox: [16, 16] + hitbox_offset: [24, 0] + audio: effects: attacking: diff --git a/assets/fighters/sharky/sharky.fighter.yaml b/assets/fighters/sharky/sharky.fighter.yaml index e3ccb046..63cbc7bf 100644 --- a/assets/fighters/sharky/sharky.fighter.yaml +++ b/assets/fighters/sharky/sharky.fighter.yaml @@ -30,9 +30,19 @@ spritesheet: frames: [71, 76] dying: frames: [71, 76] + #spritesheet does not contain unique attack animation attacking: frames: [85, 90] +attack: + name: "punch" + frames: + startup: 1 + active: 2 + recovery: 4 + hitbox: [16, 16] + hitbox_offset: [24, 0] + audio: effects: attacking: diff --git a/assets/fighters/slinger/slinger.fighter.yaml b/assets/fighters/slinger/slinger.fighter.yaml index ee233631..9a3381b0 100644 --- a/assets/fighters/slinger/slinger.fighter.yaml +++ b/assets/fighters/slinger/slinger.fighter.yaml @@ -35,6 +35,15 @@ spritesheet: repeat: false attacking: frames: [14, 18] +# this should be changed to a ranged attack +attack: + name: "punch" + frames: + startup: 1 + active: 2 + recovery: 3 + hitbox: [12, 12] + hitbox_offset: [24, 0] audio: effects: {} diff --git a/src/attack.rs b/src/attack.rs index 9ff15697..b51b7795 100644 --- a/src/attack.rs +++ b/src/attack.rs @@ -2,10 +2,12 @@ use bevy::{hierarchy::DespawnRecursiveExt, math::Vec2, prelude::*, reflect::Refl use bevy_rapier2d::prelude::*; use iyes_loopless::prelude::*; +use serde::Deserialize; + use crate::{ animation::Animation, - consts::{ATTACK_HEIGHT, ATTACK_WIDTH}, damage::{DamageEvent, Damageable, Health}, + metadata::FighterMeta, GameState, }; @@ -42,28 +44,31 @@ pub struct Attack { /// /// Must be added to an entity that is a child of an entity with an [`Animation`] and an [`Attack`] /// and will be used to spawn a collider for that attack during the `active` frames. -#[derive(Component)] +/// Each field is an index refering to an animation frame +#[derive(Component, Debug, Clone, Copy, Deserialize)] pub struct AttackFrames { pub startup: usize, pub active: usize, pub recovery: usize, } -/// Activate collisions for entities with [`AttackFrames`] fn activate_hitbox( attack_query: Query<(Entity, &AttackFrames, &Parent), Without>, - animated_query: Query<&Animation>, + fighter_query: Query<(&Animation, &Handle)>, mut commands: Commands, + fighter_assets: Res>, ) { for (entity, attack_frames, parent) in attack_query.iter() { - if let Ok(animation) = animated_query.get(**parent) { + if let Ok((animation, fighter_meta)) = fighter_query.get(**parent) { if animation.current_frame >= attack_frames.startup && animation.current_frame <= attack_frames.active { - //TODO: insert Collider based on size and transform offset in attack asset - commands - .entity(entity) - .insert(Collider::cuboid(ATTACK_WIDTH * 0.8, ATTACK_HEIGHT * 0.8)); + if let Some(fighter_data) = fighter_assets.get(fighter_meta) { + commands.entity(entity).insert(Collider::cuboid( + fighter_data.attack.hitbox.x, + fighter_data.attack.hitbox.y, + )); + } } } } diff --git a/src/consts.rs b/src/consts.rs index 06e0531a..a0038444 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -22,8 +22,7 @@ pub const CAMERA_SPEED: f32 = 0.8; pub const MAX_Y: f32 = (GROUND_HEIGHT / 2.) + GROUND_Y; pub const MIN_Y: f32 = -(GROUND_HEIGHT / 2.) + GROUND_Y; -pub const ATTACK_WIDTH: f32 = 16.; -pub const ATTACK_HEIGHT: f32 = 16.; +//TODO: remove in favor of loading attack velocity from YAML pub const ATTACK_VELOCITY: f32 = 250.0; pub const ITEM_LAYER: f32 = 100.; diff --git a/src/enemy_ai.rs b/src/enemy_ai.rs index a799a5d0..dfd2e09d 100644 --- a/src/enemy_ai.rs +++ b/src/enemy_ai.rs @@ -7,7 +7,7 @@ use crate::{ animation::Facing, consts::{self, ENEMY_MAX_ATTACK_DISTANCE, ENEMY_MIN_ATTACK_DISTANCE, ENEMY_TARGET_MAX_OFFSET}, enemy::{Enemy, TripPointX}, - fighter_state::{Attacking, Idling, Moving, StateTransition, StateTransitionIntents}, + fighter_state::{Idling, Moving, Punching, StateTransition, StateTransitionIntents}, player::Player, Stats, }; @@ -135,8 +135,8 @@ pub fn emit_enemy_intents( // And attack! intents.push_back(StateTransition::new( - Attacking::default(), - Attacking::PRIORITY, + Punching::default(), + Punching::PRIORITY, false, )); diff --git a/src/fighter_state.rs b/src/fighter_state.rs index a055165d..150d4891 100644 --- a/src/fighter_state.rs +++ b/src/fighter_state.rs @@ -55,7 +55,9 @@ impl Plugin for FighterStatePlugin { .after(FighterStateCollectSystems) .run_in_state(GameState::InGame) .with_system(transition_from_idle) - .with_system(transition_from_attacking) + .with_system(transition_from_flopping) + .with_system(transition_from_punching) + .with_system(transition_from_ground_slam) .with_system(transition_from_knocked_back) .into(), ) @@ -65,8 +67,9 @@ impl Plugin for FighterStatePlugin { ConditionSet::new() .run_in_state(GameState::InGame) .with_system(idling) - .with_system(attacking) - .with_system(boss_attacking) + .with_system(flopping) + .with_system(punching) + .with_system(ground_slam) .with_system(moving) .with_system(throwing) .with_system(grabbing) @@ -211,13 +214,41 @@ impl Grabbing { /// Component indicating the player is flopping #[derive(Component, Reflect, Default, Debug)] #[component(storage = "SparseSet")] -pub struct Attacking { +pub struct Flopping { /// The initial y-height of the figther when starting the attack pub start_y: f32, pub has_started: bool, pub is_finished: bool, } -impl Attacking { +impl Flopping { + pub const PRIORITY: i32 = 30; + //TODO: return to change assets and this to "flopping" + pub const ANIMATION: &'static str = "attacking"; +} + +/// Component indicating the player is punching +#[derive(Component, Reflect, Default, Debug)] +#[component(storage = "SparseSet")] +pub struct GroundSlam { + /// The initial y-height of the figther when starting the attack + pub start_y: f32, + pub has_started: bool, + pub is_finished: bool, +} +impl GroundSlam { + pub const PRIORITY: i32 = 30; + //TODO: return to change assets and this to "flopping" + pub const ANIMATION: &'static str = "attacking"; +} + +#[derive(Component, Reflect, Default, Debug)] +#[component(storage = "SparseSet")] + +pub struct Punching { + pub has_started: bool, + pub is_finished: bool, +} +impl Punching { pub const PRIORITY: i32 = 30; pub const ANIMATION: &'static str = "attacking"; } @@ -262,14 +293,14 @@ fn collect_player_actions( ) { for (action_state, mut transition_intents, inventory, stats) in &mut players { // Trigger attacks + //TODO: can use flop attack again after input buffer/chaining if action_state.just_pressed(PlayerAction::Attack) { transition_intents.push_back(StateTransition::new( - Attacking::default(), - Attacking::PRIORITY, + Punching::default(), + Punching::PRIORITY, false, )); } - // Trigger grab/throw if action_state.just_pressed(PlayerAction::Throw) { if inventory.is_some() { @@ -318,6 +349,7 @@ fn collect_attack_knockbacks( // Trigger knock back transition_intents.push_back(StateTransition::new( KnockedBack { + //Knockback velocity feels strange right now velocity: event.damage_velocity, timer: Timer::from_seconds(0.18, false), }, @@ -361,16 +393,16 @@ fn transition_from_idle( } // Initiate any transitions from the flopping state -fn transition_from_attacking( +fn transition_from_flopping( mut commands: Commands, - mut fighters: Query<(Entity, &mut StateTransitionIntents, &Attacking)>, + mut fighters: Query<(Entity, &mut StateTransitionIntents, &Flopping)>, ) { 'entity: for (entity, mut transition_intents, flopping) in &mut fighters { // Transition to any higher priority states let current_state_removed = transition_intents - .transition_to_higher_priority_states::( + .transition_to_higher_priority_states::( entity, - Attacking::PRIORITY, + Flopping::PRIORITY, &mut commands, ); @@ -382,7 +414,62 @@ fn transition_from_attacking( // If we're done flopping if flopping.is_finished { // Go back to idle - commands.entity(entity).remove::().insert(Idling); + commands.entity(entity).remove::().insert(Idling); + } + } +} + +fn transition_from_punching( + mut commands: Commands, + mut fighters: Query<(Entity, &mut StateTransitionIntents, &Punching)>, +) { + 'entity: for (entity, mut transition_intents, punching) in &mut fighters { + // Transition to any higher priority states + let current_state_removed = transition_intents + .transition_to_higher_priority_states::( + entity, + Punching::PRIORITY, + &mut commands, + ); + + // If our current state was removed, don't continue processing this fighter + if current_state_removed { + continue 'entity; + } + + // If we're done attacking + if punching.is_finished { + // Go back to idle + commands.entity(entity).remove::().insert(Idling); + } + } +} + +fn transition_from_ground_slam( + mut commands: Commands, + mut fighters: Query<(Entity, &mut StateTransitionIntents, &Flopping)>, +) { + 'entity: for (entity, mut transition_intents, flopping) in &mut fighters { + // Transition to any higher priority states + let current_state_removed = transition_intents + .transition_to_higher_priority_states::( + entity, + GroundSlam::PRIORITY, + &mut commands, + ); + + // If our current state was removed, don't continue processing this fighter + if current_state_removed { + continue 'entity; + } + + // If we're done flopping + if flopping.is_finished { + // Go back to idle + commands + .entity(entity) + .remove::() + .insert(Idling); } } } @@ -440,23 +527,20 @@ fn idling(mut fighters: Query<(&mut Animation, &mut LinearVelocity), With jumping "punch". In the future there will be different attacks, which will each have their own /// > state system, and we will trigger different attack states for different players and enemies, /// > based on the attacks available to that fighter. -fn attacking( +fn flopping( mut commands: Commands, - mut fighters: Query< - ( - Entity, - &mut Animation, - &mut Transform, - &mut LinearVelocity, - &Facing, - &Stats, - &Handle, - &mut Attacking, - Option<&Player>, - Option<&Enemy>, - ), - Without, - >, + mut fighters: Query<( + Entity, + &mut Animation, + &mut Transform, + &mut LinearVelocity, + &Facing, + &Stats, + &Handle, + &mut Flopping, + Option<&Player>, + Option<&Enemy>, + )>, fighter_assets: Res>, ) { for ( @@ -467,7 +551,7 @@ fn attacking( facing, stats, meta_handle, - mut attacking, + mut flopping, player, enemy, ) in &mut fighters @@ -480,12 +564,12 @@ fn attacking( } // Start the attack - if !attacking.has_started { - attacking.has_started = true; - attacking.start_y = transform.translation.y; + if !flopping.has_started { + flopping.has_started = true; + flopping.start_y = transform.translation.y; // Start the attack from the beginning - animation.play(Attacking::ANIMATION, false); + animation.play(Flopping::ANIMATION, false); // Spawn the attack entity let attack_entity = commands @@ -524,9 +608,9 @@ fn attacking( // Play attack sound effect if let Some(fighter) = fighter_assets.get(meta_handle) { - if let Some(effects) = fighter.audio.effect_handles.get(Attacking::ANIMATION) { + if let Some(effects) = fighter.audio.effect_handles.get(Flopping::ANIMATION) { let fx_playback = AnimationAudioPlayback::new( - Attacking::ANIMATION.to_owned(), + Flopping::ANIMATION.to_owned(), effects.clone(), ); commands.entity(entity).insert(fx_playback); @@ -558,16 +642,113 @@ fn attacking( **velocity = Vec2::ZERO; // Make sure we "land on the ground" ( i.e. the player y position hasn't changed ) - transform.translation.y = attacking.start_y; + transform.translation.y = flopping.start_y; // Set flopping to finished - attacking.is_finished = true; + flopping.is_finished = true; + } + } +} + +fn punching( + mut commands: Commands, + mut fighters: Query<( + Entity, + &mut Animation, + &mut LinearVelocity, + &Facing, + &Stats, + &Handle, + &mut Punching, + Option<&Player>, + Option<&Enemy>, + )>, + fighter_assets: Res>, +) { + for ( + entity, + mut animation, + mut velocity, + facing, + stats, + meta_handle, + mut punching, + player, + enemy, + ) in &mut fighters + { + let is_player = player.is_some(); + let is_enemy = enemy.is_some(); + if !is_player && !is_enemy { + // This system only knows how to attack for players and enemies + continue; + } + + if !punching.has_started { + punching.has_started = true; + + // Start the attack from the beginning + animation.play(Punching::ANIMATION, false); + + if let Some(fighter) = fighter_assets.get(meta_handle) { + let mut offset = fighter.attack.hitbox_offset; + if facing.is_left() { + offset *= -1.0 + } + let attack_frames = fighter.attack.frames; + // Spawn the attack entity + let attack_entity = commands + .spawn_bundle(TransformBundle::from_transform( + Transform::from_translation(offset.extend(0.0)), + )) + .insert(Sensor) + .insert(ActiveEvents::COLLISION_EVENTS) + .insert(ActiveCollisionTypes::default() | ActiveCollisionTypes::STATIC_STATIC) + .insert(CollisionGroups::new( + if is_player { + BodyLayers::PLAYER_ATTACK + } else { + BodyLayers::ENEMY_ATTACK + }, + if is_player { + BodyLayers::ENEMY + } else { + BodyLayers::PLAYER + }, + )) + .insert(Attack { + damage: stats.damage, + velocity: if facing.is_left() { + Vec2::NEG_X + } else { + Vec2::X + } * Vec2::new(consts::ATTACK_VELOCITY, 0.0), + }) + .insert(attack_frames) + .id(); + commands.entity(entity).push_children(&[attack_entity]); + + // Play attack sound effect + if let Some(effects) = fighter.audio.effect_handles.get(Punching::ANIMATION) { + let fx_playback = AnimationAudioPlayback::new( + Punching::ANIMATION.to_owned(), + effects.clone(), + ); + commands.entity(entity).insert(fx_playback); + } + } + } + + **velocity = Vec2::ZERO; + + if animation.is_finished() { + punching.is_finished = true; } } } /// The attacking state used for bosses -fn boss_attacking( +fn ground_slam( mut commands: Commands, mut fighters: Query< ( @@ -578,7 +759,7 @@ fn boss_attacking( &Facing, &Stats, &Handle, - &mut Attacking, + &mut GroundSlam, ), With, >, @@ -592,89 +773,93 @@ fn boss_attacking( facing, stats, meta_handle, - mut attacking, + mut ground_slam, ) in &mut fighters { // Start the attack - if !attacking.has_started { - attacking.has_started = true; - attacking.start_y = transform.translation.y; - - // Start the attack from the beginning - animation.play(Attacking::ANIMATION, false); - - // Spawn the attack entity - let attack_entity = commands - .spawn_bundle(TransformBundle::default()) - .insert(Sensor) - .insert(ActiveEvents::COLLISION_EVENTS) - .insert(ActiveCollisionTypes::default() | ActiveCollisionTypes::STATIC_STATIC) - .insert(CollisionGroups::new( - BodyLayers::ENEMY_ATTACK, - BodyLayers::PLAYER, - )) - .insert(Attack { - damage: stats.damage, - velocity: if facing.is_left() { - Vec2::NEG_X - } else { - Vec2::X - } * Vec2::new(consts::ATTACK_VELOCITY, 0.0), - }) - // TODO: Read from figher metadata - .insert(AttackFrames { - startup: 5, - active: 9, - recovery: 14, - }) - .id(); - commands.entity(entity).push_children(&[attack_entity]); - - // Play attack sound effect - if let Some(fighter) = fighter_assets.get(meta_handle) { - if let Some(effects) = fighter.audio.effect_handles.get(Attacking::ANIMATION) { - let fx_playback = AnimationAudioPlayback::new( - Attacking::ANIMATION.to_owned(), - effects.clone(), - ); - commands.entity(entity).insert(fx_playback); + if let Some(fighter) = fighter_assets.get(meta_handle) { + let mut offset = fighter.attack.hitbox_offset; + if facing.is_left() { + offset *= -1.0 + } + let attack_frames = fighter.attack.frames; + if !ground_slam.has_started { + ground_slam.has_started = true; + ground_slam.start_y = transform.translation.y; + + // Start the attack from the beginning + animation.play(GroundSlam::ANIMATION, false); + + // Spawn the attack entity + let attack_entity = commands + .spawn_bundle(TransformBundle::from_transform( + Transform::from_translation(offset.extend(0.0)), + )) + .insert(Sensor) + .insert(ActiveEvents::COLLISION_EVENTS) + .insert(ActiveCollisionTypes::default() | ActiveCollisionTypes::STATIC_STATIC) + .insert(CollisionGroups::new( + BodyLayers::ENEMY_ATTACK, + BodyLayers::PLAYER, + )) + .insert(Attack { + damage: stats.damage, + velocity: if facing.is_left() { + Vec2::NEG_X + } else { + Vec2::X + } * Vec2::new(consts::ATTACK_VELOCITY, 0.0), + }) + .insert(attack_frames) + .id(); + commands.entity(entity).push_children(&[attack_entity]); + + // Play attack sound effect + if let Some(fighter) = fighter_assets.get(meta_handle) { + if let Some(effects) = fighter.audio.effect_handles.get(GroundSlam::ANIMATION) { + let fx_playback = AnimationAudioPlayback::new( + GroundSlam::ANIMATION.to_owned(), + effects.clone(), + ); + commands.entity(entity).insert(fx_playback); + } } } - } - // Reset velocity - **velocity = Vec2::ZERO; + // Reset velocity + **velocity = Vec2::ZERO; - if !animation.is_finished() { - // Do a forward jump thing - //TODO: Fix hacky way to get a forward jump + if !animation.is_finished() { + // Do a forward jump thing + //TODO: Fix hacky way to get a forward jump - // Control x movement - if animation.current_frame < 3 { - if facing.is_left() { - velocity.x -= 150.0; - } else { - velocity.x += 150.0; + // Control x movement + if animation.current_frame < attack_frames.startup { + if facing.is_left() { + velocity.x -= 150.0; + } else { + velocity.x += 150.0; + } } - } - // Control y movement - if animation.current_frame < 1 { - velocity.y += 270.0; - } else if animation.current_frame < 3 { - velocity.y -= 180.0; - } + // Control y movement + if animation.current_frame < attack_frames.startup { + velocity.y += 270.0; + } else if animation.current_frame < attack_frames.active { + velocity.y -= 180.0; + } - // If the animation is finished - } else { - // Stop moving - **velocity = Vec2::ZERO; + // If the animation is finished + } else { + // Stop moving + **velocity = Vec2::ZERO; - // Make sure we "land on the ground" ( i.e. the player y position hasn't changed ) - transform.translation.y = attacking.start_y; + // Make sure we "land on the ground" ( i.e. the player y position hasn't changed ) + transform.translation.y = ground_slam.start_y; - // Set flopping to finished - attacking.is_finished = true; + // Set flopping to finished + ground_slam.is_finished = true; + } } } } @@ -758,7 +943,7 @@ fn dying( **velocity = Vec2::ZERO; animation.play(Dying::ANIMATION, false); - // When the animatino is finished, despawn the fighter + // When the animation is finished, despawn the fighter } else if animation.is_finished() { commands.entity(entity).despawn_recursive(); } diff --git a/src/metadata.rs b/src/metadata.rs index 63f86867..e2df6af2 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -11,7 +11,7 @@ use bevy_parallax::{LayerData, ParallaxResource}; use punchy_macros::HasLoadProgress; use serde::Deserialize; -use crate::{animation::Clip, assets::EguiFont, fighter::Stats}; +use crate::{animation::Clip, assets::EguiFont, attack::AttackFrames, fighter::Stats}; pub mod settings; pub use settings::*; @@ -91,6 +91,18 @@ pub struct FighterMeta { pub hud: FighterHudMeta, pub spritesheet: FighterSpritesheetMeta, pub audio: AudioMeta, + //Will likely need a hashmap(?) of AttackMetas, fighters will have multiple attacks + pub attack: AttackMeta, +} + +#[derive(TypeUuid, Deserialize, Clone, Debug, Component)] +#[serde(deny_unknown_fields)] +#[uuid = "45a912f4-ea5c-4eba-9ba9-f1a726140f28"] +pub struct AttackMeta { + pub name: String, + pub frames: AttackFrames, + pub hitbox: Vec2, + pub hitbox_offset: Vec2, } #[derive(TypeUuid, Deserialize, Clone, Debug, Component)]