diff --git a/src/fighter/attack.rs b/src/fighter/attack.rs index 3acc165..de2fce5 100644 --- a/src/fighter/attack.rs +++ b/src/fighter/attack.rs @@ -1,9 +1,13 @@ -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use bevy::{asset::Handle, ecs::component::Component, reflect::Reflect}; use serde::{Deserialize, Serialize}; -use crate::{fighter::animation::AnimKind, math::{int::FGi32, vec3::FGVec3}, scripting::FighterAttackScript}; +use crate::{ + fighter::{FighterAttributes, animation::AnimKind}, + math::{int::FGi32, vec3::FGVec3}, + scripting::FighterAttackScript, +}; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct FighterDamage(FGi32); @@ -16,7 +20,7 @@ impl FighterDamage { #[derive(Clone, Debug, Serialize, Deserialize)] pub enum KnockbackType { Normal, - Fixed + Fixed, } impl KnockbackType { @@ -41,7 +45,7 @@ impl Knockback { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum AttackAngle { Normal(FGi32), - Sakurai + Sakurai, } impl Default for AttackAngle { @@ -51,7 +55,6 @@ impl Default for AttackAngle { } impl AttackAngle { - pub fn make_normal(angle: FGi32) -> Self { Self::Normal(angle) } @@ -91,16 +94,77 @@ pub struct AttackHitbox { pub knockback: Knockback, pub knockback_growth: FGi32, pub start_frame: u32, - pub end_frame: u32 + pub end_frame: u32, +} + +pub struct StaleMoveQueue(pub VecDeque); + +impl AttackHitbox { + /// Knockback calculation + pub fn calculate_knockback( + &self, + attack: AttackKind, + + receiver_attribs: &FighterAttributes, + receiver_damage: FighterDamage, + total_damage_received_this_frame: FighterDamage, + interrupted_smash_charge: bool, + receiver_crouching: bool, + ) -> Knockback { + /// Weight that produces a weight modifier of exactly 1.0. Weight-independent + /// attacks pass this value in place of the target's real weight. + const BASELINE_WEIGHT: FGi32 = FGi32::lit("100.0"); + + /// Applied to the weight-scaled damage term. + const KB_GAIN: FGi32 = FGi32::lit("1.4"); + + /// Constant floor added to the damage term before knockback growth scales it. + /// At 0% and 0 damage, knockback reduces to KB_OFFSET * s + b. + const KB_OFFSET: FGi32 = FGi32::lit("18.0"); + + /// Knockback growth is authored in percent (110 means 1.1x). + const KB_GROWTH_SCALE: FGi32 = FGi32::lit("0.01"); + const DAMAGE_TERM_DIVISOR: FGi32 = FGi32::lit("20.0"); + const DAMAGE_BONUS: FGi32 = FGi32::lit("2.0"); // effective damage floor in the damage term + const CHARGE_SMASH_INTERRUPTION_MODIFIER: FGi32 = FGi32::lit("1.2"); + const CROUCH_CANCEL_MODIFIER: FGi32 = FGi32::lit("0.666667"); + + let attack_damage_unstalled = self.damage.0; + + let modifier = { + let mut out = FGi32::ONE; + if interrupted_smash_charge { + out *= CHARGE_SMASH_INTERRUPTION_MODIFIER + } else if receiver_crouching { + out *= CROUCH_CANCEL_MODIFIER + } + out + }; + + let s = KB_GROWTH_SCALE * self.knockback_growth; + let p = receiver_damage.0.floor() + total_damage_received_this_frame.0; + let d = attack_damage_unstalled; + let damage_term = p / DAMAGE_TERM_DIVISOR * (d + DAMAGE_BONUS); + + let scaled = damage_term * (FGi32::lit("2.0") * BASELINE_WEIGHT) + / (receiver_attribs.weight + BASELINE_WEIGHT); + let kb = (scaled * KB_GAIN + KB_OFFSET) * s + self.knockback.0; + + Knockback(modifier * kb) + } } pub fn update_active_hitbox_list(script: &FighterAttackScript, list: &mut Vec, frame: u32) { - *list = script.hitboxes.iter().enumerate().filter(|(_, hb)| { - hb.start_frame <= frame && hb.end_frame > frame - }).map(|(i, _)| i).collect() + *list = script + .hitboxes + .iter() + .enumerate() + .filter(|(_, hb)| hb.start_frame <= frame && hb.end_frame > frame) + .map(|(i, _)| i) + .collect() } #[derive(Component)] pub struct FighterAttackScriptAssets { - pub scripts: HashMap> -} \ No newline at end of file + pub scripts: HashMap>, +} diff --git a/src/fighter/attributes.rs b/src/fighter/attributes.rs index e1f5576..a1062b3 100644 --- a/src/fighter/attributes.rs +++ b/src/fighter/attributes.rs @@ -36,6 +36,7 @@ pub struct FighterAttributes { pub ground_friction: FGi32, pub terminal_velocity: FGi32, pub gravity: FGi32, + pub weight: FGi32, pub air_friction: FGi32, pub air_dodge_duration: u32, diff --git a/src/fighter/baked_animation.rs b/src/fighter/baked_animation.rs index 7d2b91b..1dd79ff 100644 --- a/src/fighter/baked_animation.rs +++ b/src/fighter/baked_animation.rs @@ -102,6 +102,10 @@ impl FixedMat4 { self.cols[3][2] = translation.z; } + pub fn get_translation(&self) -> FGVec3 { + FGVec3::new(self.cols[3][0], self.cols[3][1], self.cols[3][2]) + } + pub fn rotate_y(self, quarter_turns: u8) -> Self { let rotation = match quarter_turns % 4 { 0 => Self { @@ -187,10 +191,25 @@ impl BakedFighterAnimations { pub struct FighterBoneMatrices { bone_names: Vec, matrices: Vec, + previous_frame_matrices: Vec, } impl FighterBoneMatrices { - pub fn get(&self, bone: &str) -> Option { + pub fn get(&self, bone: &str) -> Option<(FixedMat4, FixedMat4)> { + let index = self.bone_names.iter().position(|name| name == bone)?; + let curr = self.matrices.get(index).copied(); + let prev = self.previous_frame_matrices.get(index).copied(); + + if let Some(prev) = prev { + if let Some(curr) = curr { + return Some((prev, curr)); + } + } + + None + } + + pub fn get_current(&self, bone: &str) -> Option { let index = self.bone_names.iter().position(|name| name == bone)?; self.matrices.get(index).copied() } @@ -213,6 +232,7 @@ impl FighterBoneMatrices { } else { assert_eq!(self.bone_names, bone_names, "baked bone names changed"); } + std::mem::swap(&mut self.matrices, &mut self.previous_frame_matrices); self.matrices.clear(); self.matrices.extend_from_slice(matrices); } @@ -707,7 +727,7 @@ mod tests { let mut matrices = FighterBoneMatrices::default(); matrices.set_pose(&["root".into(), "hand".into()], &[first, second]); - assert_eq!(matrices.get("hand"), Some(second)); + assert_eq!(matrices.get_current("hand"), Some(second)); assert_eq!( matrices.iter().collect::>(), vec![("root", first), ("hand", second)] diff --git a/src/fighter/debug.rs b/src/fighter/debug.rs index 05f3d38..7fd57b9 100644 --- a/src/fighter/debug.rs +++ b/src/fighter/debug.rs @@ -1,6 +1,7 @@ //! Fighter debug views: gizmos and the egui inspector window. use bevy::color::palettes::css::ORANGE; +use bevy::gizmos::primitives::dim3::Capsule3dBuilder; use bevy::{color::palettes::css::YELLOW, prelude::*}; use bevy_egui::{EguiContexts, egui}; @@ -178,22 +179,35 @@ pub fn attack_debug( let radius = script.hitboxes[*i].radius; let bone = &script.hitboxes[*i].bone; - let trf = matrices.get(bone); - if let Some(trf) = trf { - let trf = player_trf.mul(trf); - let start = player_trf.transform_point(FGVec3::lit("0.0", "1.0", "0.0")); - let end = player_trf.transform_point(FGVec3::lit("0.0", "1.0", "1.0")); - let pos = trf.transform_point(offset); - let pos_draw = Vec3::new(pos.x.to_num(), pos.y.to_num(), pos.z.to_num()); - //Primitive - //gizmos.primitive_3d(, isometry, color) - gizmos.sphere(pos_draw, radius.to_num(), bevy::color::palettes::css::RED); + let trfs = matrices.get(bone); + if let Some((prev_trf, curr_trf)) = trfs { + let prev_trf = player_trf.mul(prev_trf); + let curr_trf = player_trf.mul(curr_trf); + + let prev_pos = prev_trf.transform_point(offset); + let curr_pos = curr_trf.transform_point(offset); + + let prev_pos_draw = Vec3::new(prev_pos.x.to_num(), prev_pos.y.to_num(), prev_pos.z.to_num()); + let curr_pos_draw = Vec3::new(curr_pos.x.to_num(), curr_pos.y.to_num(), curr_pos.z.to_num()); + + let dist = Dir3::new_and_length(curr_pos_draw - prev_pos_draw); + + if let Ok((aim_dir, dist)) = dist { + let attack_capsule = Capsule3d { + half_length: dist * 0.5, + radius: radius.to_num() + }; + let mut capsule_draw_trf = Transform::IDENTITY; + capsule_draw_trf.align(Dir3::Y, aim_dir, Dir3::X, aim_dir.any_orthonormal_vector()); + capsule_draw_trf.translation = (prev_pos_draw + curr_pos_draw) * 0.5; + gizmos.primitive_3d(&attack_capsule, capsule_draw_trf.to_isometry(), bevy::color::palettes::css::RED); + } + + + + gizmos.sphere(prev_pos_draw, radius.to_num(), bevy::color::palettes::css::RED.darker(0.5)); + gizmos.sphere(curr_pos_draw, radius.to_num(), bevy::color::palettes::css::RED); - gizmos.arrow( - start.to_vec3(), - end.to_vec3(), - bevy::color::palettes::css::RED, - ); } else { warn!("Bone not found required by attack script: {}", bone); } diff --git a/src/fighter/state/ground_attack.rs b/src/fighter/state/ground_attack.rs index 51a703e..fab9412 100644 --- a/src/fighter/state/ground_attack.rs +++ b/src/fighter/state/ground_attack.rs @@ -1,4 +1,6 @@ -use crate::fighter::{attack::{AttackKind, update_active_hitbox_list}, state::{FighterState, FighterStateImpl, ground::GroundedStateCommon, wait::WaitState}}; +use fixed::types::I16F16; + +use crate::{fighter::{attack::{AttackKind, update_active_hitbox_list}, state::{FighterState, FighterStateImpl, ground::GroundedStateCommon, wait::WaitState}}, math::int::FGi32}; #[derive(Clone, Debug, Hash)] pub struct GroundAttackState { @@ -47,8 +49,15 @@ impl FighterStateImpl for GroundAttackState { pub fn check_interrupt(state_context: &super::FighterStateContext, grounded_common: &GroundedStateCommon) -> Option { if state_context.input.has_command(crate::input::FighterCommands::Attack) { + + let attack_kind = if state_context.input.get_last_frame().movement.y > FGi32::ZERO { + AttackKind::UpTilt + } else { + AttackKind::Jab + }; + Some(FighterState::GroundAttack(GroundAttackState { - attack_kind: AttackKind::Jab, + attack_kind, grounded_common: grounded_common.clone() })) } else { diff --git a/src/scripting/mod.rs b/src/scripting/mod.rs index dffda8e..3cef616 100644 --- a/src/scripting/mod.rs +++ b/src/scripting/mod.rs @@ -13,5 +13,7 @@ pub enum MoveAngleKind { #[derive(Asset, Reflect, Serialize, Deserialize)] pub struct FighterAttackScript { - pub hitboxes: Vec + pub hitboxes: Vec, + pub iasa_frame: u32, + pub wont_autocancel_window: (u32, u32), } \ No newline at end of file diff --git a/src/scripting/move_compiler.rs b/src/scripting/move_compiler.rs index d113ef4..f70ae47 100644 --- a/src/scripting/move_compiler.rs +++ b/src/scripting/move_compiler.rs @@ -10,6 +10,8 @@ use crate::{ struct MoveCompiler { hitboxes: HashMap, active_hitboxes: Vec, + wont_autocancel_window: Option<(u32, u32)>, + iasa_frame: Option, cursor: u32, } @@ -59,11 +61,29 @@ impl MoveCompiler { Ok(()) } + fn set_wont_autocancel_window(&mut self, start: u32, end: u32) -> Result<(), Box> { + if end < start { + return Err(format!("Couldn't set won't autocancel window, end frame {} is smaller than start frame {}", end, start).into()); + } + + self.wont_autocancel_window = Some((start, end)); + + Ok(()) + } + + fn set_iasa_frame(&mut self, iasa_frame: u32) { + self.iasa_frame = Some(iasa_frame); + } + fn into_attack_script(mut self) -> Result { self.remove_all_hitboxes().map_err(|err| err.to_string())?; let hitboxes = self.hitboxes.into_iter().map(|(_, hitbox)| hitbox).collect(); + let iasa_frame = self.iasa_frame.unwrap_or(100000); + let wont_autocancel_window = self.wont_autocancel_window.unwrap_or((0, 100000)); Ok(FighterAttackScript { - hitboxes + hitboxes, + iasa_frame, + wont_autocancel_window }) } } @@ -133,6 +153,28 @@ pub fn compile_script(text: &str) -> Result { ); } + { + let mc = mc.clone(); + engine.register_fn( + "set_wont_autocancel_window", + move |start: i64, end: i64| -> Result<(), Box> { + let mut b = mc.borrow_mut(); + b.set_wont_autocancel_window(start as u32, end as u32) + }, + ); + } + + { + let mc = mc.clone(); + engine.register_fn( + "set_iasa_frame", + move |frame: i64| { + let mut b = mc.borrow_mut(); + b.set_iasa_frame(frame as u32); + }, + ); + } + { let mc = mc.clone(); engine.register_fn(