Attack system revamp (#82)
- Add crouch cancelling - Add IASA for aerial attacks - Add scaffolding for autocancel - Add hitbox group system - Add new ergonomic features for attack scripting - Add more information to the hitbox viewer - Improve the looks of debug capsules Reviewed-on: #82 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #82.
This commit is contained in:
+205
-46
@@ -1,6 +1,7 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use rhai::EvalAltResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
fighter::attack::{self, AttackAngle, FighterDamage, Knockback, KnockbackType},
|
||||
@@ -10,13 +11,41 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MoveCompiler {
|
||||
hitboxes: HashMap<u32, attack::AttackHitbox>,
|
||||
active_hitboxes: Vec<u32>,
|
||||
hitboxes: Vec<attack::AttackHitbox>,
|
||||
active_hitboxes: HashMap<u32, usize>,
|
||||
wont_autocancel_window: Option<(u32, u32)>,
|
||||
iasa_frame: Option<u32>,
|
||||
landing_duration: Option<u32>,
|
||||
cursor: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SharedHitboxInfo {
|
||||
damage: FighterDamage,
|
||||
angle: AttackAngle,
|
||||
knockback_type: KnockbackType,
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32,
|
||||
}
|
||||
|
||||
impl SharedHitboxInfo {
|
||||
pub fn make(
|
||||
damage: FighterDamage,
|
||||
angle: AttackAngle,
|
||||
knockback_type: KnockbackType,
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32,
|
||||
) -> Self {
|
||||
Self {
|
||||
damage,
|
||||
angle,
|
||||
knockback_type,
|
||||
knockback,
|
||||
knockback_growth,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rhai_u32(value: i64, parameter: &str) -> Result<u32, Box<EvalAltResult>> {
|
||||
u32::try_from(value).map_err(|_| {
|
||||
format!(
|
||||
@@ -43,28 +72,18 @@ impl MoveCompiler {
|
||||
}
|
||||
|
||||
fn remove_hitbox(&mut self, id: u32) -> Result<(), Box<EvalAltResult>> {
|
||||
let hb = self
|
||||
.hitboxes
|
||||
.get_mut(&id)
|
||||
let hitbox_index = *self
|
||||
.active_hitboxes
|
||||
.get(&id)
|
||||
.ok_or(format!("Can't remove hitbox with {id}: Hitbox not found"))?;
|
||||
|
||||
let active_hbox_position =
|
||||
self.active_hitboxes
|
||||
.iter()
|
||||
.position(|i| *i == id)
|
||||
.ok_or(format!(
|
||||
"Can't remove hitbox with ID {id}: Hitbox was not active"
|
||||
))?;
|
||||
|
||||
hb.end_frame = self.cursor;
|
||||
|
||||
self.active_hitboxes.remove(active_hbox_position);
|
||||
self.hitboxes[hitbox_index].end_frame = self.cursor;
|
||||
self.active_hitboxes.remove(&id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn remove_all_hitboxes(&mut self) -> Result<(), Box<EvalAltResult>> {
|
||||
// We are gonna modify the array, so take a copy...
|
||||
for hitbox_id in self.active_hitboxes.clone() {
|
||||
for hitbox_id in self.active_hitboxes.keys().copied().collect::<Vec<_>>() {
|
||||
self.remove_hitbox(hitbox_id)?;
|
||||
}
|
||||
|
||||
@@ -97,14 +116,14 @@ impl MoveCompiler {
|
||||
|
||||
fn into_attack_script(mut self) -> Result<FighterAttackScript, String> {
|
||||
self.remove_all_hitboxes().map_err(|err| err.to_string())?;
|
||||
let mut hitboxes: Vec<_> = self.hitboxes.into_values().collect();
|
||||
hitboxes.sort_unstable_by_key(|hitbox| hitbox.id);
|
||||
let iasa_frame = self.iasa_frame.unwrap_or(100000);
|
||||
let wont_autocancel_window = self.wont_autocancel_window.unwrap_or((0, 100000));
|
||||
let mut hitboxes = self.hitboxes;
|
||||
hitboxes.sort_unstable_by_key(|hitbox| (hitbox.id, hitbox.start_frame));
|
||||
|
||||
Ok(FighterAttackScript {
|
||||
hitboxes,
|
||||
iasa_frame,
|
||||
wont_autocancel_window,
|
||||
iasa_frame: self.iasa_frame,
|
||||
wont_autocancel_window: self.wont_autocancel_window,
|
||||
landing_duration: self.landing_duration,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -117,7 +136,9 @@ pub fn register_common_types(engine: &mut rhai::Engine) {
|
||||
.register_fn("knockback_normal", KnockbackType::make_normal)
|
||||
.register_fn("knockback_fixed", KnockbackType::make_fixed)
|
||||
.register_type_with_name::<AttackAngle>("Angle")
|
||||
.register_fn("angle", |str: &str| AttackAngle::make_normal(FGi32::lit(str)))
|
||||
.register_fn("angle", |str: &str| {
|
||||
AttackAngle::make_normal(FGi32::lit(str))
|
||||
})
|
||||
.register_fn("angle_sakurai", AttackAngle::make_sakurai)
|
||||
.register_type_with_name::<Knockback>("Knockback")
|
||||
.register_fn("knockback", Knockback::lit)
|
||||
@@ -128,6 +149,17 @@ pub fn register_common_types(engine: &mut rhai::Engine) {
|
||||
.register_type_with_name::<FGVec3>("Vec3")
|
||||
.register_fn("vec3", FGVec3::lit)
|
||||
.register_type_with_name::<FighterDamage>("FighterDamage")
|
||||
.register_type_with_name::<SharedHitboxInfo>("SharedHitboxInfo")
|
||||
.register_fn(
|
||||
"make_hitbox_info",
|
||||
|damage: FighterDamage,
|
||||
angle: AttackAngle,
|
||||
knockback_type: KnockbackType,
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32| {
|
||||
SharedHitboxInfo::make(damage, angle, knockback_type, knockback, knockback_growth)
|
||||
},
|
||||
)
|
||||
.register_fn("damage", FighterDamage::lit);
|
||||
}
|
||||
|
||||
@@ -200,6 +232,36 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let mc = mc.clone();
|
||||
engine.register_fn(
|
||||
"hitbox",
|
||||
move |id: i64,
|
||||
group: i64,
|
||||
bone: &str,
|
||||
offset: FGVec3,
|
||||
radius: FGi32,
|
||||
shared: SharedHitboxInfo|
|
||||
-> Result<(), Box<EvalAltResult>> {
|
||||
add_hitbox(
|
||||
&mc,
|
||||
id,
|
||||
group,
|
||||
bone,
|
||||
shared.damage,
|
||||
offset,
|
||||
radius,
|
||||
shared.angle.clone(),
|
||||
shared.knockback_type,
|
||||
shared.knockback,
|
||||
shared.knockback_growth,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Keep the original script syntax working. Melee's default collision
|
||||
// group is shared by all hitbox slots in an attack.
|
||||
{
|
||||
let mc = mc.clone();
|
||||
engine.register_fn(
|
||||
@@ -214,18 +276,11 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32|
|
||||
-> Result<(), Box<EvalAltResult>> {
|
||||
let mut b = mc.borrow_mut();
|
||||
|
||||
// Rhai does not have first class u32 support, so we have to do this.
|
||||
let id = rhai_u32(id, "hitbox id")?;
|
||||
|
||||
if b.hitboxes.contains_key(&id) {
|
||||
return Err(format!("Hitbox with ID {} already existed!", id).into());
|
||||
}
|
||||
|
||||
let hitbox = attack::AttackHitbox {
|
||||
add_hitbox(
|
||||
&mc,
|
||||
id,
|
||||
bone: bone.to_owned(),
|
||||
0,
|
||||
bone,
|
||||
damage,
|
||||
offset,
|
||||
radius,
|
||||
@@ -233,28 +288,92 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
|
||||
knockback_type,
|
||||
knockback,
|
||||
knockback_growth,
|
||||
start_frame: b.cursor,
|
||||
end_frame: b.cursor,
|
||||
};
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
b.hitboxes.insert(id, hitbox);
|
||||
b.active_hitboxes.push(id);
|
||||
|
||||
Ok(())
|
||||
{
|
||||
let mc = mc.clone();
|
||||
engine.register_fn(
|
||||
"hitbox",
|
||||
move |id: i64,
|
||||
group: i64,
|
||||
bone: &str,
|
||||
damage: FighterDamage,
|
||||
offset: FGVec3,
|
||||
radius: FGi32,
|
||||
angle: AttackAngle,
|
||||
knockback_type: KnockbackType,
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32|
|
||||
-> Result<(), Box<EvalAltResult>> {
|
||||
add_hitbox(
|
||||
&mc,
|
||||
id,
|
||||
group,
|
||||
bone,
|
||||
damage,
|
||||
offset,
|
||||
radius,
|
||||
angle,
|
||||
knockback_type,
|
||||
knockback,
|
||||
knockback_growth,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
engine.run(&text)
|
||||
};
|
||||
|
||||
result
|
||||
let script = result
|
||||
.map(|_| {
|
||||
Rc::into_inner(mc)
|
||||
.unwrap()
|
||||
.into_inner()
|
||||
.into_attack_script()
|
||||
})
|
||||
.map_err(|err| err.to_string())?
|
||||
.map_err(|err| err.to_string())??;
|
||||
script.validate().map(|()| script)
|
||||
}
|
||||
|
||||
fn add_hitbox(
|
||||
mc: &Rc<RefCell<MoveCompiler>>,
|
||||
raw_id: i64,
|
||||
raw_group: i64,
|
||||
bone: &str,
|
||||
damage: FighterDamage,
|
||||
offset: FGVec3,
|
||||
radius: FGi32,
|
||||
angle: AttackAngle,
|
||||
knockback_type: KnockbackType,
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
let mut b = mc.borrow_mut();
|
||||
let id = rhai_u32(raw_id, "hitbox id")?;
|
||||
let group = rhai_u32(raw_group, "hitbox group")?;
|
||||
if b.active_hitboxes.contains_key(&id) {
|
||||
return Err(format!("Hitbox with ID {} is already active!", id).into());
|
||||
}
|
||||
let hitbox_index = b.hitboxes.len();
|
||||
let frame = b.cursor;
|
||||
b.hitboxes.push(attack::AttackHitbox {
|
||||
id,
|
||||
group,
|
||||
bone: bone.to_owned(),
|
||||
damage,
|
||||
offset,
|
||||
radius,
|
||||
angle,
|
||||
knockback_type,
|
||||
knockback,
|
||||
knockback_growth,
|
||||
start_frame: frame,
|
||||
end_frame: frame,
|
||||
});
|
||||
b.active_hitboxes.insert(id, hitbox_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -316,4 +435,44 @@ mod tests {
|
||||
assert_eq!(script.hitboxes[0].start_frame, 1);
|
||||
assert_eq!(script.hitboxes[0].end_frame, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitbox_ids_can_be_reused_after_removal() {
|
||||
let script = compile_script(&format!(
|
||||
"frame(1); {HITBOX} frame(2); remove_hitbox(0); frame(4); {HITBOX} frame(5);"
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(script.hitboxes.len(), 2);
|
||||
assert_eq!(script.hitboxes[0].start_frame, 1);
|
||||
assert_eq!(script.hitboxes[0].end_frame, 2);
|
||||
assert_eq!(script.hitboxes[1].start_frame, 4);
|
||||
assert_eq!(script.hitboxes[1].end_frame, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_hitbox_group_is_compiled() {
|
||||
let script = compile_script(
|
||||
r#"frame(1); hitbox(7, 3, "hand_l", damage("1"), vec3("0", "0", "0"), fg32("1"), angle("0"), knockback_normal(), knockback("1"), fg32("1")); frame(2);"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(script.hitboxes[0].id, 7);
|
||||
assert_eq!(script.hitboxes[0].group, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_hitboxes_share_the_default_collision_group() {
|
||||
let script = compile_script(
|
||||
r#"
|
||||
frame(1);
|
||||
hitbox(0, "hand_l", damage("1"), vec3("0", "0", "0"), fg32("1"), angle("0"), knockback_normal(), knockback("1"), fg32("1"));
|
||||
hitbox(1, "hand_l", damage("1"), vec3("0", "0", "0"), fg32("1"), angle("0"), knockback_normal(), knockback("1"), fg32("1"));
|
||||
frame(2);
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(script.hitboxes.iter().map(|hitbox| hitbox.group).collect::<Vec<_>>(), vec![0, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user