Attack system revamp #82
Generated
+114
-210
@@ -531,7 +531,7 @@ dependencies = [
|
|||||||
"bevy_color",
|
"bevy_color",
|
||||||
"bevy_core_pipeline",
|
"bevy_core_pipeline",
|
||||||
"bevy_ecs",
|
"bevy_ecs",
|
||||||
"bevy_egui 0.40.1",
|
"bevy_egui",
|
||||||
"bevy_gizmos",
|
"bevy_gizmos",
|
||||||
"bevy_image",
|
"bevy_image",
|
||||||
"bevy_light",
|
"bevy_light",
|
||||||
@@ -549,7 +549,7 @@ dependencies = [
|
|||||||
"bevy_window",
|
"bevy_window",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"disqualified",
|
"disqualified",
|
||||||
"egui 0.34.3",
|
"egui",
|
||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
"image",
|
"image",
|
||||||
"opener",
|
"opener",
|
||||||
@@ -1061,55 +1061,7 @@ dependencies = [
|
|||||||
"bevy_winit",
|
"bevy_winit",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"egui 0.34.3",
|
"egui",
|
||||||
"encase",
|
|
||||||
"getrandom 0.4.3",
|
|
||||||
"image",
|
|
||||||
"itertools 0.14.0",
|
|
||||||
"js-sys",
|
|
||||||
"smithay-clipboard",
|
|
||||||
"thread_local",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
"webbrowser",
|
|
||||||
"wgpu-types",
|
|
||||||
"winit",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bevy_egui"
|
|
||||||
version = "0.42.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e99e2d018c7e567a268638d3058e57895e914e5650dcf7bd80a5fead9e6413a9"
|
|
||||||
dependencies = [
|
|
||||||
"arboard",
|
|
||||||
"bevy_app",
|
|
||||||
"bevy_asset",
|
|
||||||
"bevy_camera",
|
|
||||||
"bevy_color",
|
|
||||||
"bevy_core_pipeline",
|
|
||||||
"bevy_derive",
|
|
||||||
"bevy_ecs",
|
|
||||||
"bevy_image",
|
|
||||||
"bevy_input",
|
|
||||||
"bevy_log",
|
|
||||||
"bevy_math",
|
|
||||||
"bevy_mesh",
|
|
||||||
"bevy_picking",
|
|
||||||
"bevy_platform",
|
|
||||||
"bevy_reflect",
|
|
||||||
"bevy_render",
|
|
||||||
"bevy_shader",
|
|
||||||
"bevy_time",
|
|
||||||
"bevy_transform",
|
|
||||||
"bevy_ui_render",
|
|
||||||
"bevy_utils",
|
|
||||||
"bevy_window",
|
|
||||||
"bevy_winit",
|
|
||||||
"bytemuck",
|
|
||||||
"crossbeam-channel",
|
|
||||||
"egui 0.36.2",
|
|
||||||
"encase",
|
"encase",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"image",
|
"image",
|
||||||
@@ -1324,7 +1276,7 @@ dependencies = [
|
|||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"guillotiere 0.6.2",
|
"guillotiere",
|
||||||
"half",
|
"half",
|
||||||
"image",
|
"image",
|
||||||
"ktx2",
|
"ktx2",
|
||||||
@@ -2214,17 +2166,6 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bevygraph"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "git+https://github.com/tbillington/bevy_debug_view#0d520f5f4e8693166408f082e8d25dd377dabc3e"
|
|
||||||
dependencies = [
|
|
||||||
"bevy",
|
|
||||||
"bevy_egui 0.42.0",
|
|
||||||
"indexmap",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
@@ -3041,17 +2982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a05fbfa222ffb51989d5ccf33e5f7aebfcf96c5023413856b0c3618a7f79896e"
|
checksum = "a05fbfa222ffb51989d5ccf33e5f7aebfcf96c5023413856b0c3618a7f79896e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"emath 0.34.3",
|
"emath",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ecolor"
|
|
||||||
version = "0.36.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0656a7530b9e2841ee7a5f983965bdd9840c32f4012f759cfd44b912cced6b34"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"emath 0.36.2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3063,8 +2994,8 @@ dependencies = [
|
|||||||
"accesskit",
|
"accesskit",
|
||||||
"ahash",
|
"ahash",
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
"emath 0.34.3",
|
"emath",
|
||||||
"epaint 0.34.3",
|
"epaint",
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
"profiling",
|
"profiling",
|
||||||
@@ -3073,23 +3004,17 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "egui"
|
name = "egui_extras"
|
||||||
version = "0.36.2"
|
version = "0.34.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc938cc27cd911415e1e4d151bc56ff9df063d8db081374e87aeb805ea74b2ba"
|
checksum = "598d8675f6fd9088db8a93d8c7aacda936b2f3d0c2b0660ad1744a45b5caf922"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
|
||||||
"ahash",
|
"ahash",
|
||||||
"bitflags 2.13.1",
|
"egui",
|
||||||
"emath 0.36.2",
|
"enum-map",
|
||||||
"epaint 0.36.2",
|
|
||||||
"itertools 0.15.0",
|
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"mime_guess2",
|
||||||
"profiling",
|
"profiling",
|
||||||
"smallvec",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"web-sys",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3107,15 +3032,6 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "emath"
|
|
||||||
version = "0.36.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ena"
|
name = "ena"
|
||||||
version = "0.14.4"
|
version = "0.14.4"
|
||||||
@@ -3156,6 +3072,26 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-map"
|
||||||
|
version = "2.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9"
|
||||||
|
dependencies = [
|
||||||
|
"enum-map-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-map-derive"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enum_cycling"
|
name = "enum_cycling"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -3181,9 +3117,9 @@ checksum = "6675898a291ec212fc3df04f537d177fce8496120244590e6359dcaa4c25da79"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"ecolor 0.34.3",
|
"ecolor",
|
||||||
"emath 0.34.3",
|
"emath",
|
||||||
"epaint_default_fonts 0.34.3",
|
"epaint_default_fonts",
|
||||||
"font-types 0.11.3",
|
"font-types 0.11.3",
|
||||||
"log",
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
@@ -3192,32 +3128,7 @@ dependencies = [
|
|||||||
"self_cell",
|
"self_cell",
|
||||||
"skrifa 0.40.0",
|
"skrifa 0.40.0",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"vello_cpu 0.0.6",
|
"vello_cpu",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "epaint"
|
|
||||||
version = "0.36.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a0ec0308dc23130fc1a7d4623a8534c2ed069c6dc84e258ee4f3de99615aa1a3"
|
|
||||||
dependencies = [
|
|
||||||
"ahash",
|
|
||||||
"bytemuck",
|
|
||||||
"ecolor 0.36.2",
|
|
||||||
"emath 0.36.2",
|
|
||||||
"epaint_default_fonts 0.36.2",
|
|
||||||
"font-types 0.12.2",
|
|
||||||
"harfrust 0.12.0",
|
|
||||||
"log",
|
|
||||||
"nohash-hasher",
|
|
||||||
"parking_lot",
|
|
||||||
"profiling",
|
|
||||||
"self_cell",
|
|
||||||
"skrifa 0.44.0",
|
|
||||||
"smallvec",
|
|
||||||
"unicode-general-category",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"vello_cpu 0.1.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3226,12 +3137,6 @@ version = "0.34.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8970033a4282a7bcf899b38b5ed3a58b732fe093d03785d58648515d8d309da"
|
checksum = "f8970033a4282a7bcf899b38b5ed3a58b732fe093d03785d58648515d8d309da"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "epaint_default_fonts"
|
|
||||||
version = "0.36.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "773fa9c96dd0dbef887e39d0ed6177f141cce4d68e6041df77570aa3702dfa13"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -3324,12 +3229,6 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fearless_simd"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "file-id"
|
name = "file-id"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -3715,22 +3614,6 @@ dependencies = [
|
|||||||
"simba",
|
"simba",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "glifo"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"foldhash 0.2.0",
|
|
||||||
"hashbrown 0.17.1",
|
|
||||||
"log",
|
|
||||||
"peniko",
|
|
||||||
"skrifa 0.44.0",
|
|
||||||
"smallvec",
|
|
||||||
"vello_common 0.1.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glow"
|
name = "glow"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
@@ -3841,15 +3724,6 @@ dependencies = [
|
|||||||
"svg_fmt",
|
"svg_fmt",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "guillotiere"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab"
|
|
||||||
dependencies = [
|
|
||||||
"euclid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "half"
|
name = "half"
|
||||||
version = "2.7.1"
|
version = "2.7.1"
|
||||||
@@ -3876,18 +3750,6 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "harfrust"
|
|
||||||
version = "0.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.13.1",
|
|
||||||
"bytemuck",
|
|
||||||
"read-fonts 0.41.0",
|
|
||||||
"smallvec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hash32"
|
name = "hash32"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -4552,6 +4414,24 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess2"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"phf",
|
||||||
|
"phf_shared",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -5370,7 +5250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8fad031076f48f0d4d85ce1aea9b94b4e715a4d636a030a123038f8f5b5e4343"
|
checksum = "8fad031076f48f0d4d85ce1aea9b94b4e715a4d636a030a123038f8f5b5e4343"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fontique",
|
"fontique",
|
||||||
"harfrust 0.6.2",
|
"harfrust",
|
||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
"icu_normalizer",
|
"icu_normalizer",
|
||||||
"icu_properties",
|
"icu_properties",
|
||||||
@@ -5497,6 +5377,50 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||||
|
dependencies = [
|
||||||
|
"phf_macros",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared",
|
||||||
|
"rand 0.8.7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_macros"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.13"
|
version = "1.1.13"
|
||||||
@@ -6272,18 +6196,19 @@ dependencies = [
|
|||||||
"bevy",
|
"bevy",
|
||||||
"bevy-inspector-egui",
|
"bevy-inspector-egui",
|
||||||
"bevy_asset_loader",
|
"bevy_asset_loader",
|
||||||
"bevy_egui 0.40.1",
|
"bevy_egui",
|
||||||
"bevy_ggrs",
|
"bevy_ggrs",
|
||||||
"bevygraph",
|
|
||||||
"bincode",
|
"bincode",
|
||||||
"blake3",
|
"blake3",
|
||||||
"clap",
|
"clap",
|
||||||
|
"egui_extras",
|
||||||
"enum_cycling",
|
"enum_cycling",
|
||||||
"enum_cycling_derive",
|
"enum_cycling_derive",
|
||||||
"fixed",
|
"fixed",
|
||||||
"fixed_math",
|
"fixed_math",
|
||||||
"gc-adapter-neo",
|
"gc-adapter-neo",
|
||||||
"gltf",
|
"gltf",
|
||||||
|
"itertools 0.15.0",
|
||||||
"rhai",
|
"rhai",
|
||||||
"ron",
|
"ron",
|
||||||
"ron_asset_manager",
|
"ron_asset_manager",
|
||||||
@@ -6335,6 +6260,12 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "skrifa"
|
name = "skrifa"
|
||||||
version = "0.40.0"
|
version = "0.40.0"
|
||||||
@@ -6992,10 +6923,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6"
|
checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-general-category"
|
name = "unicase"
|
||||||
version = "1.1.0"
|
version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
@@ -7105,7 +7036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b"
|
checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"fearless_simd 0.3.0",
|
"fearless_simd",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
"log",
|
"log",
|
||||||
"peniko",
|
"peniko",
|
||||||
@@ -7113,21 +7044,6 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vello_common"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"fearless_simd 0.4.1",
|
|
||||||
"guillotiere 0.7.0",
|
|
||||||
"log",
|
|
||||||
"peniko",
|
|
||||||
"smallvec",
|
|
||||||
"thiserror 2.0.19",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vello_cpu"
|
name = "vello_cpu"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
@@ -7136,19 +7052,7 @@ checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
"vello_common 0.0.6",
|
"vello_common",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vello_cpu"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"glifo",
|
|
||||||
"hashbrown 0.17.1",
|
|
||||||
"vello_common 0.1.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+9
-5
@@ -8,23 +8,22 @@ authors = [""]
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["development", "debug_features", "gamecube-adapter"]
|
default = ["development", "debug_features", "gamecube-adapter"]
|
||||||
development = ["bevy/dynamic_linking", "bevy/file_watcher", "bevy/embedded_watcher"]
|
development = ["bevy/dynamic_linking", "bevy/file_watcher", "bevy/embedded_watcher", "hitbox-viewer"]
|
||||||
debug_features = ["bevy/free_camera"]
|
debug_features = ["bevy/free_camera"]
|
||||||
gamecube-adapter = ["dep:gc-adapter-neo"]
|
gamecube-adapter = ["dep:gc-adapter-neo"]
|
||||||
|
hitbox-viewer = ["bevy/bevy_dev_tools", "dep:egui_extras"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = { version = "0.19.1", features = ["asset_processor", "basis-universal", "compressed_image_saver", "bevy_settings"] }
|
bevy = { version = "0.19.1", features = ["asset_processor", "basis-universal", "compressed_image_saver", "bevy_settings"] }
|
||||||
# Loads the scenes you author in the editor (assets/*.bsn) and builds
|
|
||||||
# physics colliders from them. An ordinary dependency; remove it if you
|
|
||||||
# stop using authored scenes.
|
|
||||||
blake3 = { version = "1.8.5", features = [ "pure" ] }
|
blake3 = { version = "1.8.5", features = [ "pure" ] }
|
||||||
|
itertools = "0.15.0"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
avian3d = "0.7"
|
avian3d = "0.7"
|
||||||
arrayvec = "0.7.8"
|
arrayvec = "0.7.8"
|
||||||
bevy-inspector-egui = "0.37.0"
|
bevy-inspector-egui = "0.37.0"
|
||||||
bevy_egui = "0.40.0"
|
bevy_egui = "0.40.0"
|
||||||
|
egui_extras = { version = "0.34.3", optional = true}
|
||||||
bevy_ggrs = { git = "https://github.com/gschup/bevy_ggrs" }
|
bevy_ggrs = { git = "https://github.com/gschup/bevy_ggrs" }
|
||||||
bevygraph = { git = "https://github.com/tbillington/bevy_debug_view" }
|
|
||||||
serde = "1.0.229"
|
serde = "1.0.229"
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
@@ -53,3 +52,8 @@ strip = "symbols"
|
|||||||
|
|
||||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||||
gc-adapter-neo = { version = "1.1.1", features = ["libusb"], optional = true }
|
gc-adapter-neo = { version = "1.1.1", features = ["libusb"], optional = true }
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "hitbox-editor"
|
||||||
|
path = "src/bin/hitbox-editor.rs"
|
||||||
|
required-features = ["hitbox-viewer"]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use bevy_egui::egui::Color32;
|
||||||
|
|
||||||
|
pub trait ColorToEgui {
|
||||||
|
fn to_egui(&self) -> Color32;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorToEgui for bevy::color::Srgba {
|
||||||
|
fn to_egui(&self) -> Color32 {
|
||||||
|
Color32::from_rgba_unmultiplied(
|
||||||
|
(self.red * 255.0) as u8,
|
||||||
|
(self.green * 255.0) as u8,
|
||||||
|
(self.blue * 255.0) as u8,
|
||||||
|
(self.alpha * 255.0) as u8,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ use crate::{
|
|||||||
#[derive(Deref, Component, Default, Hash, Debug, Copy, Clone)]
|
#[derive(Deref, Component, Default, Hash, Debug, Copy, Clone)]
|
||||||
pub struct FighterKnockbackVelocity(pub FGVec2);
|
pub struct FighterKnockbackVelocity(pub FGVec2);
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Component, Hash, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Component, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct FighterDamage(FGi32);
|
pub struct FighterDamage(FGi32);
|
||||||
|
|
||||||
impl std::fmt::Display for FighterDamage {
|
impl std::fmt::Display for FighterDamage {
|
||||||
@@ -52,7 +52,8 @@ pub enum HitReactionKind {
|
|||||||
pub struct FighterHitReaction {
|
pub struct FighterHitReaction {
|
||||||
pub launch_velocity: FGi32,
|
pub launch_velocity: FGi32,
|
||||||
pub launch_angle: FGi32,
|
pub launch_angle: FGi32,
|
||||||
pub hitstop_frames: HitStop,
|
pub victim_hitstop_frames: HitStop,
|
||||||
|
pub attacker_hitstop_frames: HitStop,
|
||||||
pub hitstun_frames: u32,
|
pub hitstun_frames: u32,
|
||||||
pub kind: HitReactionKind,
|
pub kind: HitReactionKind,
|
||||||
pub damage: FighterDamage,
|
pub damage: FighterDamage,
|
||||||
@@ -80,7 +81,7 @@ impl HitStop {
|
|||||||
HitStop(hitstop)
|
HitStop(hitstop)
|
||||||
}
|
}
|
||||||
pub fn calculate(attack_damage_stalled: FighterDamage, crouch_cancelling: bool) -> Self {
|
pub fn calculate(attack_damage_stalled: FighterDamage, crouch_cancelling: bool) -> Self {
|
||||||
let a = (attack_damage_stalled.0 / FGi32::lit("3.0") + FGi32::lit("4.0")).round_to_zero();
|
let a = (attack_damage_stalled.0 / FGi32::lit("3.0") + FGi32::lit("3.0")).round_to_zero();
|
||||||
let crouch_cancelling_factor = if crouch_cancelling {
|
let crouch_cancelling_factor = if crouch_cancelling {
|
||||||
FGi32::lit("0.66666666")
|
FGi32::lit("0.66666666")
|
||||||
} else {
|
} else {
|
||||||
@@ -97,6 +98,12 @@ impl HitStop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for HitStop {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||||
pub enum KnockbackType {
|
pub enum KnockbackType {
|
||||||
Normal,
|
Normal,
|
||||||
@@ -113,9 +120,15 @@ impl KnockbackType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct Knockback(FGi32);
|
pub struct Knockback(FGi32);
|
||||||
|
|
||||||
|
impl std::fmt::Display for Knockback {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{:.2}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Knockback {
|
impl Knockback {
|
||||||
pub fn lit(val: &str) -> Self {
|
pub fn lit(val: &str) -> Self {
|
||||||
Self(FGi32::lit(val))
|
Self(FGi32::lit(val))
|
||||||
@@ -144,6 +157,15 @@ pub enum AttackAngle {
|
|||||||
Sakurai,
|
Sakurai,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for AttackAngle {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
AttackAngle::Normal(fixed_i32) => write!(f, "{:.2}º", fixed_i32),
|
||||||
|
AttackAngle::Sakurai => write!(f, "Sakurai"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for AttackAngle {
|
impl Default for AttackAngle {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
AttackAngle::Normal(FGi32::lit("0"))
|
AttackAngle::Normal(FGi32::lit("0"))
|
||||||
@@ -175,6 +197,13 @@ impl AttackAngle {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_melee_angle_representation(&self) -> FGi32 {
|
||||||
|
match self {
|
||||||
|
AttackAngle::Normal(fixed_i32) => *fixed_i32,
|
||||||
|
AttackAngle::Sakurai => FGi32::lit("361"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Reflect, PartialEq, Eq, Hash, Clone, Debug, Default, Copy)]
|
#[derive(Serialize, Deserialize, Reflect, PartialEq, Eq, Hash, Clone, Debug, Default, Copy)]
|
||||||
@@ -217,6 +246,7 @@ impl AttackKind {
|
|||||||
#[reflect(opaque)]
|
#[reflect(opaque)]
|
||||||
pub struct AttackHitbox {
|
pub struct AttackHitbox {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
|
pub group: u32,
|
||||||
pub bone: String,
|
pub bone: String,
|
||||||
pub damage: FighterDamage,
|
pub damage: FighterDamage,
|
||||||
pub offset: FGVec3,
|
pub offset: FGVec3,
|
||||||
@@ -229,6 +259,14 @@ pub struct AttackHitbox {
|
|||||||
pub end_frame: u32,
|
pub end_frame: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ATTACK_HITBOX_ID_PALETTE: [bevy::color::Srgba; 5] = [
|
||||||
|
bevy::color::palettes::tailwind::RED_500,
|
||||||
|
bevy::color::palettes::tailwind::GREEN_500,
|
||||||
|
bevy::color::palettes::tailwind::BLUE_500,
|
||||||
|
bevy::color::palettes::tailwind::FUCHSIA_500,
|
||||||
|
bevy::color::palettes::tailwind::ORANGE_500,
|
||||||
|
];
|
||||||
|
|
||||||
const STALE_MOVE_QUEUE_COUNT: usize = 9;
|
const STALE_MOVE_QUEUE_COUNT: usize = 9;
|
||||||
|
|
||||||
#[derive(Component, Hash, Clone, Default)]
|
#[derive(Component, Hash, Clone, Default)]
|
||||||
@@ -276,6 +314,10 @@ impl FighterStaleMoveQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AttackHitbox {
|
impl AttackHitbox {
|
||||||
|
pub fn get_debug_draw_color(&self) -> bevy::color::Srgba {
|
||||||
|
ATTACK_HITBOX_ID_PALETTE[self.id as usize % ATTACK_HITBOX_ID_PALETTE.len()]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validate(&self) -> Result<(), String> {
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
if self.bone.trim().is_empty() {
|
if self.bone.trim().is_empty() {
|
||||||
return Err("bone name is empty".into());
|
return Err("bone name is empty".into());
|
||||||
@@ -369,6 +411,36 @@ pub fn update_active_hitbox_list(script: &FighterAttackScript, list: &mut Vec<us
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates both the currently active hitbox intervals and the lifetime of
|
||||||
|
/// their per-group victim histories. A group remains immune only while at
|
||||||
|
/// least one hitbox belonging to that group is active.
|
||||||
|
pub fn update_active_attack(
|
||||||
|
script: &FighterAttackScript,
|
||||||
|
attack: &mut super::FighterActiveAttack,
|
||||||
|
frame: u32,
|
||||||
|
) {
|
||||||
|
update_active_hitbox_list(script, &mut attack.active_hitboxes, frame);
|
||||||
|
|
||||||
|
let mut active_groups = Vec::new();
|
||||||
|
for &index in &attack.active_hitboxes {
|
||||||
|
let group = script.hitboxes[index].group;
|
||||||
|
if !active_groups.contains(&group) {
|
||||||
|
active_groups.push(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attack.hit_groups.retain(|state| active_groups.contains(&state.group));
|
||||||
|
for group in active_groups {
|
||||||
|
if !attack.hit_groups.iter().any(|state| state.group == group) {
|
||||||
|
attack.hit_groups.push(super::FighterHitGroupState {
|
||||||
|
group,
|
||||||
|
hit_fighters: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attack.hit_groups.sort_unstable_by_key(|state| state.group);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Hash)]
|
||||||
pub struct FighterSolvedHurtbox {
|
pub struct FighterSolvedHurtbox {
|
||||||
pub hurtbox_idx: usize,
|
pub hurtbox_idx: usize,
|
||||||
@@ -571,10 +643,6 @@ pub fn intersect_attacks_with_hurtboxes(
|
|||||||
let Some(active_attack) = hitboxes.0.as_mut() else {
|
let Some(active_attack) = hitboxes.0.as_mut() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if active_attack.hit_fighters.contains(&entity_receiving) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let attack_script = attack_scripts
|
let attack_script = attack_scripts
|
||||||
.get(&active_attack.script)
|
.get(&active_attack.script)
|
||||||
.expect("Attack script should be valid");
|
.expect("Attack script should be valid");
|
||||||
@@ -584,6 +652,15 @@ pub fn intersect_attacks_with_hurtboxes(
|
|||||||
for solved_hitbox in active_attack.solved_hitboxes.iter() {
|
for solved_hitbox in active_attack.solved_hitboxes.iter() {
|
||||||
if hurtbox.capsule.intersects(solved_hitbox.capsule) {
|
if hurtbox.capsule.intersects(solved_hitbox.capsule) {
|
||||||
let candidate = solved_hitbox.hitbox_idx;
|
let candidate = solved_hitbox.hitbox_idx;
|
||||||
|
let group = attack_script.hitboxes[candidate].group;
|
||||||
|
let already_hit = active_attack
|
||||||
|
.hit_groups
|
||||||
|
.iter()
|
||||||
|
.find(|state| state.group == group)
|
||||||
|
.is_some_and(|state| state.hit_fighters.contains(&entity_receiving));
|
||||||
|
if already_hit {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if hitting_hitbox_idx.is_none_or(|current: usize| {
|
if hitting_hitbox_idx.is_none_or(|current: usize| {
|
||||||
attack_script.hitboxes[candidate].id
|
attack_script.hitboxes[candidate].id
|
||||||
< attack_script.hitboxes[current].id
|
< attack_script.hitboxes[current].id
|
||||||
@@ -606,7 +683,14 @@ pub fn intersect_attacks_with_hurtboxes(
|
|||||||
hitbox_index: hitting_hitbox_idx,
|
hitbox_index: hitting_hitbox_idx,
|
||||||
});
|
});
|
||||||
|
|
||||||
active_attack.hit_fighters.push(entity_receiving);
|
let group = attack_script.hitboxes[hitting_hitbox_idx].group;
|
||||||
|
if let Some(state) = active_attack
|
||||||
|
.hit_groups
|
||||||
|
.iter_mut()
|
||||||
|
.find(|state| state.group == group)
|
||||||
|
{
|
||||||
|
state.hit_fighters.push(entity_receiving);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use bevy_ggrs::prelude::GgrsSchedule;
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
fighter::{
|
fighter::{
|
||||||
Fighter, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake,
|
Fighter, FighterHitboxes, FighterHitstop, FighterHitstopShake,
|
||||||
FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPendingHitReaction,
|
FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPendingHitReaction,
|
||||||
FighterStaleMoveQueue, FighterSurfaceState, errors::FighterErrors,
|
FighterStaleMoveQueue, FighterSurfaceState, errors::FighterErrors,
|
||||||
manifest::FighterManifest, state::FighterState,
|
manifest::FighterManifest, state::FighterState,
|
||||||
@@ -20,11 +20,11 @@ pub fn resolve_fighter_hits(
|
|||||||
mut pending_hits: ResMut<FighterPendingHits>,
|
mut pending_hits: ResMut<FighterPendingHits>,
|
||||||
mut attacker: Query<(
|
mut attacker: Query<(
|
||||||
&mut FighterStaleMoveQueue,
|
&mut FighterStaleMoveQueue,
|
||||||
&FighterFacingDirection,
|
|
||||||
&mut FighterHitboxes,
|
&mut FighterHitboxes,
|
||||||
&Player,
|
&Player,
|
||||||
&mut FighterHitstop,
|
&mut FighterHitstop,
|
||||||
&mut FighterHitstopShake,
|
&mut FighterHitstopShake,
|
||||||
|
&FighterTranslation,
|
||||||
)>,
|
)>,
|
||||||
mut receiver: Query<(
|
mut receiver: Query<(
|
||||||
&mut FighterDamage,
|
&mut FighterDamage,
|
||||||
@@ -33,6 +33,7 @@ pub fn resolve_fighter_hits(
|
|||||||
&FighterState,
|
&FighterState,
|
||||||
&FighterInvulnerabilityState,
|
&FighterInvulnerabilityState,
|
||||||
&FighterSurfaceState,
|
&FighterSurfaceState,
|
||||||
|
&FighterTranslation,
|
||||||
)>,
|
)>,
|
||||||
attack_scripts: Res<Assets<FighterAttackScript>>,
|
attack_scripts: Res<Assets<FighterAttackScript>>,
|
||||||
game_settings: Res<GameSettings>,
|
game_settings: Res<GameSettings>,
|
||||||
@@ -55,6 +56,7 @@ pub fn resolve_fighter_hits(
|
|||||||
receiver_state,
|
receiver_state,
|
||||||
invuln_state,
|
invuln_state,
|
||||||
receiver_surface_state,
|
receiver_surface_state,
|
||||||
|
receiver_translation,
|
||||||
)) = receiver.get_mut(defender)
|
)) = receiver.get_mut(defender)
|
||||||
else {
|
else {
|
||||||
hit_offset = next_defender;
|
hit_offset = next_defender;
|
||||||
@@ -64,7 +66,7 @@ pub fn resolve_fighter_hits(
|
|||||||
let damage_data = hits[hit_offset..next_defender]
|
let damage_data = hits[hit_offset..next_defender]
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|hit| {
|
.filter_map(|hit| {
|
||||||
let Ok((stale_move_queue, facing_dir, _, player, _, _)) =
|
let Ok((stale_move_queue, _, player, _, _, attacker_translation)) =
|
||||||
attacker.get(hit.attacker)
|
attacker.get(hit.attacker)
|
||||||
else {
|
else {
|
||||||
return None;
|
return None;
|
||||||
@@ -74,12 +76,13 @@ pub fn resolve_fighter_hits(
|
|||||||
let damage_received = stale_move_queue
|
let damage_received = stale_move_queue
|
||||||
.get_attack_damage_multiplier(hit.attack_kind)
|
.get_attack_damage_multiplier(hit.attack_kind)
|
||||||
* hitbox.damage.0;
|
* hitbox.damage.0;
|
||||||
|
let angle_flip = receiver_translation.0.x <= attacker_translation.0.x;
|
||||||
Some((
|
Some((
|
||||||
hit.attacker,
|
hit.attacker,
|
||||||
damage_received,
|
damage_received,
|
||||||
hitbox,
|
hitbox,
|
||||||
matches!(facing_dir, FighterFacingDirection::Left),
|
|
||||||
player.handle,
|
player.handle,
|
||||||
|
angle_flip,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -93,28 +96,35 @@ pub fn resolve_fighter_hits(
|
|||||||
.ok_or(FighterErrors::FighterManifestNotFound)?;
|
.ok_or(FighterErrors::FighterManifestNotFound)?;
|
||||||
let mut reactions = Vec::with_capacity(damage_data.len());
|
let mut reactions = Vec::with_capacity(damage_data.len());
|
||||||
|
|
||||||
for (attacker_entity, attack_damage, hitbox, facing_left, attacker_slot) in damage_data {
|
let crouch_canceling = if let FighterState::Crouch(crouch_state) = &receiver_state {
|
||||||
|
crouch_state.is_crouch_cancelling()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
for (attacker_entity, attack_damage, hitbox, attacker_slot, angle_flip) in damage_data {
|
||||||
let knockback = hitbox.calculate_knockback(
|
let knockback = hitbox.calculate_knockback(
|
||||||
&fighter_manifest.attributes,
|
&fighter_manifest.attributes,
|
||||||
*receiver_damage,
|
*receiver_damage,
|
||||||
FighterDamage(total_damage_received_this_frame),
|
FighterDamage(total_damage_received_this_frame),
|
||||||
false,
|
false,
|
||||||
false,
|
crouch_canceling,
|
||||||
);
|
);
|
||||||
let angle = hitbox.angle.get_angle(
|
let launch_angle = hitbox.angle.get_angle(
|
||||||
&knockback,
|
&knockback,
|
||||||
receiver_surface_state.is_grounded(),
|
receiver_surface_state.is_grounded(),
|
||||||
&game_settings,
|
&game_settings,
|
||||||
);
|
);
|
||||||
dbg!(knockback, angle);
|
|
||||||
let launch_angle = if facing_left {
|
let launch_angle = if angle_flip {
|
||||||
FGi32::lit("180.0") - angle
|
FGi32::lit("180.0") - launch_angle
|
||||||
} else {
|
} else {
|
||||||
angle
|
launch_angle
|
||||||
};
|
};
|
||||||
|
|
||||||
// Attacker hitstop and data
|
// Attacker hitstop and data
|
||||||
let hitstop = HitStop::calculate(FighterDamage(attack_damage), false);
|
let victim_hitstop_frames = HitStop::calculate(FighterDamage(attack_damage), crouch_canceling);
|
||||||
|
let attacker_hitstop_frames = HitStop::calculate(FighterDamage(attack_damage), false);
|
||||||
reactions.push((
|
reactions.push((
|
||||||
attacker_entity,
|
attacker_entity,
|
||||||
attacker_slot,
|
attacker_slot,
|
||||||
@@ -122,7 +132,8 @@ pub fn resolve_fighter_hits(
|
|||||||
launch_velocity: knockback.get_launch_speed(&game_settings),
|
launch_velocity: knockback.get_launch_speed(&game_settings),
|
||||||
launch_angle,
|
launch_angle,
|
||||||
hitstun_frames: knockback.get_hitstun_frames(&game_settings),
|
hitstun_frames: knockback.get_hitstun_frames(&game_settings),
|
||||||
hitstop_frames: hitstop,
|
victim_hitstop_frames,
|
||||||
|
attacker_hitstop_frames,
|
||||||
kind: HitReactionKind::Flinch,
|
kind: HitReactionKind::Flinch,
|
||||||
damage: FighterDamage(attack_damage),
|
damage: FighterDamage(attack_damage),
|
||||||
},
|
},
|
||||||
@@ -137,20 +148,20 @@ pub fn resolve_fighter_hits(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (attacker_entity, _, reaction) in &reactions {
|
for (attacker_entity, _, reaction) in &reactions {
|
||||||
let Ok((.., _, mut hitstop, mut hitstop_shake)) = attacker.get_mut(*attacker_entity)
|
let Ok((.., _, mut hitstop, mut hitstop_shake, _)) = attacker.get_mut(*attacker_entity)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if hitstop.remaining_frames < reaction.hitstop_frames.0 {
|
if hitstop.remaining_frames < reaction.attacker_hitstop_frames.0 {
|
||||||
let shake_kind = if receiver_state.is_visually_grounded() {
|
let shake_kind = if receiver_state.is_visually_grounded() {
|
||||||
FighterHitstopShakeKind::Horizontal
|
FighterHitstopShakeKind::Horizontal
|
||||||
} else {
|
} else {
|
||||||
FighterHitstopShakeKind::Vertical
|
FighterHitstopShakeKind::Vertical
|
||||||
};
|
};
|
||||||
hitstop.remaining_frames = reaction.hitstop_frames.0;
|
hitstop.remaining_frames = reaction.attacker_hitstop_frames.0;
|
||||||
let shake_frames = reaction
|
let shake_frames = reaction
|
||||||
.hitstop_frames
|
.attacker_hitstop_frames
|
||||||
.calculate_shake_frames(&game_settings);
|
.calculate_shake_frames(&game_settings);
|
||||||
hitstop_shake.total_frames = shake_frames;
|
hitstop_shake.total_frames = shake_frames;
|
||||||
hitstop_shake.remaining_frames = shake_frames;
|
hitstop_shake.remaining_frames = shake_frames;
|
||||||
@@ -173,7 +184,7 @@ pub fn resolve_fighter_hits(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for hit in hits {
|
for hit in hits {
|
||||||
let Ok((mut stale_move_queue, _facing_direction, mut hitboxes, ..)) =
|
let Ok((mut stale_move_queue, mut hitboxes, ..)) =
|
||||||
attacker.get_mut(hit.attacker)
|
attacker.get_mut(hit.attacker)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+18
-8
@@ -28,6 +28,7 @@ use crate::math::int::FGi32;
|
|||||||
use crate::math::vec::FGVec2;
|
use crate::math::vec::FGVec2;
|
||||||
use crate::math::vec3::FGVec3;
|
use crate::math::vec3::FGVec3;
|
||||||
use crate::player::Player;
|
use crate::player::Player;
|
||||||
|
use crate::scripting::FighterAttackScript;
|
||||||
|
|
||||||
pub fn debug_draw_ecb(
|
pub fn debug_draw_ecb(
|
||||||
ecbs: Query<(
|
ecbs: Query<(
|
||||||
@@ -368,6 +369,7 @@ pub fn attack_debug(
|
|||||||
)>,
|
)>,
|
||||||
mut gizmos: Gizmos,
|
mut gizmos: Gizmos,
|
||||||
mut filled_gizmos: FilledGizmos,
|
mut filled_gizmos: FilledGizmos,
|
||||||
|
attack_scripts: Res<Assets<FighterAttackScript>>
|
||||||
) {
|
) {
|
||||||
for (hitboxes, _matrices, _translation, _facing_direction, _frame) in query {
|
for (hitboxes, _matrices, _translation, _facing_direction, _frame) in query {
|
||||||
if let Some(active_attack) = &hitboxes.0 {
|
if let Some(active_attack) = &hitboxes.0 {
|
||||||
@@ -378,27 +380,35 @@ pub fn attack_debug(
|
|||||||
(solved_hitbox.capsule.start + solved_hitbox.capsule.end) * FGi32::lit("0.5");
|
(solved_hitbox.capsule.start + solved_hitbox.capsule.end) * FGi32::lit("0.5");
|
||||||
let length = (start - end).length();
|
let length = (start - end).length();
|
||||||
|
|
||||||
let attack_capsule = Capsule3d {
|
|
||||||
half_length: length * 0.5,
|
|
||||||
radius: solved_hitbox.capsule.radius.to_num(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let aim_dir = (end - start).normalize_or(Vec3::Y);
|
let aim_dir = (end - start).normalize_or(Vec3::Y);
|
||||||
|
|
||||||
let mut capsule_draw_trf = Transform::IDENTITY;
|
let mut capsule_draw_trf = Transform::IDENTITY;
|
||||||
capsule_draw_trf.translation = center.to_vec3();
|
capsule_draw_trf.translation = center.to_vec3();
|
||||||
capsule_draw_trf.align(Dir3::Y, aim_dir, Dir3::X, aim_dir.any_orthonormal_vector());
|
capsule_draw_trf.align(Dir3::Y, aim_dir, Dir3::X, aim_dir.any_orthonormal_vector());
|
||||||
|
|
||||||
|
let Some(script) = attack_scripts.get(&active_attack.script) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let hitbox_color = script.hitboxes.get(solved_hitbox.hitbox_idx).expect("This hitbox should be valid").get_debug_draw_color();
|
||||||
|
/*
|
||||||
|
let attack_capsule = Capsule3d {
|
||||||
|
half_length: length * 0.5,
|
||||||
|
radius: solved_hitbox.capsule.radius.to_num(),
|
||||||
|
};
|
||||||
gizmos.primitive_3d(
|
gizmos.primitive_3d(
|
||||||
&attack_capsule,
|
&attack_capsule,
|
||||||
capsule_draw_trf.to_isometry(),
|
capsule_draw_trf.to_isometry(),
|
||||||
bevy::color::palettes::css::RED,
|
hitbox_color,
|
||||||
);
|
);*/
|
||||||
|
|
||||||
|
let hitbox_inner_color = bevy::color::Srgba { alpha: 0.18, ..hitbox_color };
|
||||||
|
|
||||||
filled_gizmos.capsule(
|
filled_gizmos.capsule(
|
||||||
capsule_draw_trf.to_isometry(),
|
capsule_draw_trf.to_isometry(),
|
||||||
solved_hitbox.capsule.radius.to_num(),
|
solved_hitbox.capsule.radius.to_num(),
|
||||||
length * 0.5,
|
length * 0.5,
|
||||||
Color::srgba(1.0, 0.0, 0.0, 0.18),
|
hitbox_inner_color,
|
||||||
);
|
);
|
||||||
|
|
||||||
gizmos.cross(start, 1.0, bevy::color::palettes::css::GREEN);
|
gizmos.cross(start, 1.0, bevy::color::palettes::css::GREEN);
|
||||||
|
|||||||
+7
-1
@@ -293,10 +293,16 @@ pub struct FighterActiveAttack {
|
|||||||
pub script: Handle<FighterAttackScript>,
|
pub script: Handle<FighterAttackScript>,
|
||||||
pub active_hitboxes: Vec<usize>,
|
pub active_hitboxes: Vec<usize>,
|
||||||
pub solved_hitboxes: Vec<attack::FighterSolvedHitbox>,
|
pub solved_hitboxes: Vec<attack::FighterSolvedHitbox>,
|
||||||
pub hit_fighters: Vec<Entity>,
|
pub hit_groups: Vec<FighterHitGroupState>,
|
||||||
pub stalled: bool,
|
pub stalled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Hash)]
|
||||||
|
pub struct FighterHitGroupState {
|
||||||
|
pub group: u32,
|
||||||
|
pub hit_fighters: Vec<Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Component, Clone, Debug, Default, Hash)]
|
#[derive(Component, Clone, Debug, Default, Hash)]
|
||||||
pub struct FighterHitboxes(pub Option<FighterActiveAttack>);
|
pub struct FighterHitboxes(pub Option<FighterActiveAttack>);
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,42 @@
|
|||||||
use bevy::log::error;
|
use bevy::log::error;
|
||||||
|
|
||||||
use crate::fighter::{
|
use crate::fighter::{
|
||||||
attack::{AttackKind, FighterAttackScriptAssets, update_active_hitbox_list}, state::{
|
attack::{AttackKind, FighterAttackScriptAssets, update_active_attack}, state::{
|
||||||
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, fall::FallState,
|
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, air_attack, fall::FallState, jump,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash)]
|
#[derive(Clone, Debug, Hash)]
|
||||||
pub struct AirAttackState {
|
pub struct AirAttackState {
|
||||||
pub attack_kind: AttackKind,
|
pub attack_kind: AttackKind,
|
||||||
pub airborne_state_common: AirborneStateCommon
|
pub airborne_state_common: AirborneStateCommon,
|
||||||
|
pub is_in_autocancel_window: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FighterStateImpl for AirAttackState {
|
impl FighterStateImpl for AirAttackState {
|
||||||
const NAME: &'static str = "AirAttack";
|
const NAME: &'static str = "AirAttack";
|
||||||
|
|
||||||
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
|
||||||
|
let animation_frame = state_context.get_current_animation_frame();
|
||||||
|
let script = state_context
|
||||||
|
.attack_scripts
|
||||||
|
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
||||||
|
|
||||||
|
if let Some(script) = script {
|
||||||
|
if let Some(iasa_frame) = script.iasa_frame {
|
||||||
|
if animation_frame >= iasa_frame {
|
||||||
|
let new_state = jump::check_input_double_jump(state_context).or_else(|| air_attack::check_interrupt(state_context, &self.airborne_state_common));
|
||||||
|
if let Some(new_state) = new_state {
|
||||||
|
return Some(new_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if state_context.is_current_animation_finished() {
|
if state_context.is_current_animation_finished() {
|
||||||
Some(FighterState::Fall(FallState::new(self.airborne_state_common)))
|
Some(FighterState::Fall(FallState::new(
|
||||||
|
self.airborne_state_common,
|
||||||
|
)))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -29,16 +48,19 @@ impl FighterStateImpl for AirAttackState {
|
|||||||
.attack_scripts
|
.attack_scripts
|
||||||
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
||||||
if let Some(script) = script {
|
if let Some(script) = script {
|
||||||
update_active_hitbox_list(
|
update_active_attack(
|
||||||
script,
|
script,
|
||||||
&mut state_context
|
state_context
|
||||||
.hitboxes
|
.hitboxes
|
||||||
.0
|
.0
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.expect("ground attack must have active hitboxes")
|
.expect("ground attack must have active hitboxes"),
|
||||||
.active_hitboxes,
|
|
||||||
animation_frame,
|
animation_frame,
|
||||||
);
|
);
|
||||||
|
if let Some((autocancel_start, autocancel_end)) = script.wont_autocancel_window {
|
||||||
|
self.is_in_autocancel_window =
|
||||||
|
animation_frame < autocancel_start || animation_frame > autocancel_end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
air::update_standard_motion(state_context, &mut self.airborne_state_common);
|
air::update_standard_motion(state_context, &mut self.airborne_state_common);
|
||||||
@@ -55,7 +77,7 @@ impl FighterStateImpl for AirAttackState {
|
|||||||
script,
|
script,
|
||||||
active_hitboxes: Vec::new(),
|
active_hitboxes: Vec::new(),
|
||||||
solved_hitboxes: Vec::new(),
|
solved_hitboxes: Vec::new(),
|
||||||
hit_fighters: Vec::new(),
|
hit_groups: Vec::new(),
|
||||||
stalled: false,
|
stalled: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -68,11 +90,20 @@ impl FighterStateImpl for AirAttackState {
|
|||||||
&mut self,
|
&mut self,
|
||||||
state_context: &mut CollisionContext,
|
state_context: &mut CollisionContext,
|
||||||
) -> Option<FighterState> {
|
) -> Option<FighterState> {
|
||||||
air::collision_interrupt(
|
let coll_interrupt = air::collision_interrupt(
|
||||||
state_context,
|
state_context,
|
||||||
crate::fighter::state::land::LandingType::Normal,
|
crate::fighter::state::land::LandingType::Normal,
|
||||||
false,
|
false,
|
||||||
)
|
);
|
||||||
|
|
||||||
|
coll_interrupt.map(|state| {
|
||||||
|
if self.is_in_autocancel_window {
|
||||||
|
state
|
||||||
|
} else {
|
||||||
|
// TODO: Implement proper landing lag
|
||||||
|
state
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_visually_grounded<'a>(&'a self) -> bool {
|
fn is_visually_grounded<'a>(&'a self) -> bool {
|
||||||
@@ -83,25 +114,33 @@ impl FighterStateImpl for AirAttackState {
|
|||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum AirAttackStateCreateError {
|
pub enum AirAttackStateCreateError {
|
||||||
#[error("Attack script: {attack:?} not found")]
|
#[error("Attack script: {attack:?} not found")]
|
||||||
AttackScriptNotFound{attack: AttackKind},
|
AttackScriptNotFound { attack: AttackKind },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AirAttackState {
|
impl AirAttackState {
|
||||||
pub fn create(attack_kind: AttackKind, attack_script_assets: &FighterAttackScriptAssets, airborne_state_common: &AirborneStateCommon) -> Result<Self, AirAttackStateCreateError> {
|
pub fn create(
|
||||||
|
attack_kind: AttackKind,
|
||||||
|
attack_script_assets: &FighterAttackScriptAssets,
|
||||||
|
airborne_state_common: &AirborneStateCommon,
|
||||||
|
) -> Result<Self, AirAttackStateCreateError> {
|
||||||
if attack_script_assets.scripts.contains_key(&attack_kind) {
|
if attack_script_assets.scripts.contains_key(&attack_kind) {
|
||||||
Ok(AirAttackState {
|
Ok(AirAttackState {
|
||||||
airborne_state_common: airborne_state_common.clone(),
|
airborne_state_common: airborne_state_common.clone(),
|
||||||
attack_kind
|
attack_kind,
|
||||||
|
is_in_autocancel_window: false,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(AirAttackStateCreateError::AttackScriptNotFound { attack: attack_kind })
|
Err(AirAttackStateCreateError::AttackScriptNotFound {
|
||||||
|
attack: attack_kind,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn check_interrupt(
|
||||||
pub fn check_interrupt(state_context: &InterruptContext, airborne_state_common: &AirborneStateCommon) -> Option<FighterState> {
|
state_context: &InterruptContext,
|
||||||
|
airborne_state_common: &AirborneStateCommon,
|
||||||
|
) -> Option<FighterState> {
|
||||||
if state_context
|
if state_context
|
||||||
.input
|
.input
|
||||||
.has_command(crate::input::FighterCommands::Attack)
|
.has_command(crate::input::FighterCommands::Attack)
|
||||||
@@ -110,16 +149,18 @@ pub fn check_interrupt(state_context: &InterruptContext, airborne_state_common:
|
|||||||
.input
|
.input
|
||||||
.get_attack_command_type()
|
.get_attack_command_type()
|
||||||
.to_attack_kind_airborne(state_context.facing_direction);
|
.to_attack_kind_airborne(state_context.facing_direction);
|
||||||
let state = AirAttackState::create(attack_kind, state_context.attack_script_assets, airborne_state_common);
|
let state = AirAttackState::create(
|
||||||
|
attack_kind,
|
||||||
|
state_context.attack_script_assets,
|
||||||
|
airborne_state_common,
|
||||||
|
);
|
||||||
|
|
||||||
match state {
|
match state {
|
||||||
Ok(state) => {
|
Ok(state) => Some(FighterState::AirAttack(state)),
|
||||||
Some(FighterState::AirAttack(state))
|
|
||||||
},
|
|
||||||
Err(create_error) => {
|
Err(create_error) => {
|
||||||
error!("{}", create_error);
|
error!("{}", create_error);
|
||||||
None
|
None
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -148,6 +148,15 @@ impl FighterStateImpl for CrouchState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CrouchState {
|
||||||
|
pub fn is_crouch_cancelling(&self) -> bool {
|
||||||
|
match self.sub_state {
|
||||||
|
CrouchSubState::Entering { .. } | CrouchSubState::Idle => true,
|
||||||
|
CrouchSubState::Exiting => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_input(state_context: &InterruptContext) -> Option<FighterState> {
|
pub fn check_input(state_context: &InterruptContext) -> Option<FighterState> {
|
||||||
let input_frame = state_context.input.get_last_frame();
|
let input_frame = state_context.input.get_last_frame();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::fighter::{
|
use crate::fighter::{
|
||||||
attack::{AttackKind, update_active_hitbox_list},
|
attack::{AttackKind, update_active_attack},
|
||||||
state::{
|
state::{
|
||||||
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl,
|
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl,
|
||||||
InterruptContext, UpdateContext, wait::WaitState,
|
InterruptContext, UpdateContext, wait::WaitState,
|
||||||
@@ -28,14 +28,13 @@ impl FighterStateImpl for GroundAttackState {
|
|||||||
.attack_scripts
|
.attack_scripts
|
||||||
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
||||||
if let Some(script) = script {
|
if let Some(script) = script {
|
||||||
update_active_hitbox_list(
|
update_active_attack(
|
||||||
script,
|
script,
|
||||||
&mut state_context
|
state_context
|
||||||
.hitboxes
|
.hitboxes
|
||||||
.0
|
.0
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.expect("ground attack must have active hitboxes")
|
.expect("ground attack must have active hitboxes"),
|
||||||
.active_hitboxes,
|
|
||||||
animation_frame,
|
animation_frame,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -52,7 +51,7 @@ impl FighterStateImpl for GroundAttackState {
|
|||||||
script,
|
script,
|
||||||
active_hitboxes: Vec::new(),
|
active_hitboxes: Vec::new(),
|
||||||
solved_hitboxes: Vec::new(),
|
solved_hitboxes: Vec::new(),
|
||||||
hit_fighters: Vec::new(),
|
hit_groups: Vec::new(),
|
||||||
stalled: false,
|
stalled: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ pub struct InterruptContext<'a> {
|
|||||||
pub surface: &'a FighterSurfaceState,
|
pub surface: &'a FighterSurfaceState,
|
||||||
pub animation: AnimationStatus,
|
pub animation: AnimationStatus,
|
||||||
pub stage_collision: &'a StageCollision,
|
pub stage_collision: &'a StageCollision,
|
||||||
|
pub attack_scripts: &'a Assets<FighterAttackScript>,
|
||||||
pub attack_script_assets: &'a FighterAttackScriptAssets,
|
pub attack_script_assets: &'a FighterAttackScriptAssets,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,6 +301,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
baked_animations: &'a Assets<BakedFighterAnimations>,
|
baked_animations: &'a Assets<BakedFighterAnimations>,
|
||||||
attributes: &'a FighterAttributes,
|
attributes: &'a FighterAttributes,
|
||||||
stage_collision: &'a StageCollision,
|
stage_collision: &'a StageCollision,
|
||||||
|
attack_scripts: &'a Assets<FighterAttackScript>,
|
||||||
attack_script_assets: &'a FighterAttackScriptAssets,
|
attack_script_assets: &'a FighterAttackScriptAssets,
|
||||||
) -> InterruptContext<'a> {
|
) -> InterruptContext<'a> {
|
||||||
let animation = self.animation_status(animations, baked_animations);
|
let animation = self.animation_status(animations, baked_animations);
|
||||||
@@ -312,6 +314,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
|||||||
surface: &self.surface,
|
surface: &self.surface,
|
||||||
stage_collision,
|
stage_collision,
|
||||||
attack_script_assets: attack_script_assets,
|
attack_script_assets: attack_script_assets,
|
||||||
|
attack_scripts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,6 +474,7 @@ pub fn state_interrupt_system(
|
|||||||
game_settings: Res<GameSettings>,
|
game_settings: Res<GameSettings>,
|
||||||
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||||
fighter_manifests: Res<Assets<FighterManifest>>,
|
fighter_manifests: Res<Assets<FighterManifest>>,
|
||||||
|
attack_scripts: Res<Assets<FighterAttackScript>>,
|
||||||
stage_collision: Res<StageCollision>,
|
stage_collision: Res<StageCollision>,
|
||||||
) {
|
) {
|
||||||
for fighter in &mut query {
|
for fighter in &mut query {
|
||||||
@@ -490,9 +494,9 @@ pub fn state_interrupt_system(
|
|||||||
.expect("Fighter manifest should be valid");
|
.expect("Fighter manifest should be valid");
|
||||||
|
|
||||||
let new_state = if let Some(reaction) = pending_hit_reaction.take() {
|
let new_state = if let Some(reaction) = pending_hit_reaction.take() {
|
||||||
hitstop.remaining_frames = *reaction.hitstop_frames;
|
hitstop.remaining_frames = *reaction.victim_hitstop_frames;
|
||||||
let shake_frames = reaction
|
let shake_frames = reaction
|
||||||
.hitstop_frames
|
.victim_hitstop_frames
|
||||||
.calculate_shake_frames(&game_settings);
|
.calculate_shake_frames(&game_settings);
|
||||||
hitstop_shake.role = FighterHitstopShakeRole::Receiver;
|
hitstop_shake.role = FighterHitstopShakeRole::Receiver;
|
||||||
hitstop_shake.total_frames = shake_frames;
|
hitstop_shake.total_frames = shake_frames;
|
||||||
@@ -511,7 +515,7 @@ pub fn state_interrupt_system(
|
|||||||
|
|
||||||
if let hitstun::HitstopStateKind::Grounded = hitstop_kind {
|
if let hitstun::HitstopStateKind::Grounded = hitstop_kind {
|
||||||
let straight_up_angle = FGi32::lit("90.0");
|
let straight_up_angle = FGi32::lit("90.0");
|
||||||
if reaction.hitstop_frames
|
if reaction.victim_hitstop_frames
|
||||||
>= HitStop::new(game_settings.fighter_common.tumble_bounce_hitstop_threshold)
|
>= HitStop::new(game_settings.fighter_common.tumble_bounce_hitstop_threshold)
|
||||||
{
|
{
|
||||||
hitstop_kind = hitstun::HitstopStateKind::Airborne;
|
hitstop_kind = hitstun::HitstopStateKind::Airborne;
|
||||||
@@ -547,6 +551,7 @@ pub fn state_interrupt_system(
|
|||||||
&baked_animations,
|
&baked_animations,
|
||||||
&manifest.attributes,
|
&manifest.attributes,
|
||||||
&stage_collision,
|
&stage_collision,
|
||||||
|
&attack_scripts,
|
||||||
attack_script_assets,
|
attack_script_assets,
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
|
|||||||
+441
-108
@@ -1,28 +1,30 @@
|
|||||||
//! Standalone, read-only attack-script previewer used by `hitbox-editor`.
|
//! Standalone, read-only attack-script previewer used by `hitbox-editor`.
|
||||||
|
|
||||||
use bevy::{gltf::Gltf, prelude::*, world_serialization::WorldAssetRoot};
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
camera::ScalingMode,
|
||||||
|
dev_tools::infinite_grid::{InfiniteGrid, InfiniteGridPlugin, InfiniteGridSettings},
|
||||||
|
gltf::Gltf,
|
||||||
|
prelude::*,
|
||||||
|
world_serialization::WorldAssetRoot,
|
||||||
|
};
|
||||||
use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui};
|
use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui};
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
|
use egui_extras::Column;
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
GameAssetPlugin,
|
GameAssetPlugin, egui_color_convert::ColorToEgui, fighter::{
|
||||||
fighter::{
|
animation::FighterAnimationFrame, attack::{AttackHitbox, AttackKind, HitStop}, baked_animation::{
|
||||||
animation::FighterAnimationFrame,
|
|
||||||
attack::{AttackHitbox, AttackKind},
|
|
||||||
baked_animation::{
|
|
||||||
BakedAnimationAssetProcessingPlugin, BakedAnimationSamplingSet,
|
BakedAnimationAssetProcessingPlugin, BakedAnimationSamplingSet,
|
||||||
BakedAnimationVisualPlugin, BakedFighterAnimations, FighterBoneIndexMap,
|
BakedAnimationVisualPlugin, BakedFighterAnimations, FighterBoneIndexMap,
|
||||||
FighterBoneMatrices, FixedMat4,
|
FighterBoneMatrices, FixedMat4,
|
||||||
},
|
}, manifest::{FighterHurtbox, FighterManifest}, visual::FighterAnimations,
|
||||||
manifest::{FighterHurtbox, FighterManifest},
|
}, filled_gizmos::{FilledGizmos, FilledGizmosPlugin}, game_settings::GameSettings, math::{
|
||||||
visual::FighterAnimations,
|
|
||||||
},
|
|
||||||
filled_gizmos::{FilledGizmos, FilledGizmosPlugin},
|
|
||||||
math::{
|
|
||||||
int::{FGi32, FGi32Ext},
|
int::{FGi32, FGi32Ext},
|
||||||
vec3::FGVec3,
|
vec3::FGVec3,
|
||||||
},
|
}, scripting::{FighterAttackScript, importer::FighterScriptImportPlugin},
|
||||||
scripting::{FighterAttackScript, importer::FighterScriptImportPlugin},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||||
@@ -67,7 +69,7 @@ pub struct HitboxViewerArgs {
|
|||||||
#[arg(long, default_value = "fighters/test/test.fighter.ron")]
|
#[arg(long, default_value = "fighters/test/test.fighter.ron")]
|
||||||
pub manifest: String,
|
pub manifest: String,
|
||||||
/// Attack script and matching animation to preview.
|
/// Attack script and matching animation to preview.
|
||||||
#[arg(long, value_enum, default_value_t = ViewerAttack::Jab)]
|
#[arg(long, value_enum, default_value_t = ViewerAttack::NeutralAir)]
|
||||||
pub attack: ViewerAttack,
|
pub attack: ViewerAttack,
|
||||||
/// Start with fighter hurtboxes hidden.
|
/// Start with fighter hurtboxes hidden.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -76,10 +78,14 @@ pub struct HitboxViewerArgs {
|
|||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct ViewerAssets {
|
struct ViewerAssets {
|
||||||
|
settings: Handle<GameSettings>,
|
||||||
manifest: Handle<FighterManifest>,
|
manifest: Handle<FighterManifest>,
|
||||||
|
manifest_path: String,
|
||||||
|
manifest_choices: Vec<(String, Handle<FighterManifest>)>,
|
||||||
model: Option<Handle<Gltf>>,
|
model: Option<Handle<Gltf>>,
|
||||||
baked: Option<Handle<BakedFighterAnimations>>,
|
baked: Option<Handle<BakedFighterAnimations>>,
|
||||||
script: Option<Handle<FighterAttackScript>>,
|
script: Option<Handle<FighterAttackScript>>,
|
||||||
|
script_attack: Option<AttackKind>,
|
||||||
fighter: Option<Entity>,
|
fighter: Option<Entity>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -91,33 +97,57 @@ struct ViewerState {
|
|||||||
playing: bool,
|
playing: bool,
|
||||||
show_hurtboxes: bool,
|
show_hurtboxes: bool,
|
||||||
elapsed: f32,
|
elapsed: f32,
|
||||||
|
manifest_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VIEWER_ATTACKS: [AttackKind; 12] = [
|
||||||
|
AttackKind::NeutralAir,
|
||||||
|
AttackKind::ForwardAir,
|
||||||
|
AttackKind::BackAir,
|
||||||
|
AttackKind::DownAir,
|
||||||
|
AttackKind::UpAir,
|
||||||
|
AttackKind::Jab,
|
||||||
|
AttackKind::UpTilt,
|
||||||
|
AttackKind::DownTilt,
|
||||||
|
AttackKind::ForwardTilt,
|
||||||
|
AttackKind::UpSmash,
|
||||||
|
AttackKind::DownSmash,
|
||||||
|
AttackKind::ForwardSmash,
|
||||||
|
];
|
||||||
|
|
||||||
pub struct HitboxViewerPlugin;
|
pub struct HitboxViewerPlugin;
|
||||||
|
|
||||||
impl Plugin for HitboxViewerPlugin {
|
impl Plugin for HitboxViewerPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
|
let (attack, show_hurtboxes, manifest_path) = {
|
||||||
let args = app.world().resource::<HitboxViewerArgs>();
|
let args = app.world().resource::<HitboxViewerArgs>();
|
||||||
let attack = args.attack.into();
|
(
|
||||||
let show_hurtboxes = !args.hide_hurtboxes;
|
args.attack.into(),
|
||||||
|
!args.hide_hurtboxes,
|
||||||
|
args.manifest.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
app.add_plugins((
|
app.add_plugins((
|
||||||
GameAssetPlugin,
|
GameAssetPlugin,
|
||||||
FighterScriptImportPlugin,
|
FighterScriptImportPlugin,
|
||||||
BakedAnimationAssetProcessingPlugin,
|
BakedAnimationAssetProcessingPlugin,
|
||||||
BakedAnimationVisualPlugin,
|
BakedAnimationVisualPlugin,
|
||||||
FilledGizmosPlugin,
|
FilledGizmosPlugin,
|
||||||
|
InfiniteGridPlugin,
|
||||||
))
|
))
|
||||||
.insert_resource(ViewerState {
|
.insert_resource(ViewerState {
|
||||||
attack,
|
attack,
|
||||||
frame: 0,
|
frame: 0,
|
||||||
playing: true,
|
playing: false,
|
||||||
show_hurtboxes,
|
show_hurtboxes,
|
||||||
elapsed: 0.0,
|
elapsed: 0.0,
|
||||||
|
manifest_path,
|
||||||
})
|
})
|
||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
FixedUpdate,
|
FixedUpdate,
|
||||||
(
|
(
|
||||||
|
apply_viewer_selection,
|
||||||
request_fighter_dependencies,
|
request_fighter_dependencies,
|
||||||
spawn_preview_fighter,
|
spawn_preview_fighter,
|
||||||
handle_keyboard,
|
handle_keyboard,
|
||||||
@@ -133,17 +163,28 @@ impl Plugin for HitboxViewerPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<HitboxViewerArgs>) {
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<HitboxViewerArgs>) {
|
||||||
|
let manifest_path = args.manifest.clone();
|
||||||
commands.insert_resource(ViewerAssets {
|
commands.insert_resource(ViewerAssets {
|
||||||
manifest: asset_server.load(args.manifest.clone()),
|
settings: asset_server.load("game_settings.ron"),
|
||||||
|
manifest: asset_server.load(manifest_path.clone()),
|
||||||
|
manifest_path: manifest_path.clone(),
|
||||||
|
manifest_choices: vec![(manifest_path.clone(), asset_server.load(manifest_path))],
|
||||||
model: None,
|
model: None,
|
||||||
baked: None,
|
baked: None,
|
||||||
script: None,
|
script: None,
|
||||||
|
script_attack: None,
|
||||||
fighter: None,
|
fighter: None,
|
||||||
error: None,
|
error: None,
|
||||||
});
|
});
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Name::new("Hitbox viewer camera"),
|
Name::new("Hitbox viewer camera"),
|
||||||
Camera3d::default(),
|
Camera3d::default(),
|
||||||
|
Projection::from(OrthographicProjection {
|
||||||
|
scaling_mode: ScalingMode::FixedVertical {
|
||||||
|
viewport_height: 60.0,
|
||||||
|
},
|
||||||
|
..OrthographicProjection::default_3d()
|
||||||
|
}),
|
||||||
Transform::from_xyz(0.0, 9.0, 38.0).looking_at(Vec3::new(0.0, 8.0, 0.0), Vec3::Y),
|
Transform::from_xyz(0.0, 9.0, 38.0).looking_at(Vec3::new(0.0, 8.0, 0.0), Vec3::Y),
|
||||||
));
|
));
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
@@ -160,30 +201,102 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Hitbo
|
|||||||
brightness: 250.0,
|
brightness: 250.0,
|
||||||
affects_lightmapped_meshes: true,
|
affects_lightmapped_meshes: true,
|
||||||
});
|
});
|
||||||
|
let mut trf = Transform::IDENTITY;
|
||||||
|
trf.rotate_local_x(PI * 0.5);
|
||||||
|
commands.spawn((InfiniteGrid, InfiniteGridSettings { ..default() }, trf));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_viewer_selection(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
|
mut viewer: ResMut<ViewerAssets>,
|
||||||
|
mut state: ResMut<ViewerState>,
|
||||||
|
) {
|
||||||
|
if viewer.manifest_path == state.manifest_path {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
viewer.manifest_path = state.manifest_path.clone();
|
||||||
|
viewer.manifest = asset_server.load(viewer.manifest_path.clone());
|
||||||
|
viewer.model = None;
|
||||||
|
viewer.baked = None;
|
||||||
|
viewer.script = None;
|
||||||
|
viewer.script_attack = None;
|
||||||
|
viewer.error = None;
|
||||||
|
if let Some(fighter) = viewer.fighter.take() {
|
||||||
|
commands.entity(fighter).despawn();
|
||||||
|
}
|
||||||
|
state.frame = 0;
|
||||||
|
state.elapsed = 0.0;
|
||||||
|
state.playing = false;
|
||||||
|
if let Some(manifest) = manifests.get(&viewer.manifest) {
|
||||||
|
if !manifest.attack_scripts.contains_key(&state.attack) {
|
||||||
|
if let Some(attack) = VIEWER_ATTACKS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|attack| manifest.attack_scripts.contains_key(attack))
|
||||||
|
{
|
||||||
|
state.attack = attack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_fighter_dependencies(
|
fn request_fighter_dependencies(
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
|
settings: Res<Assets<GameSettings>>,
|
||||||
manifests: Res<Assets<FighterManifest>>,
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
mut viewer: ResMut<ViewerAssets>,
|
mut viewer: ResMut<ViewerAssets>,
|
||||||
state: Res<ViewerState>,
|
mut state: ResMut<ViewerState>,
|
||||||
) {
|
) {
|
||||||
if viewer.model.is_some() {
|
if let Some(settings) = settings.get(&viewer.settings) {
|
||||||
return;
|
for path in &settings.fighter_manifest_paths {
|
||||||
|
if !viewer
|
||||||
|
.manifest_choices
|
||||||
|
.iter()
|
||||||
|
.any(|(known, _)| known == path)
|
||||||
|
{
|
||||||
|
viewer
|
||||||
|
.manifest_choices
|
||||||
|
.push((path.clone(), asset_server.load(path.clone())));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let Some(manifest) = manifests.get(&viewer.manifest) else {
|
let Some(manifest) = manifests.get(&viewer.manifest) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(script_path) = manifest.attack_scripts.get(&state.attack) else {
|
let Some(script_path) = manifest.attack_scripts.get(&state.attack) else {
|
||||||
|
if let Some(attack) = VIEWER_ATTACKS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|attack| manifest.attack_scripts.contains_key(attack))
|
||||||
|
{
|
||||||
|
state.attack = attack;
|
||||||
|
return;
|
||||||
|
}
|
||||||
viewer.error = Some(format!(
|
viewer.error = Some(format!(
|
||||||
"{:?} has no {:?} attack script",
|
"{:?} has no {:?} attack script",
|
||||||
manifest.id, state.attack
|
manifest.id, state.attack
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if viewer.script_attack != Some(state.attack) {
|
||||||
|
viewer.script = None;
|
||||||
|
viewer.script_attack = None;
|
||||||
|
state.frame = 0;
|
||||||
|
state.elapsed = 0.0;
|
||||||
|
state.playing = false;
|
||||||
|
}
|
||||||
|
if viewer.model.is_none() {
|
||||||
viewer.model = Some(asset_server.load(manifest.model_path.clone()));
|
viewer.model = Some(asset_server.load(manifest.model_path.clone()));
|
||||||
|
}
|
||||||
|
if viewer.baked.is_none() {
|
||||||
viewer.baked = Some(asset_server.load(manifest.baked_animation_path.clone()));
|
viewer.baked = Some(asset_server.load(manifest.baked_animation_path.clone()));
|
||||||
|
}
|
||||||
|
if viewer.script.is_none() {
|
||||||
viewer.script = Some(asset_server.load(script_path.clone()));
|
viewer.script = Some(asset_server.load(script_path.clone()));
|
||||||
|
viewer.script_attack = Some(state.attack);
|
||||||
|
}
|
||||||
viewer.error = None;
|
viewer.error = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +462,6 @@ fn draw_preview(
|
|||||||
scripts: Res<Assets<FighterAttackScript>>,
|
scripts: Res<Assets<FighterAttackScript>>,
|
||||||
baked_assets: Res<Assets<BakedFighterAnimations>>,
|
baked_assets: Res<Assets<BakedFighterAnimations>>,
|
||||||
manifests: Res<Assets<FighterManifest>>,
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
mut gizmos: Gizmos,
|
|
||||||
mut filled: FilledGizmos,
|
mut filled: FilledGizmos,
|
||||||
) {
|
) {
|
||||||
let (Some(script_handle), Some(baked_handle)) = (&viewer.script, &viewer.baked) else {
|
let (Some(script_handle), Some(baked_handle)) = (&viewer.script, &viewer.baked) else {
|
||||||
@@ -398,7 +510,6 @@ fn draw_preview(
|
|||||||
&previous_animation,
|
&previous_animation,
|
||||||
manifest.model_scale,
|
manifest.model_scale,
|
||||||
fighter_transform,
|
fighter_transform,
|
||||||
&mut gizmos,
|
|
||||||
&mut filled,
|
&mut filled,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -411,7 +522,6 @@ fn draw_hitbox(
|
|||||||
previous_animation: &FighterAnimationFrame,
|
previous_animation: &FighterAnimationFrame,
|
||||||
model_scale: FGi32,
|
model_scale: FGi32,
|
||||||
fighter_transform: FixedMat4,
|
fighter_transform: FixedMat4,
|
||||||
gizmos: &mut Gizmos,
|
|
||||||
filled: &mut FilledGizmos,
|
filled: &mut FilledGizmos,
|
||||||
) {
|
) {
|
||||||
let Some(current_bone) =
|
let Some(current_bone) =
|
||||||
@@ -451,13 +561,16 @@ fn draw_hitbox(
|
|||||||
transform.to_isometry(),
|
transform.to_isometry(),
|
||||||
radius,
|
radius,
|
||||||
length * 0.5,
|
length * 0.5,
|
||||||
Color::srgba(1.0, 0.1, 0.05, 0.32),
|
bevy::color::Srgba {
|
||||||
|
alpha: 0.32,
|
||||||
|
..hitbox.get_debug_draw_color()
|
||||||
|
},
|
||||||
);
|
);
|
||||||
gizmos.primitive_3d(
|
/*gizmos.primitive_3d(
|
||||||
&Capsule3d::new(radius, length),
|
&Capsule3d::new(radius, length),
|
||||||
transform.to_isometry(),
|
transform.to_isometry(),
|
||||||
Color::srgb(1.0, 0.15, 0.1),
|
Color::srgb(1.0, 0.15, 0.1),
|
||||||
);
|
);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_hurtbox(
|
fn draw_hurtbox(
|
||||||
@@ -487,10 +600,204 @@ fn draw_hurtbox(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_summary_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
script: &FighterAttackScript,
|
||||||
|
end: u32,
|
||||||
|
) {
|
||||||
|
egui::Grid::new("Summary Grid")
|
||||||
|
.num_columns(4)
|
||||||
|
.striped(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.label("First active");
|
||||||
|
ui.label("Last active");
|
||||||
|
ui.label("Total");
|
||||||
|
ui.label("IASA");
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label(
|
||||||
|
script
|
||||||
|
.hitboxes
|
||||||
|
.iter()
|
||||||
|
.map(|hb| hb.start_frame)
|
||||||
|
.min()
|
||||||
|
.map(|frame| format!("{}", frame))
|
||||||
|
.unwrap_or("-".to_owned()),
|
||||||
|
);
|
||||||
|
ui.label(
|
||||||
|
script
|
||||||
|
.hitboxes
|
||||||
|
.iter()
|
||||||
|
.map(|hb| hb.end_frame)
|
||||||
|
.max()
|
||||||
|
.map(|frame| format!("{}", frame - 1))
|
||||||
|
.unwrap_or("-".to_owned()),
|
||||||
|
);
|
||||||
|
ui.label(format!("{}", end));
|
||||||
|
let iasa_frame_label = if let Some(iasa_frame) = script.iasa_frame {
|
||||||
|
format!("{}", iasa_frame)
|
||||||
|
} else {
|
||||||
|
"-".to_owned()
|
||||||
|
};
|
||||||
|
ui.label(iasa_frame_label);
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Landing lag");
|
||||||
|
ui.label("Autocancel");
|
||||||
|
ui.label("Damage");
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
let damages = script
|
||||||
|
.hitboxes
|
||||||
|
.iter()
|
||||||
|
.map(|hb| hb.damage)
|
||||||
|
.sorted()
|
||||||
|
.dedup()
|
||||||
|
.map(|d| format!("{}", d))
|
||||||
|
.join(" / ");
|
||||||
|
|
||||||
|
ui.label(
|
||||||
|
script
|
||||||
|
.landing_duration
|
||||||
|
.map(|duration| format!("{}", duration))
|
||||||
|
.unwrap_or("-".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let autocancel_text =
|
||||||
|
if let Some(wont_autocancel_window) = script.wont_autocancel_window {
|
||||||
|
let autocancel_start = if wont_autocancel_window.0 > 0 {
|
||||||
|
format!("{}", wont_autocancel_window.0)
|
||||||
|
} else {
|
||||||
|
"-".to_string()
|
||||||
|
};
|
||||||
|
let autocancel_end = if wont_autocancel_window.1 < end {
|
||||||
|
format!("{}", wont_autocancel_window.1)
|
||||||
|
} else {
|
||||||
|
"-".to_string()
|
||||||
|
};
|
||||||
|
format!("<{} {}>", autocancel_start, autocancel_end)
|
||||||
|
} else {
|
||||||
|
"-".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.label(autocancel_text);
|
||||||
|
ui.label(format!("{} %", damages));
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
ui.label("Base knockback");
|
||||||
|
ui.label("Knockback Growth");
|
||||||
|
ui.label("Set knockback");
|
||||||
|
ui.label("Angle");
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
let base_knockbacks = script
|
||||||
|
.hitboxes
|
||||||
|
.iter()
|
||||||
|
.map(|hb| hb.knockback)
|
||||||
|
.sorted()
|
||||||
|
.dedup()
|
||||||
|
.map(|d| format!("{}", d))
|
||||||
|
.join(" / ");
|
||||||
|
ui.label(base_knockbacks);
|
||||||
|
|
||||||
|
let knockback_growths = script
|
||||||
|
.hitboxes
|
||||||
|
.iter()
|
||||||
|
.map(|hb| hb.knockback_growth)
|
||||||
|
.sorted()
|
||||||
|
.dedup()
|
||||||
|
.map(|d| format!("{}", d))
|
||||||
|
.join(" / ");
|
||||||
|
ui.label(knockback_growths);
|
||||||
|
ui.label("-");
|
||||||
|
|
||||||
|
let angles = script
|
||||||
|
.hitboxes
|
||||||
|
.iter()
|
||||||
|
.map(|hb| hb.angle.to_melee_angle_representation())
|
||||||
|
.sorted()
|
||||||
|
.dedup()
|
||||||
|
.map(|d| {
|
||||||
|
format!("{}", d)
|
||||||
|
})
|
||||||
|
.join(" / ");
|
||||||
|
ui.label(angles);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
const COLS: [&str; 8] = [
|
||||||
|
"Hitbox",
|
||||||
|
"Damage",
|
||||||
|
"Angle",
|
||||||
|
"Base Knockback",
|
||||||
|
"Knockback Growth",
|
||||||
|
"Set Knockback",
|
||||||
|
"Hitstop Attacker",
|
||||||
|
"Hitstop Defender",
|
||||||
|
];
|
||||||
|
|
||||||
|
let table_builder = egui_extras::TableBuilder::new(ui)
|
||||||
|
.striped(true)
|
||||||
|
.resizable(true)
|
||||||
|
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||||
|
.columns(Column::remainder(), COLS.len());
|
||||||
|
|
||||||
|
table_builder.header(20.0, |mut header| {
|
||||||
|
for col in COLS {
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(col);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).body(|body| {
|
||||||
|
body.rows(18.0, script.hitboxes.len(), |mut row| {
|
||||||
|
let hitbox = &script.hitboxes[row.index()];
|
||||||
|
let hitbox_color = hitbox.get_debug_draw_color().to_egui();
|
||||||
|
row.col(|ui| {
|
||||||
|
let (response, painter) = ui.allocate_painter(egui::Vec2::new(16.0, 16.0), egui::Sense::empty());
|
||||||
|
painter.rect_filled(
|
||||||
|
response.rect,
|
||||||
|
2.0,
|
||||||
|
hitbox_color,
|
||||||
|
);
|
||||||
|
ui.label(format!("ID {}", hitbox.id));
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(format!("{} %", hitbox.damage));
|
||||||
|
});
|
||||||
|
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(format!("{}", hitbox.angle));
|
||||||
|
});
|
||||||
|
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(format!("{}", hitbox.knockback));
|
||||||
|
});
|
||||||
|
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(format!("{}", hitbox.knockback_growth));
|
||||||
|
});
|
||||||
|
// TODO: Set knockback
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label("-");
|
||||||
|
});
|
||||||
|
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(format!("{}", HitStop::calculate(hitbox.damage, false)));
|
||||||
|
});
|
||||||
|
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(format!("{} ({} crouched)", HitStop::calculate(hitbox.damage, false), HitStop::calculate(hitbox.damage, true)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_ui(
|
fn draw_ui(
|
||||||
mut contexts: EguiContexts,
|
mut contexts: EguiContexts,
|
||||||
mut state: ResMut<ViewerState>,
|
mut state: ResMut<ViewerState>,
|
||||||
viewer: Res<ViewerAssets>,
|
viewer: Res<ViewerAssets>,
|
||||||
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
scripts: Res<Assets<FighterAttackScript>>,
|
scripts: Res<Assets<FighterAttackScript>>,
|
||||||
baked_assets: Res<Assets<BakedFighterAnimations>>,
|
baked_assets: Res<Assets<BakedFighterAnimations>>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
@@ -499,10 +806,62 @@ fn draw_ui(
|
|||||||
.script
|
.script
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|handle| scripts.get(handle));
|
.and_then(|handle| scripts.get(handle));
|
||||||
|
egui::Window::new("Attack")
|
||||||
|
.anchor(egui::Align2::RIGHT_TOP, egui::vec2(-10.0, 10.0))
|
||||||
|
.resizable(true)
|
||||||
|
.show(contexts.ctx_mut()?, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
egui::ComboBox::from_id_salt("viewer-fighter")
|
||||||
|
.selected_text(
|
||||||
|
manifests
|
||||||
|
.get(&viewer.manifest)
|
||||||
|
.map(|manifest| format!("{:?}", manifest.id))
|
||||||
|
.unwrap_or_else(|| state.manifest_path.clone()),
|
||||||
|
)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for (path, handle) in &viewer.manifest_choices {
|
||||||
|
let label = manifests
|
||||||
|
.get(handle)
|
||||||
|
.map(|manifest| format!("{:?}", manifest.id))
|
||||||
|
.unwrap_or_else(|| path.clone());
|
||||||
|
if ui
|
||||||
|
.selectable_label(state.manifest_path == *path, label)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
state.manifest_path = path.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
egui::ComboBox::from_id_salt("viewer-attack")
|
||||||
|
.selected_text(format!("{:?}", state.attack))
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
if let Some(manifest) = manifests.get(&viewer.manifest) {
|
||||||
|
for attack in VIEWER_ATTACKS {
|
||||||
|
if manifest.attack_scripts.contains_key(&attack) {
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut state.attack,
|
||||||
|
attack,
|
||||||
|
format!("{:?}", attack),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
let Some(script) = script else {
|
||||||
|
ui.spinner();
|
||||||
|
ui.label("Waiting for the processed attack script…");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
draw_summary_ui(ui, script, end);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
egui::Window::new("Timeline")
|
egui::Window::new("Timeline")
|
||||||
.anchor(egui::Align2::CENTER_BOTTOM, egui::vec2(0.0, -10.0))
|
.anchor(egui::Align2::CENTER_BOTTOM, egui::vec2(0.0, -10.0))
|
||||||
.resizable(true)
|
.resizable(false)
|
||||||
.default_size(egui::vec2(780.0, 190.0))
|
.default_size(egui::vec2(780.0, 250.0))
|
||||||
.show(contexts.ctx_mut()?, |ui| {
|
.show(contexts.ctx_mut()?, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui
|
if ui
|
||||||
@@ -521,8 +880,6 @@ fn draw_ui(
|
|||||||
}
|
}
|
||||||
ui.label(format!("Frame {} / {}", state.frame, end.saturating_sub(1)));
|
ui.label(format!("Frame {} / {}", state.frame, end.saturating_sub(1)));
|
||||||
ui.checkbox(&mut state.show_hurtboxes, "Hurtboxes");
|
ui.checkbox(&mut state.show_hurtboxes, "Hurtboxes");
|
||||||
ui.separator();
|
|
||||||
ui.label(format!("{:?}", state.attack));
|
|
||||||
});
|
});
|
||||||
if let Some(error) = &viewer.error {
|
if let Some(error) = &viewer.error {
|
||||||
ui.colored_label(egui::Color32::RED, error);
|
ui.colored_label(egui::Color32::RED, error);
|
||||||
@@ -532,82 +889,12 @@ fn draw_ui(
|
|||||||
ui.label("Waiting for the processed attack script…");
|
ui.label("Waiting for the processed attack script…");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
draw_summary_timeline(ui, &mut state, script, end);
|
//draw_summary_timeline(ui, &mut state, script, end);
|
||||||
draw_timeline(ui, &mut state, script, end);
|
draw_timeline(ui, &mut state, script, end);
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_summary_timeline(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
state: &mut ViewerState,
|
|
||||||
script: &FighterAttackScript,
|
|
||||||
end: u32,
|
|
||||||
) {
|
|
||||||
let row_height = 20.0;
|
|
||||||
let label_width = 55.0;
|
|
||||||
let height = (row_height + 25.0f32).min(ui.available_height());
|
|
||||||
let (response, painter) = ui.allocate_painter(
|
|
||||||
egui::vec2(ui.available_width(), height),
|
|
||||||
egui::Sense::click_and_drag(),
|
|
||||||
);
|
|
||||||
let rect = response.rect;
|
|
||||||
let timeline = egui::Rect::from_min_max(
|
|
||||||
egui::pos2(rect.left() + label_width, rect.top()),
|
|
||||||
rect.right_bottom(),
|
|
||||||
);
|
|
||||||
if (response.clicked() || response.dragged())
|
|
||||||
&& let Some(pointer) = response.interact_pointer_pos()
|
|
||||||
&& timeline.contains(pointer)
|
|
||||||
{
|
|
||||||
let fraction = ((pointer.x - timeline.left()) / timeline.width()).clamp(0.0, 1.0);
|
|
||||||
state.frame = ((end.saturating_sub(1)) as f32 * fraction).round() as u32;
|
|
||||||
state.playing = false;
|
|
||||||
}
|
|
||||||
for frame in 0..end {
|
|
||||||
if frame % 5 != 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let x = frame_x(timeline, frame, end);
|
|
||||||
painter.line_segment(
|
|
||||||
[egui::pos2(x, rect.top()), egui::pos2(x, rect.bottom())],
|
|
||||||
egui::Stroke::new(1.0_f32, egui::Color32::from_gray(55)),
|
|
||||||
);
|
|
||||||
painter.text(
|
|
||||||
egui::pos2(x + 2.0, rect.top()),
|
|
||||||
egui::Align2::LEFT_TOP,
|
|
||||||
frame.to_string(),
|
|
||||||
egui::FontId::monospace(10.0),
|
|
||||||
egui::Color32::GRAY,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for frame in 0..end {
|
|
||||||
let top = rect.top() + 20.0 as f32;
|
|
||||||
painter.text(
|
|
||||||
egui::pos2(rect.left(), top + 2.0),
|
|
||||||
egui::Align2::LEFT_TOP,
|
|
||||||
format!("HB {}", 2),
|
|
||||||
egui::FontId::monospace(12.0),
|
|
||||||
egui::Color32::LIGHT_GRAY,
|
|
||||||
);
|
|
||||||
let active_hitbox = script
|
|
||||||
.hitboxes
|
|
||||||
.iter()
|
|
||||||
.filter(|hitbox| hitbox.start_frame <= frame && hitbox.end_frame > frame)
|
|
||||||
.max_by(|a, b| a.id.cmp(&b.id));
|
|
||||||
let bar = egui::Rect::from_min_max(
|
|
||||||
egui::pos2(frame_x(timeline, frame, end), top + 2.0),
|
|
||||||
egui::pos2(frame_x(timeline, frame + 1, end), top + row_height - 2.0),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(_active_hitbox) = active_hitbox {
|
|
||||||
painter.rect_filled(bar, 2.0, egui::Color32::from_rgb(245, 75, 55));
|
|
||||||
} else {
|
|
||||||
painter.rect_stroke(bar, 2.0, egui::Stroke::new(2.0f32, egui::Color32::from_rgb(75, 75, 75)), egui::StrokeKind::Inside);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_timeline(
|
fn draw_timeline(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
state: &mut ViewerState,
|
state: &mut ViewerState,
|
||||||
@@ -616,8 +903,9 @@ fn draw_timeline(
|
|||||||
) {
|
) {
|
||||||
let row_height = 20.0;
|
let row_height = 20.0;
|
||||||
let label_width = 55.0;
|
let label_width = 55.0;
|
||||||
|
// +1 for the summary layer
|
||||||
let height =
|
let height =
|
||||||
(script.hitboxes.len().max(1) as f32 * row_height + 25.0).min(ui.available_height());
|
((script.hitboxes.len().max(1) + 1) as f32 * row_height + 25.0).min(ui.available_height());
|
||||||
let (response, painter) = ui.allocate_painter(
|
let (response, painter) = ui.allocate_painter(
|
||||||
egui::vec2(ui.available_width(), height),
|
egui::vec2(ui.available_width(), height),
|
||||||
egui::Sense::click_and_drag(),
|
egui::Sense::click_and_drag(),
|
||||||
@@ -652,12 +940,56 @@ fn draw_timeline(
|
|||||||
egui::Color32::GRAY,
|
egui::Color32::GRAY,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw summary row
|
||||||
|
for frame in 0..end {
|
||||||
|
let top = rect.top() + 20.0 as f32;
|
||||||
|
let active_hitbox = script
|
||||||
|
.hitboxes
|
||||||
|
.iter()
|
||||||
|
.filter(|hitbox| hitbox.start_frame <= frame && hitbox.end_frame > frame)
|
||||||
|
.max_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
let bar = egui::Rect::from_min_max(
|
||||||
|
egui::pos2(frame_x(timeline, frame, end), top + 2.0),
|
||||||
|
egui::pos2(frame_x(timeline, frame + 1, end), top + row_height - 2.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(active_hitbox) = active_hitbox {
|
||||||
|
painter.rect_filled(bar, 2.0, active_hitbox.get_debug_draw_color().to_egui());
|
||||||
|
}
|
||||||
|
|
||||||
|
let stroke_color = if let Some(iasa_frame) = script.iasa_frame
|
||||||
|
&& frame == iasa_frame
|
||||||
|
{
|
||||||
|
Some(bevy::color::palettes::tailwind::CYAN_300)
|
||||||
|
} else if let Some(wont_autocancel_window) = script.wont_autocancel_window
|
||||||
|
&& (frame < wont_autocancel_window.0 || frame > wont_autocancel_window.1)
|
||||||
|
{
|
||||||
|
Some(bevy::color::palettes::tailwind::LIME_300)
|
||||||
|
} else if let None = active_hitbox {
|
||||||
|
Some(bevy::color::palettes::tailwind::GRAY_600)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(stroke_color) = stroke_color {
|
||||||
|
painter.rect_stroke(
|
||||||
|
bar,
|
||||||
|
2.0,
|
||||||
|
egui::Stroke::new(2.0f32, stroke_color.to_egui()),
|
||||||
|
egui::StrokeKind::Inside,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw hitbox rows
|
||||||
for (row, hitbox) in script.hitboxes.iter().enumerate() {
|
for (row, hitbox) in script.hitboxes.iter().enumerate() {
|
||||||
|
let row = row + 1;
|
||||||
let top = rect.top() + 20.0 + row as f32 * row_height;
|
let top = rect.top() + 20.0 + row as f32 * row_height;
|
||||||
painter.text(
|
painter.text(
|
||||||
egui::pos2(rect.left(), top + 2.0),
|
egui::pos2(rect.left(), top + 2.0),
|
||||||
egui::Align2::LEFT_TOP,
|
egui::Align2::LEFT_TOP,
|
||||||
format!("HB {}", hitbox.id),
|
format!("{}", hitbox.id),
|
||||||
egui::FontId::monospace(12.0),
|
egui::FontId::monospace(12.0),
|
||||||
egui::Color32::LIGHT_GRAY,
|
egui::Color32::LIGHT_GRAY,
|
||||||
);
|
);
|
||||||
@@ -673,10 +1005,11 @@ fn draw_timeline(
|
|||||||
bar,
|
bar,
|
||||||
2.0,
|
2.0,
|
||||||
if active {
|
if active {
|
||||||
egui::Color32::from_rgb(245, 75, 55)
|
hitbox.get_debug_draw_color()
|
||||||
} else {
|
} else {
|
||||||
egui::Color32::from_rgb(125, 48, 42)
|
hitbox.get_debug_draw_color().darker(0.2)
|
||||||
},
|
}
|
||||||
|
.to_egui(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let playhead = frame_x(timeline, state.frame, end);
|
let playhead = frame_x(timeline, state.frame, end);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ pub mod fighter;
|
|||||||
pub mod filled_gizmos;
|
pub mod filled_gizmos;
|
||||||
pub mod game_settings;
|
pub mod game_settings;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
#[cfg(feature = "hitbox-viewer")]
|
||||||
pub mod hitbox_viewer;
|
pub mod hitbox_viewer;
|
||||||
pub mod match_loading;
|
pub mod match_loading;
|
||||||
pub mod match_runtime;
|
pub mod match_runtime;
|
||||||
@@ -62,6 +63,7 @@ pub mod scripting;
|
|||||||
pub mod stage;
|
pub mod stage;
|
||||||
pub mod texture_reference;
|
pub mod texture_reference;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
pub mod egui_color_convert;
|
||||||
|
|
||||||
#[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
#[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||||
pub enum AppState {
|
pub enum AppState {
|
||||||
|
|||||||
+21
-7
@@ -1,6 +1,5 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use crate::{fighter::attack::AttackHitbox, math::int::FGi32};
|
use crate::{fighter::attack::AttackHitbox, math::int::FGi32};
|
||||||
|
|
||||||
@@ -15,22 +14,37 @@ 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 iasa_frame: Option<u32>,
|
||||||
pub wont_autocancel_window: (u32, u32),
|
pub wont_autocancel_window: Option<(u32, u32)>,
|
||||||
|
pub landing_duration: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FighterAttackScript {
|
impl FighterAttackScript {
|
||||||
pub fn validate(&self) -> Result<(), String> {
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
let mut ids = HashSet::with_capacity(self.hitboxes.len());
|
|
||||||
for hitbox in &self.hitboxes {
|
for hitbox in &self.hitboxes {
|
||||||
hitbox.validate()?;
|
hitbox.validate()?;
|
||||||
if !ids.insert(hitbox.id) {
|
}
|
||||||
return Err(format!("duplicate hitbox id {}", hitbox.id));
|
for (index, left) in self.hitboxes.iter().enumerate() {
|
||||||
|
for right in self.hitboxes.iter().skip(index + 1) {
|
||||||
|
if left.id == right.id
|
||||||
|
&& left.start_frame < right.end_frame
|
||||||
|
&& right.start_frame < left.end_frame
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"hitbox id {} has overlapping intervals {}..{} and {}..{}",
|
||||||
|
left.id, left.start_frame, left.end_frame, right.start_frame,
|
||||||
|
right.end_frame
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.wont_autocancel_window.0 > self.wont_autocancel_window.1 {
|
}
|
||||||
|
|
||||||
|
if let Some(wont_autocancel_window) = self.wont_autocancel_window {
|
||||||
|
if wont_autocancel_window.0 > wont_autocancel_window.1 {
|
||||||
return Err("autocancel window end precedes its start".into());
|
return Err("autocancel window end precedes its start".into());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+213
-54
@@ -1,6 +1,7 @@
|
|||||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
use rhai::EvalAltResult;
|
use rhai::EvalAltResult;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fighter::attack::{self, AttackAngle, FighterDamage, Knockback, KnockbackType},
|
fighter::attack::{self, AttackAngle, FighterDamage, Knockback, KnockbackType},
|
||||||
@@ -10,13 +11,41 @@ use crate::{
|
|||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct MoveCompiler {
|
struct MoveCompiler {
|
||||||
hitboxes: HashMap<u32, attack::AttackHitbox>,
|
hitboxes: Vec<attack::AttackHitbox>,
|
||||||
active_hitboxes: Vec<u32>,
|
active_hitboxes: HashMap<u32, usize>,
|
||||||
wont_autocancel_window: Option<(u32, u32)>,
|
wont_autocancel_window: Option<(u32, u32)>,
|
||||||
iasa_frame: Option<u32>,
|
iasa_frame: Option<u32>,
|
||||||
|
landing_duration: Option<u32>,
|
||||||
cursor: 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>> {
|
fn rhai_u32(value: i64, parameter: &str) -> Result<u32, Box<EvalAltResult>> {
|
||||||
u32::try_from(value).map_err(|_| {
|
u32::try_from(value).map_err(|_| {
|
||||||
format!(
|
format!(
|
||||||
@@ -43,28 +72,18 @@ impl MoveCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_hitbox(&mut self, id: u32) -> Result<(), Box<EvalAltResult>> {
|
fn remove_hitbox(&mut self, id: u32) -> Result<(), Box<EvalAltResult>> {
|
||||||
let hb = self
|
let hitbox_index = *self
|
||||||
.hitboxes
|
.active_hitboxes
|
||||||
.get_mut(&id)
|
.get(&id)
|
||||||
.ok_or(format!("Can't remove hitbox with {id}: Hitbox not found"))?;
|
.ok_or(format!("Can't remove hitbox with {id}: Hitbox not found"))?;
|
||||||
|
self.hitboxes[hitbox_index].end_frame = self.cursor;
|
||||||
let active_hbox_position =
|
self.active_hitboxes.remove(&id);
|
||||||
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn remove_all_hitboxes(&mut self) -> Result<(), Box<EvalAltResult>> {
|
fn remove_all_hitboxes(&mut self) -> Result<(), Box<EvalAltResult>> {
|
||||||
// We are gonna modify the array, so take a copy...
|
// 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)?;
|
self.remove_hitbox(hitbox_id)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,14 +116,14 @@ impl MoveCompiler {
|
|||||||
|
|
||||||
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 mut hitboxes: Vec<_> = self.hitboxes.into_values().collect();
|
let mut hitboxes = self.hitboxes;
|
||||||
hitboxes.sort_unstable_by_key(|hitbox| hitbox.id);
|
hitboxes.sort_unstable_by_key(|hitbox| (hitbox.id, hitbox.start_frame));
|
||||||
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,
|
iasa_frame: self.iasa_frame,
|
||||||
wont_autocancel_window,
|
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_normal", KnockbackType::make_normal)
|
||||||
.register_fn("knockback_fixed", KnockbackType::make_fixed)
|
.register_fn("knockback_fixed", KnockbackType::make_fixed)
|
||||||
.register_type_with_name::<AttackAngle>("Angle")
|
.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_fn("angle_sakurai", AttackAngle::make_sakurai)
|
||||||
.register_type_with_name::<Knockback>("Knockback")
|
.register_type_with_name::<Knockback>("Knockback")
|
||||||
.register_fn("knockback", Knockback::lit)
|
.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_type_with_name::<FGVec3>("Vec3")
|
||||||
.register_fn("vec3", FGVec3::lit)
|
.register_fn("vec3", FGVec3::lit)
|
||||||
.register_type_with_name::<FighterDamage>("FighterDamage")
|
.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);
|
.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();
|
let mc = mc.clone();
|
||||||
engine.register_fn(
|
engine.register_fn(
|
||||||
@@ -214,17 +276,91 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
|
|||||||
knockback: Knockback,
|
knockback: Knockback,
|
||||||
knockback_growth: FGi32|
|
knockback_growth: FGi32|
|
||||||
-> Result<(), Box<EvalAltResult>> {
|
-> Result<(), Box<EvalAltResult>> {
|
||||||
let mut b = mc.borrow_mut();
|
add_hitbox(
|
||||||
|
&mc,
|
||||||
// Rhai does not have first class u32 support, so we have to do this.
|
id,
|
||||||
let id = rhai_u32(id, "hitbox id")?;
|
0,
|
||||||
|
bone,
|
||||||
if b.hitboxes.contains_key(&id) {
|
damage,
|
||||||
return Err(format!("Hitbox with ID {} already existed!", id).into());
|
offset,
|
||||||
|
radius,
|
||||||
|
angle,
|
||||||
|
knockback_type,
|
||||||
|
knockback,
|
||||||
|
knockback_growth,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hitbox = attack::AttackHitbox {
|
{
|
||||||
|
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,
|
id,
|
||||||
|
group,
|
||||||
|
bone,
|
||||||
|
damage,
|
||||||
|
offset,
|
||||||
|
radius,
|
||||||
|
angle,
|
||||||
|
knockback_type,
|
||||||
|
knockback,
|
||||||
|
knockback_growth,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
engine.run(&text)
|
||||||
|
};
|
||||||
|
let script = result
|
||||||
|
.map(|_| {
|
||||||
|
Rc::into_inner(mc)
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.into_attack_script()
|
||||||
|
})
|
||||||
|
.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(),
|
bone: bone.to_owned(),
|
||||||
damage,
|
damage,
|
||||||
offset,
|
offset,
|
||||||
@@ -233,28 +369,11 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
|
|||||||
knockback_type,
|
knockback_type,
|
||||||
knockback,
|
knockback,
|
||||||
knockback_growth,
|
knockback_growth,
|
||||||
start_frame: b.cursor,
|
start_frame: frame,
|
||||||
end_frame: b.cursor,
|
end_frame: frame,
|
||||||
};
|
});
|
||||||
|
b.active_hitboxes.insert(id, hitbox_index);
|
||||||
b.hitboxes.insert(id, hitbox);
|
|
||||||
b.active_hitboxes.push(id);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
engine.run(&text)
|
|
||||||
};
|
|
||||||
|
|
||||||
result
|
|
||||||
.map(|_| {
|
|
||||||
Rc::into_inner(mc)
|
|
||||||
.unwrap()
|
|
||||||
.into_inner()
|
|
||||||
.into_attack_script()
|
|
||||||
})
|
|
||||||
.map_err(|err| err.to_string())?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -316,4 +435,44 @@ mod tests {
|
|||||||
assert_eq!(script.hitboxes[0].start_frame, 1);
|
assert_eq!(script.hitboxes[0].start_frame, 1);
|
||||||
assert_eq!(script.hitboxes[0].end_frame, 2);
|
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