Initial work on attack scripting system

This commit is contained in:
2026-08-29 20:38:49 +02:00
parent 9546df578d
commit 631096a262
10 changed files with 393 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
use crate::math::{int::FGi32, vec3::FGVec3};
pub struct FighterMoveManifest {
script_path: String,
}
#[derive(Debug, Default, Clone)]
pub struct FighterDamage(FGi32);
impl FighterDamage {
pub fn lit(text: &str) -> Self {
Self(FGi32::lit(text))
}
}
#[derive(Clone, Debug)]
pub enum KnockbackType {
Normal,
Fixed
}
impl KnockbackType {
pub fn make_normal() -> Self {
Self::Normal
}
pub fn make_fixed() -> Self {
Self::Fixed
}
}
#[derive(Debug, Default, Clone)]
pub struct Knockback(FGi32);
impl Knockback {
pub fn lit(val: &str) -> Self {
Self(FGi32::lit(val))
}
}
#[derive(Debug, Clone)]
pub enum AttackAngle {
Normal(FGi32),
Sakurai
}
impl Default for AttackAngle {
fn default() -> Self {
AttackAngle::Normal(FGi32::lit("0"))
}
}
impl AttackAngle {
pub fn make_normal(angle: FGi32) -> Self {
Self::Normal(angle)
}
pub fn make_sakurai() -> Self {
Self::Sakurai
}
}
#[derive(Debug)]
pub struct AttackHitbox {
pub id: u32,
pub bone: String,
pub damage: FighterDamage,
pub offset: FGVec3,
pub radius: FGi32,
pub angle: AttackAngle,
pub knockback_type: KnockbackType,
pub knockback: Knockback,
pub knockback_growth: FGi32,
pub start_frame: u32,
pub end_frame: u32
}
+1
View File
@@ -16,6 +16,7 @@ pub mod manifest;
pub mod motion;
pub mod state;
pub mod visual;
pub mod attack;
// The fighter components are re-exported so the rest of the crate can say
// `fighter::FighterVelocity` without caring which file it lives in. Named
+1
View File
@@ -42,6 +42,7 @@ pub mod netcode;
pub mod player;
pub mod schedule;
pub mod stage;
pub mod scripting;
#[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
pub enum AppState {
+1
View File
@@ -1,3 +1,4 @@
pub mod int;
pub mod segment;
pub mod vec;
pub mod vec3;
+9
View File
@@ -239,6 +239,15 @@ impl MulAssign<FGi32> for FGVec2 {
}
}
impl From<(FGi32, FGi32)> for FGVec2 {
fn from(value: (FGi32, FGi32)) -> Self {
Self {
x: value.0,
y: value.1,
}
}
}
#[cfg(test)]
mod is_normalized_tests {
use super::*;
+22
View File
@@ -0,0 +1,22 @@
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use crate::math::int::FGi32;
#[derive(Clone, Copy, PartialEq, Default, Debug, Reflect, Serialize, Deserialize, Hash)]
#[reflect(opaque)]
pub struct FGVec3 {
pub x: FGi32,
pub y: FGi32,
pub z: FGi32,
}
impl FGVec3 {
pub fn lit(x: &str, y: &str, z: &str) -> Self {
Self {
x: FGi32::lit(x),
y: FGi32::lit(y),
z: FGi32::lit(z),
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use bevy::ecs::name::HashedStr;
use crate::math::int::FGi32;
pub mod move_compiler;
pub enum MoveAngleKind {
Normal(FGi32),
Sakurai
}
pub struct MoveEventHurtbox {
pub priority: u32,
pub bone: HashedStr,
pub radius: FGi32,
pub damage: FGi32,
pub knockback_scaling: FGi32,
pub angle: MoveAngleKind,
}
pub enum MoveEvent {
Hurtbox(MoveEventHurtbox)
}
+177
View File
@@ -0,0 +1,177 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use rhai::EvalAltResult;
use crate::{
fighter::attack::{self, AttackAngle, FighterDamage, Knockback, KnockbackType},
math::{int::FGi32, vec::FGVec2, vec3::FGVec3},
};
#[derive(Debug, Default)]
struct MoveCompiler {
hitboxes: HashMap<u32, attack::AttackHitbox>,
active_hitboxes: Vec<u32>,
cursor: u32,
}
impl MoveCompiler {
fn frame(&mut self, new_frame: u32) -> Result<(), Box<EvalAltResult>> {
if new_frame < self.cursor {
return Err(format!(
"Tried to go back from frame {} -> {}, rolling back is not supported",
self.cursor, new_frame
)
.into());
}
self.cursor = new_frame;
Ok(())
}
fn remove_hitbox(&mut self, id: u32) -> Result<(), Box<EvalAltResult>> {
let hb = self
.hitboxes
.get_mut(&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);
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() {
self.remove_hitbox(hitbox_id)?;
}
self.active_hitboxes.clear();
Ok(())
}
}
// scripting functions
pub fn register_common_types(engine: &mut rhai::Engine) {
engine
.register_type_with_name::<KnockbackType>("KnockbackType")
.register_fn("knockback_normal", KnockbackType::make_normal)
.register_fn("knockback_fixed", KnockbackType::make_fixed)
.register_type_with_name::<AttackAngle>("Angle")
.register_fn("angle", AttackAngle::make_normal)
.register_fn("angle_sakurai", AttackAngle::make_sakurai)
.register_type_with_name::<Knockback>("Knockback")
.register_fn("knockback", Knockback::lit)
.register_type_with_name::<FGi32>("FGi32")
.register_fn("fg32", FGi32::lit)
.register_type_with_name::<FGVec2>("Vec2")
.register_fn("vec2", FGVec2::lit)
.register_type_with_name::<FGVec3>("Vec3")
.register_fn("vec3", FGVec3::lit)
.register_type_with_name::<FighterDamage>("FighterDamage")
.register_fn("damage", FighterDamage::lit);
}
pub fn compile_script(text: &str) -> Result<MoveCompiler, String> {
let mc = Rc::new(RefCell::new(MoveCompiler {
cursor: 0,
..Default::default()
}));
let mut engine = rhai::Engine::new();
register_common_types(&mut engine);
{
let mc = mc.clone();
engine.register_fn(
"frame",
move |frame: i64| -> Result<(), Box<EvalAltResult>> {
let mut b = mc.borrow_mut();
b.frame(frame as u32)
},
);
}
{
let mc = mc.clone();
engine.register_fn(
"remove_all_hitboxes",
move || -> Result<(), Box<EvalAltResult>> {
let mut b = mc.borrow_mut();
b.remove_all_hitboxes()
},
);
}
{
let mc = mc.clone();
engine.register_fn(
"remove_all_hitboxes",
move |id: i64| -> Result<(), Box<EvalAltResult>> {
let mut b = mc.borrow_mut();
b.remove_hitbox(id as u32)
},
);
}
{
let mc = mc.clone();
engine.register_fn(
"hitbox",
move |id: 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();
// Rhai does not have first class u32 support, so we have to do this.
let id = id as u32;
if b.hitboxes.contains_key(&id) {
return Err(format!("Hitbox with ID {} already existed!", id).into());
}
let hitbox = attack::AttackHitbox {
id,
bone: bone.to_owned(),
damage,
offset,
radius,
angle,
knockback_type,
knockback,
knockback_growth,
start_frame: b.cursor,
end_frame: b.cursor,
};
b.hitboxes.insert(id, hitbox);
b.active_hitboxes.push(id);
Ok(())
},
);
}
engine
.run(&text)
.map(|_| Rc::into_inner(mc).unwrap().into_inner())
.map_err(|err| err.to_string())
}