Implement attack script baking

This commit is contained in:
2026-08-29 23:47:29 +02:00
parent 631096a262
commit 3649034b03
5 changed files with 225 additions and 103 deletions
+9 -12
View File
@@ -1,12 +1,8 @@
use bevy::reflect::Reflect;
use serde::{Deserialize, Serialize};
use crate::math::{int::FGi32, vec3::FGVec3}; use crate::math::{int::FGi32, vec3::FGVec3};
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct FighterMoveManifest {
script_path: String,
}
#[derive(Debug, Default, Clone)]
pub struct FighterDamage(FGi32); pub struct FighterDamage(FGi32);
impl FighterDamage { impl FighterDamage {
@@ -15,7 +11,7 @@ impl FighterDamage {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum KnockbackType { pub enum KnockbackType {
Normal, Normal,
Fixed Fixed
@@ -31,7 +27,7 @@ impl KnockbackType {
} }
} }
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Knockback(FGi32); pub struct Knockback(FGi32);
impl Knockback { impl Knockback {
@@ -40,7 +36,7 @@ impl Knockback {
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AttackAngle { pub enum AttackAngle {
Normal(FGi32), Normal(FGi32),
Sakurai Sakurai
@@ -63,7 +59,8 @@ impl AttackAngle {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub struct AttackHitbox { pub struct AttackHitbox {
pub id: u32, pub id: u32,
pub bone: String, pub bone: String,
+1
View File
@@ -81,6 +81,7 @@ impl Plugin for GamePlugin {
input::FighterInputPlugin, input::FighterInputPlugin,
fighter::FighterPlugin, fighter::FighterPlugin,
fighter::baked_animation::BakedAnimationPlugin, fighter::baked_animation::BakedAnimationPlugin,
scripting::importer::FighterScriptImportPlugin,
stage::StagePlugin, stage::StagePlugin,
MatchCameraPlugin, MatchCameraPlugin,
bevy_common_assets::ron::RonAssetPlugin::<FighterManifest>::new(&["fighter.ron"]), bevy_common_assets::ron::RonAssetPlugin::<FighterManifest>::new(&["fighter.ron"]),
+121
View File
@@ -0,0 +1,121 @@
use std::io;
use bevy::{asset::{AssetLoader, AssetPath, AsyncReadExt, AsyncWriteExt, LoadContext, io::{Reader, Writer}, processor::LoadTransformAndSave, saver::{AssetSaver, SavedAsset}, transformer::{AssetTransformer, TransformedAsset}}, prelude::*};
use crate::scripting::{FighterAttackScript, move_compiler};
#[derive(Default, TypePath)]
struct FighterAttackScriptSourceLoader;
#[derive(Asset, TypePath)]
struct FighterAttackScriptContents(String);
impl AssetLoader for FighterAttackScriptSourceLoader {
type Asset = FighterAttackScriptContents;
type Settings = ();
type Error = io::Error;
async fn load(
&self,
reader: &mut dyn Reader,
_: &Self::Settings,
_: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut contents = String::default();
reader.read_to_string(&mut contents).await?;
Ok(FighterAttackScriptContents(contents))
}
fn extensions(&self) -> &[&str] {
&["attack.rhai"]
}
}
#[derive(Default, TypePath)]
struct FighterAttackScriptTransformer;
impl AssetTransformer for FighterAttackScriptTransformer {
type AssetInput = FighterAttackScriptContents;
type AssetOutput = FighterAttackScript;
type Settings = ();
type Error = String;
async fn transform<'a>(
&'a self,
source: TransformedAsset<Self::AssetInput>,
_: &'a Self::Settings,
) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> {
let output = move_compiler::compile_script(&source.0)?;
Ok(source.replace_asset(output))
}
}
#[derive(Default, TypePath)]
struct FighterAttackScriptLoader;
impl AssetLoader for FighterAttackScriptLoader {
type Asset = FighterAttackScript;
type Settings = ();
type Error = io::Error;
async fn load(
&self,
reader: &mut dyn Reader,
_: &Self::Settings,
_: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
bincode::deserialize(&bytes)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
}
fn extensions(&self) -> &[&str] {
&["attack.rhai"]
}
}
#[derive(Default, TypePath)]
struct FighterAttackScriptSaver;
impl AssetSaver for FighterAttackScriptSaver {
type Asset = FighterAttackScript;
type Settings = ();
type OutputLoader = FighterAttackScriptLoader;
type Error = io::Error;
async fn save(
&self,
writer: &mut Writer,
asset: SavedAsset<'_, '_, Self::Asset>,
_: &Self::Settings,
_: AssetPath<'_>,
) -> Result<(), Self::Error> {
let bytes = bincode::serialize(&*asset)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
writer.write_all(&bytes).await
}
}
type FighterAttackScriptProcessor = LoadTransformAndSave<
FighterAttackScriptSourceLoader,
FighterAttackScriptTransformer,
FighterAttackScriptSaver,
>;
pub struct FighterScriptImportPlugin;
impl Plugin for FighterScriptImportPlugin {
fn build(&self, app: &mut App) {
app.init_asset::<FighterAttackScript>()
.register_asset_loader(FighterAttackScriptSourceLoader)
.register_asset_loader(FighterAttackScriptLoader)
.register_asset_processor(FighterAttackScriptProcessor::new(
FighterAttackScriptTransformer,
FighterAttackScriptSaver,
))
.set_default_asset_processor::<FighterAttackScriptProcessor>("attack.rhai");
}
}
+7 -13
View File
@@ -1,23 +1,17 @@
use bevy::ecs::name::HashedStr; use bevy::{prelude::*};
use serde::{Deserialize, Serialize};
use crate::math::int::FGi32; use crate::{fighter::attack::AttackHitbox, math::int::FGi32};
pub mod move_compiler; pub mod move_compiler;
pub mod importer;
pub enum MoveAngleKind { pub enum MoveAngleKind {
Normal(FGi32), Normal(FGi32),
Sakurai Sakurai
} }
pub struct MoveEventHurtbox { #[derive(Asset, Reflect, Serialize, Deserialize)]
pub priority: u32, pub struct FighterAttackScript {
pub bone: HashedStr, pub hitboxes: Vec<AttackHitbox>
pub radius: FGi32,
pub damage: FGi32,
pub knockback_scaling: FGi32,
pub angle: MoveAngleKind,
}
pub enum MoveEvent {
Hurtbox(MoveEventHurtbox)
} }
+87 -78
View File
@@ -3,8 +3,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc};
use rhai::EvalAltResult; use rhai::EvalAltResult;
use crate::{ use crate::{
fighter::attack::{self, AttackAngle, FighterDamage, Knockback, KnockbackType}, fighter::attack::{self, AttackAngle, FighterDamage, Knockback, KnockbackType}, math::{int::FGi32, vec::FGVec2, vec3::FGVec3}, scripting::FighterAttackScript,
math::{int::FGi32, vec::FGVec2, vec3::FGVec3},
}; };
#[derive(Debug, Default)] #[derive(Debug, Default)]
@@ -59,6 +58,14 @@ impl MoveCompiler {
Ok(()) Ok(())
} }
fn into_attack_script(mut self) -> Result<FighterAttackScript, String> {
self.remove_all_hitboxes().map_err(|err| err.to_string())?;
let hitboxes = self.hitboxes.into_iter().map(|(_, hitbox)| hitbox).collect();
Ok(FighterAttackScript {
hitboxes
})
}
} }
// scripting functions // scripting functions
@@ -83,95 +90,97 @@ pub fn register_common_types(engine: &mut rhai::Engine) {
.register_fn("damage", FighterDamage::lit); .register_fn("damage", FighterDamage::lit);
} }
pub fn compile_script(text: &str) -> Result<MoveCompiler, String> { pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
let mc = Rc::new(RefCell::new(MoveCompiler { let mc = Rc::new(RefCell::new(MoveCompiler {
cursor: 0, cursor: 0,
..Default::default() ..Default::default()
})); }));
let mut engine = rhai::Engine::new(); let result = {
let mut engine = rhai::Engine::new();
register_common_types(&mut engine); register_common_types(&mut engine);
{ {
let mc = mc.clone(); let mc = mc.clone();
engine.register_fn( engine.register_fn(
"frame", "frame",
move |frame: i64| -> Result<(), Box<EvalAltResult>> { move |frame: i64| -> Result<(), Box<EvalAltResult>> {
let mut b = mc.borrow_mut(); let mut b = mc.borrow_mut();
b.frame(frame as u32) b.frame(frame as u32)
}, },
); );
} }
{ {
let mc = mc.clone(); let mc = mc.clone();
engine.register_fn( engine.register_fn(
"remove_all_hitboxes", "remove_all_hitboxes",
move || -> Result<(), Box<EvalAltResult>> { move || -> Result<(), Box<EvalAltResult>> {
let mut b = mc.borrow_mut(); let mut b = mc.borrow_mut();
b.remove_all_hitboxes() b.remove_all_hitboxes()
}, },
); );
} }
{ {
let mc = mc.clone(); let mc = mc.clone();
engine.register_fn( engine.register_fn(
"remove_all_hitboxes", "remove_all_hitboxes",
move |id: i64| -> Result<(), Box<EvalAltResult>> { move |id: i64| -> Result<(), Box<EvalAltResult>> {
let mut b = mc.borrow_mut(); let mut b = mc.borrow_mut();
b.remove_hitbox(id as u32) b.remove_hitbox(id as u32)
}, },
); );
} }
{ {
let mc = mc.clone(); let mc = mc.clone();
engine.register_fn( engine.register_fn(
"hitbox", "hitbox",
move |id: i64, move |id: i64,
bone: &str, bone: &str,
damage: FighterDamage, damage: FighterDamage,
offset: FGVec3, offset: FGVec3,
radius: FGi32, radius: FGi32,
angle: AttackAngle, angle: AttackAngle,
knockback_type: KnockbackType, knockback_type: KnockbackType,
knockback: Knockback, knockback: Knockback,
knockback_growth: FGi32| knockback_growth: FGi32|
-> Result<(), Box<EvalAltResult>> { -> Result<(), Box<EvalAltResult>> {
let mut b = mc.borrow_mut(); let mut b = mc.borrow_mut();
// Rhai does not have first class u32 support, so we have to do this. // Rhai does not have first class u32 support, so we have to do this.
let id = id as u32; let id = id as u32;
if b.hitboxes.contains_key(&id) { if b.hitboxes.contains_key(&id) {
return Err(format!("Hitbox with ID {} already existed!", id).into()); return Err(format!("Hitbox with ID {} already existed!", id).into());
} }
let hitbox = attack::AttackHitbox { let hitbox = attack::AttackHitbox {
id, id,
bone: bone.to_owned(), bone: bone.to_owned(),
damage, damage,
offset, offset,
radius, radius,
angle, angle,
knockback_type, knockback_type,
knockback, knockback,
knockback_growth, knockback_growth,
start_frame: b.cursor, start_frame: b.cursor,
end_frame: b.cursor, end_frame: b.cursor,
}; };
b.hitboxes.insert(id, hitbox); b.hitboxes.insert(id, hitbox);
b.active_hitboxes.push(id); b.active_hitboxes.push(id);
Ok(()) Ok(())
}, },
); );
} }
engine.run(&text)
};
engine
.run(&text) result.map(|_| Rc::into_inner(mc).unwrap().into_inner().into_attack_script())
.map(|_| Rc::into_inner(mc).unwrap().into_inner()) .map_err(|err| err.to_string())?
.map_err(|err| err.to_string())
} }