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};
pub struct FighterMoveManifest {
script_path: String,
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct FighterDamage(FGi32);
impl FighterDamage {
@@ -15,7 +11,7 @@ impl FighterDamage {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum KnockbackType {
Normal,
Fixed
@@ -31,7 +27,7 @@ impl KnockbackType {
}
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Knockback(FGi32);
impl Knockback {
@@ -40,7 +36,7 @@ impl Knockback {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AttackAngle {
Normal(FGi32),
Sakurai
@@ -63,7 +59,8 @@ impl AttackAngle {
}
}
#[derive(Debug)]
#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect(opaque)]
pub struct AttackHitbox {
pub id: u32,
pub bone: String,
+1
View File
@@ -81,6 +81,7 @@ impl Plugin for GamePlugin {
input::FighterInputPlugin,
fighter::FighterPlugin,
fighter::baked_animation::BakedAnimationPlugin,
scripting::importer::FighterScriptImportPlugin,
stage::StagePlugin,
MatchCameraPlugin,
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 importer;
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)
#[derive(Asset, Reflect, Serialize, Deserialize)]
pub struct FighterAttackScript {
pub hitboxes: Vec<AttackHitbox>
}
+87 -78
View File
@@ -3,8 +3,7 @@ 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},
fighter::attack::{self, AttackAngle, FighterDamage, Knockback, KnockbackType}, math::{int::FGi32, vec::FGVec2, vec3::FGVec3}, scripting::FighterAttackScript,
};
#[derive(Debug, Default)]
@@ -59,6 +58,14 @@ impl MoveCompiler {
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
@@ -83,95 +90,97 @@ pub fn register_common_types(engine: &mut rhai::Engine) {
.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 {
cursor: 0,
..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();
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(
"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 || -> 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(
"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();
{
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;
// 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());
}
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,
};
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);
b.hitboxes.insert(id, hitbox);
b.active_hitboxes.push(id);
Ok(())
},
);
}
Ok(())
},
);
}
engine.run(&text)
};
engine
.run(&text)
.map(|_| Rc::into_inner(mc).unwrap().into_inner())
.map_err(|err| err.to_string())
result.map(|_| Rc::into_inner(mc).unwrap().into_inner().into_attack_script())
.map_err(|err| err.to_string())?
}