Initial work on attack and hitbox interpolation
This commit is contained in:
+74
-10
@@ -1,9 +1,13 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
|
||||||
use bevy::{asset::Handle, ecs::component::Component, reflect::Reflect};
|
use bevy::{asset::Handle, ecs::component::Component, reflect::Reflect};
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
pub struct FighterDamage(FGi32);
|
pub struct FighterDamage(FGi32);
|
||||||
|
|
||||||
@@ -16,7 +20,7 @@ impl FighterDamage {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum KnockbackType {
|
pub enum KnockbackType {
|
||||||
Normal,
|
Normal,
|
||||||
Fixed
|
Fixed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KnockbackType {
|
impl KnockbackType {
|
||||||
@@ -41,7 +45,7 @@ impl Knockback {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum AttackAngle {
|
pub enum AttackAngle {
|
||||||
Normal(FGi32),
|
Normal(FGi32),
|
||||||
Sakurai
|
Sakurai,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AttackAngle {
|
impl Default for AttackAngle {
|
||||||
@@ -51,7 +55,6 @@ impl Default for AttackAngle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AttackAngle {
|
impl AttackAngle {
|
||||||
|
|
||||||
pub fn make_normal(angle: FGi32) -> Self {
|
pub fn make_normal(angle: FGi32) -> Self {
|
||||||
Self::Normal(angle)
|
Self::Normal(angle)
|
||||||
}
|
}
|
||||||
@@ -91,16 +94,77 @@ pub struct AttackHitbox {
|
|||||||
pub knockback: Knockback,
|
pub knockback: Knockback,
|
||||||
pub knockback_growth: FGi32,
|
pub knockback_growth: FGi32,
|
||||||
pub start_frame: u32,
|
pub start_frame: u32,
|
||||||
pub end_frame: u32
|
pub end_frame: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StaleMoveQueue(pub VecDeque<AttackKind>);
|
||||||
|
|
||||||
|
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<usize>, frame: u32) {
|
pub fn update_active_hitbox_list(script: &FighterAttackScript, list: &mut Vec<usize>, frame: u32) {
|
||||||
*list = script.hitboxes.iter().enumerate().filter(|(_, hb)| {
|
*list = script
|
||||||
hb.start_frame <= frame && hb.end_frame > frame
|
.hitboxes
|
||||||
}).map(|(i, _)| i).collect()
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, hb)| hb.start_frame <= frame && hb.end_frame > frame)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct FighterAttackScriptAssets {
|
pub struct FighterAttackScriptAssets {
|
||||||
pub scripts: HashMap<AttackKind, Handle<FighterAttackScript>>
|
pub scripts: HashMap<AttackKind, Handle<FighterAttackScript>>,
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,7 @@ pub struct FighterAttributes {
|
|||||||
pub ground_friction: FGi32,
|
pub ground_friction: FGi32,
|
||||||
pub terminal_velocity: FGi32,
|
pub terminal_velocity: FGi32,
|
||||||
pub gravity: FGi32,
|
pub gravity: FGi32,
|
||||||
|
pub weight: FGi32,
|
||||||
pub air_friction: FGi32,
|
pub air_friction: FGi32,
|
||||||
|
|
||||||
pub air_dodge_duration: u32,
|
pub air_dodge_duration: u32,
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ impl FixedMat4 {
|
|||||||
self.cols[3][2] = translation.z;
|
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 {
|
pub fn rotate_y(self, quarter_turns: u8) -> Self {
|
||||||
let rotation = match quarter_turns % 4 {
|
let rotation = match quarter_turns % 4 {
|
||||||
0 => Self {
|
0 => Self {
|
||||||
@@ -187,10 +191,25 @@ impl BakedFighterAnimations {
|
|||||||
pub struct FighterBoneMatrices {
|
pub struct FighterBoneMatrices {
|
||||||
bone_names: Vec<String>,
|
bone_names: Vec<String>,
|
||||||
matrices: Vec<FixedMat4>,
|
matrices: Vec<FixedMat4>,
|
||||||
|
previous_frame_matrices: Vec<FixedMat4>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FighterBoneMatrices {
|
impl FighterBoneMatrices {
|
||||||
pub fn get(&self, bone: &str) -> Option<FixedMat4> {
|
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<FixedMat4> {
|
||||||
let index = self.bone_names.iter().position(|name| name == bone)?;
|
let index = self.bone_names.iter().position(|name| name == bone)?;
|
||||||
self.matrices.get(index).copied()
|
self.matrices.get(index).copied()
|
||||||
}
|
}
|
||||||
@@ -213,6 +232,7 @@ impl FighterBoneMatrices {
|
|||||||
} else {
|
} else {
|
||||||
assert_eq!(self.bone_names, bone_names, "baked bone names changed");
|
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.clear();
|
||||||
self.matrices.extend_from_slice(matrices);
|
self.matrices.extend_from_slice(matrices);
|
||||||
}
|
}
|
||||||
@@ -707,7 +727,7 @@ mod tests {
|
|||||||
let mut matrices = FighterBoneMatrices::default();
|
let mut matrices = FighterBoneMatrices::default();
|
||||||
matrices.set_pose(&["root".into(), "hand".into()], &[first, second]);
|
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!(
|
assert_eq!(
|
||||||
matrices.iter().collect::<Vec<_>>(),
|
matrices.iter().collect::<Vec<_>>(),
|
||||||
vec![("root", first), ("hand", second)]
|
vec![("root", first), ("hand", second)]
|
||||||
|
|||||||
+29
-15
@@ -1,6 +1,7 @@
|
|||||||
//! Fighter debug views: gizmos and the egui inspector window.
|
//! Fighter debug views: gizmos and the egui inspector window.
|
||||||
|
|
||||||
use bevy::color::palettes::css::ORANGE;
|
use bevy::color::palettes::css::ORANGE;
|
||||||
|
use bevy::gizmos::primitives::dim3::Capsule3dBuilder;
|
||||||
use bevy::{color::palettes::css::YELLOW, prelude::*};
|
use bevy::{color::palettes::css::YELLOW, prelude::*};
|
||||||
use bevy_egui::{EguiContexts, egui};
|
use bevy_egui::{EguiContexts, egui};
|
||||||
|
|
||||||
@@ -178,22 +179,35 @@ pub fn attack_debug(
|
|||||||
let radius = script.hitboxes[*i].radius;
|
let radius = script.hitboxes[*i].radius;
|
||||||
let bone = &script.hitboxes[*i].bone;
|
let bone = &script.hitboxes[*i].bone;
|
||||||
|
|
||||||
let trf = matrices.get(bone);
|
let trfs = matrices.get(bone);
|
||||||
if let Some(trf) = trf {
|
if let Some((prev_trf, curr_trf)) = trfs {
|
||||||
let trf = player_trf.mul(trf);
|
let prev_trf = player_trf.mul(prev_trf);
|
||||||
let start = player_trf.transform_point(FGVec3::lit("0.0", "1.0", "0.0"));
|
let curr_trf = player_trf.mul(curr_trf);
|
||||||
let end = player_trf.transform_point(FGVec3::lit("0.0", "1.0", "1.0"));
|
|
||||||
let pos = trf.transform_point(offset);
|
let prev_pos = prev_trf.transform_point(offset);
|
||||||
let pos_draw = Vec3::new(pos.x.to_num(), pos.y.to_num(), pos.z.to_num());
|
let curr_pos = curr_trf.transform_point(offset);
|
||||||
//Primitive
|
|
||||||
//gizmos.primitive_3d(, isometry, color)
|
let prev_pos_draw = Vec3::new(prev_pos.x.to_num(), prev_pos.y.to_num(), prev_pos.z.to_num());
|
||||||
gizmos.sphere(pos_draw, radius.to_num(), bevy::color::palettes::css::RED);
|
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 {
|
} else {
|
||||||
warn!("Bone not found required by attack script: {}", bone);
|
warn!("Bone not found required by attack script: {}", bone);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
#[derive(Clone, Debug, Hash)]
|
||||||
pub struct GroundAttackState {
|
pub struct GroundAttackState {
|
||||||
@@ -47,8 +49,15 @@ impl FighterStateImpl for GroundAttackState {
|
|||||||
|
|
||||||
pub fn check_interrupt(state_context: &super::FighterStateContext, grounded_common: &GroundedStateCommon) -> Option<FighterState> {
|
pub fn check_interrupt(state_context: &super::FighterStateContext, grounded_common: &GroundedStateCommon) -> Option<FighterState> {
|
||||||
if state_context.input.has_command(crate::input::FighterCommands::Attack) {
|
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 {
|
Some(FighterState::GroundAttack(GroundAttackState {
|
||||||
attack_kind: AttackKind::Jab,
|
attack_kind,
|
||||||
grounded_common: grounded_common.clone()
|
grounded_common: grounded_common.clone()
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,5 +13,7 @@ pub enum MoveAngleKind {
|
|||||||
|
|
||||||
#[derive(Asset, Reflect, Serialize, Deserialize)]
|
#[derive(Asset, Reflect, Serialize, Deserialize)]
|
||||||
pub struct FighterAttackScript {
|
pub struct FighterAttackScript {
|
||||||
pub hitboxes: Vec<AttackHitbox>
|
pub hitboxes: Vec<AttackHitbox>,
|
||||||
|
pub iasa_frame: u32,
|
||||||
|
pub wont_autocancel_window: (u32, u32),
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,8 @@ use crate::{
|
|||||||
struct MoveCompiler {
|
struct MoveCompiler {
|
||||||
hitboxes: HashMap<u32, attack::AttackHitbox>,
|
hitboxes: HashMap<u32, attack::AttackHitbox>,
|
||||||
active_hitboxes: Vec<u32>,
|
active_hitboxes: Vec<u32>,
|
||||||
|
wont_autocancel_window: Option<(u32, u32)>,
|
||||||
|
iasa_frame: Option<u32>,
|
||||||
cursor: u32,
|
cursor: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +61,29 @@ impl MoveCompiler {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_wont_autocancel_window(&mut self, start: u32, end: u32) -> Result<(), Box<EvalAltResult>> {
|
||||||
|
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<FighterAttackScript, String> {
|
fn into_attack_script(mut self) -> Result<FighterAttackScript, String> {
|
||||||
self.remove_all_hitboxes().map_err(|err| err.to_string())?;
|
self.remove_all_hitboxes().map_err(|err| err.to_string())?;
|
||||||
let hitboxes = self.hitboxes.into_iter().map(|(_, hitbox)| hitbox).collect();
|
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 {
|
Ok(FighterAttackScript {
|
||||||
hitboxes
|
hitboxes,
|
||||||
|
iasa_frame,
|
||||||
|
wont_autocancel_window
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +153,28 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mc = mc.clone();
|
||||||
|
engine.register_fn(
|
||||||
|
"set_wont_autocancel_window",
|
||||||
|
move |start: i64, end: i64| -> Result<(), Box<EvalAltResult>> {
|
||||||
|
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();
|
let mc = mc.clone();
|
||||||
engine.register_fn(
|
engine.register_fn(
|
||||||
|
|||||||
Reference in New Issue
Block a user