Compare commits
2
Commits
04aceb05a9
...
f8aa76bb52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8aa76bb52 | ||
|
|
a0f6813387 |
Generated
+114
-210
@@ -531,7 +531,7 @@ dependencies = [
|
||||
"bevy_color",
|
||||
"bevy_core_pipeline",
|
||||
"bevy_ecs",
|
||||
"bevy_egui 0.40.1",
|
||||
"bevy_egui",
|
||||
"bevy_gizmos",
|
||||
"bevy_image",
|
||||
"bevy_light",
|
||||
@@ -549,7 +549,7 @@ dependencies = [
|
||||
"bevy_window",
|
||||
"bytemuck",
|
||||
"disqualified",
|
||||
"egui 0.34.3",
|
||||
"egui",
|
||||
"fuzzy-matcher",
|
||||
"image",
|
||||
"opener",
|
||||
@@ -1061,55 +1061,7 @@ dependencies = [
|
||||
"bevy_winit",
|
||||
"bytemuck",
|
||||
"crossbeam-channel",
|
||||
"egui 0.34.3",
|
||||
"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",
|
||||
"egui",
|
||||
"encase",
|
||||
"getrandom 0.4.3",
|
||||
"image",
|
||||
@@ -1324,7 +1276,7 @@ dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"bytemuck",
|
||||
"futures-lite",
|
||||
"guillotiere 0.6.2",
|
||||
"guillotiere",
|
||||
"half",
|
||||
"image",
|
||||
"ktx2",
|
||||
@@ -2214,17 +2166,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
@@ -3041,17 +2982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a05fbfa222ffb51989d5ccf33e5f7aebfcf96c5023413856b0c3618a7f79896e"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"emath 0.34.3",
|
||||
]
|
||||
|
||||
[[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",
|
||||
"emath",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3063,8 +2994,8 @@ dependencies = [
|
||||
"accesskit",
|
||||
"ahash",
|
||||
"bitflags 2.13.1",
|
||||
"emath 0.34.3",
|
||||
"epaint 0.34.3",
|
||||
"emath",
|
||||
"epaint",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"profiling",
|
||||
@@ -3073,23 +3004,17 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egui"
|
||||
version = "0.36.2"
|
||||
name = "egui_extras"
|
||||
version = "0.34.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc938cc27cd911415e1e4d151bc56ff9df063d8db081374e87aeb805ea74b2ba"
|
||||
checksum = "598d8675f6fd9088db8a93d8c7aacda936b2f3d0c2b0660ad1744a45b5caf922"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"ahash",
|
||||
"bitflags 2.13.1",
|
||||
"emath 0.36.2",
|
||||
"epaint 0.36.2",
|
||||
"itertools 0.15.0",
|
||||
"egui",
|
||||
"enum-map",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"mime_guess2",
|
||||
"profiling",
|
||||
"smallvec",
|
||||
"unicode-segmentation",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3107,15 +3032,6 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "emath"
|
||||
version = "0.36.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ena"
|
||||
version = "0.14.4"
|
||||
@@ -3156,6 +3072,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "enum_cycling"
|
||||
version = "0.2.1"
|
||||
@@ -3181,9 +3117,9 @@ checksum = "6675898a291ec212fc3df04f537d177fce8496120244590e6359dcaa4c25da79"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
"ecolor 0.34.3",
|
||||
"emath 0.34.3",
|
||||
"epaint_default_fonts 0.34.3",
|
||||
"ecolor",
|
||||
"emath",
|
||||
"epaint_default_fonts",
|
||||
"font-types 0.11.3",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
@@ -3192,32 +3128,7 @@ dependencies = [
|
||||
"self_cell",
|
||||
"skrifa 0.40.0",
|
||||
"smallvec",
|
||||
"vello_cpu 0.0.6",
|
||||
]
|
||||
|
||||
[[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",
|
||||
"vello_cpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3226,12 +3137,6 @@ version = "0.34.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8970033a4282a7bcf899b38b5ed3a58b732fe093d03785d58648515d8d309da"
|
||||
|
||||
[[package]]
|
||||
name = "epaint_default_fonts"
|
||||
version = "0.36.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773fa9c96dd0dbef887e39d0ed6177f141cce4d68e6041df77570aa3702dfa13"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -3324,12 +3229,6 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fearless_simd"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693"
|
||||
|
||||
[[package]]
|
||||
name = "file-id"
|
||||
version = "0.2.3"
|
||||
@@ -3715,22 +3614,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "glow"
|
||||
version = "0.17.0"
|
||||
@@ -3841,15 +3724,6 @@ dependencies = [
|
||||
"svg_fmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "guillotiere"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab"
|
||||
dependencies = [
|
||||
"euclid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
@@ -3876,18 +3750,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "hash32"
|
||||
version = "0.3.1"
|
||||
@@ -4552,6 +4414,24 @@ dependencies = [
|
||||
"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]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -5370,7 +5250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fad031076f48f0d4d85ce1aea9b94b4e715a4d636a030a123038f8f5b5e4343"
|
||||
dependencies = [
|
||||
"fontique",
|
||||
"harfrust 0.6.2",
|
||||
"harfrust",
|
||||
"hashbrown 0.17.1",
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
@@ -5497,6 +5377,50 @@ dependencies = [
|
||||
"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]]
|
||||
name = "pin-project"
|
||||
version = "1.1.13"
|
||||
@@ -6272,18 +6196,19 @@ dependencies = [
|
||||
"bevy",
|
||||
"bevy-inspector-egui",
|
||||
"bevy_asset_loader",
|
||||
"bevy_egui 0.40.1",
|
||||
"bevy_egui",
|
||||
"bevy_ggrs",
|
||||
"bevygraph",
|
||||
"bincode",
|
||||
"blake3",
|
||||
"clap",
|
||||
"egui_extras",
|
||||
"enum_cycling",
|
||||
"enum_cycling_derive",
|
||||
"fixed",
|
||||
"fixed_math",
|
||||
"gc-adapter-neo",
|
||||
"gltf",
|
||||
"itertools 0.15.0",
|
||||
"rhai",
|
||||
"ron",
|
||||
"ron_asset_manager",
|
||||
@@ -6335,6 +6260,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
|
||||
|
||||
[[package]]
|
||||
name = "skrifa"
|
||||
version = "0.40.0"
|
||||
@@ -6992,10 +6923,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
@@ -7105,7 +7036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"fearless_simd 0.3.0",
|
||||
"fearless_simd",
|
||||
"hashbrown 0.16.1",
|
||||
"log",
|
||||
"peniko",
|
||||
@@ -7113,21 +7044,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "vello_cpu"
|
||||
version = "0.0.6"
|
||||
@@ -7136,19 +7052,7 @@ checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"hashbrown 0.16.1",
|
||||
"vello_common 0.0.6",
|
||||
]
|
||||
|
||||
[[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",
|
||||
"vello_common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+9
-5
@@ -8,23 +8,22 @@ authors = [""]
|
||||
|
||||
[features]
|
||||
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"]
|
||||
gamecube-adapter = ["dep:gc-adapter-neo"]
|
||||
hitbox-viewer = ["bevy/bevy_dev_tools", "dep:egui_extras"]
|
||||
|
||||
[dependencies]
|
||||
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" ] }
|
||||
itertools = "0.15.0"
|
||||
thiserror = "2"
|
||||
avian3d = "0.7"
|
||||
arrayvec = "0.7.8"
|
||||
bevy-inspector-egui = "0.37.0"
|
||||
bevy_egui = "0.40.0"
|
||||
egui_extras = { version = "0.34.3", optional = true}
|
||||
bevy_ggrs = { git = "https://github.com/gschup/bevy_ggrs" }
|
||||
bevygraph = { git = "https://github.com/tbillington/bevy_debug_view" }
|
||||
serde = "1.0.229"
|
||||
serde_json = "1.0.145"
|
||||
bincode = "1.3.3"
|
||||
@@ -53,3 +52,8 @@ strip = "symbols"
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
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)]
|
||||
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);
|
||||
|
||||
impl std::fmt::Display for FighterDamage {
|
||||
@@ -80,7 +80,7 @@ impl HitStop {
|
||||
HitStop(hitstop)
|
||||
}
|
||||
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 {
|
||||
FGi32::lit("0.66666666")
|
||||
} else {
|
||||
@@ -97,6 +97,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)]
|
||||
pub enum KnockbackType {
|
||||
Normal,
|
||||
@@ -113,9 +119,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);
|
||||
|
||||
impl std::fmt::Display for Knockback {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:.2}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Knockback {
|
||||
pub fn lit(val: &str) -> Self {
|
||||
Self(FGi32::lit(val))
|
||||
@@ -144,6 +156,15 @@ pub enum AttackAngle {
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
AttackAngle::Normal(FGi32::lit("0"))
|
||||
@@ -175,6 +196,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)]
|
||||
@@ -217,6 +245,7 @@ impl AttackKind {
|
||||
#[reflect(opaque)]
|
||||
pub struct AttackHitbox {
|
||||
pub id: u32,
|
||||
pub group: u32,
|
||||
pub bone: String,
|
||||
pub damage: FighterDamage,
|
||||
pub offset: FGVec3,
|
||||
@@ -229,6 +258,14 @@ pub struct AttackHitbox {
|
||||
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;
|
||||
|
||||
#[derive(Component, Hash, Clone, Default)]
|
||||
@@ -276,6 +313,10 @@ impl FighterStaleMoveQueue {
|
||||
}
|
||||
|
||||
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> {
|
||||
if self.bone.trim().is_empty() {
|
||||
return Err("bone name is empty".into());
|
||||
@@ -369,6 +410,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)]
|
||||
pub struct FighterSolvedHurtbox {
|
||||
pub hurtbox_idx: usize,
|
||||
@@ -571,10 +642,6 @@ pub fn intersect_attacks_with_hurtboxes(
|
||||
let Some(active_attack) = hitboxes.0.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
if active_attack.hit_fighters.contains(&entity_receiving) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let attack_script = attack_scripts
|
||||
.get(&active_attack.script)
|
||||
.expect("Attack script should be valid");
|
||||
@@ -584,6 +651,15 @@ pub fn intersect_attacks_with_hurtboxes(
|
||||
for solved_hitbox in active_attack.solved_hitboxes.iter() {
|
||||
if hurtbox.capsule.intersects(solved_hitbox.capsule) {
|
||||
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| {
|
||||
attack_script.hitboxes[candidate].id
|
||||
< attack_script.hitboxes[current].id
|
||||
@@ -606,7 +682,14 @@ pub fn intersect_attacks_with_hurtboxes(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ pub fn resolve_fighter_hits(
|
||||
};
|
||||
|
||||
// Attacker hitstop and data
|
||||
let hitstop = HitStop::calculate(FighterDamage(attack_damage), false);
|
||||
let hitstop = HitStop::calculate(FighterDamage(attack_damage), matches!(receiver_state, FighterState::Crouch(..)));
|
||||
reactions.push((
|
||||
attacker_entity,
|
||||
attacker_slot,
|
||||
|
||||
+18
-8
@@ -28,6 +28,7 @@ use crate::math::int::FGi32;
|
||||
use crate::math::vec::FGVec2;
|
||||
use crate::math::vec3::FGVec3;
|
||||
use crate::player::Player;
|
||||
use crate::scripting::FighterAttackScript;
|
||||
|
||||
pub fn debug_draw_ecb(
|
||||
ecbs: Query<(
|
||||
@@ -368,6 +369,7 @@ pub fn attack_debug(
|
||||
)>,
|
||||
mut gizmos: Gizmos,
|
||||
mut filled_gizmos: FilledGizmos,
|
||||
attack_scripts: Res<Assets<FighterAttackScript>>
|
||||
) {
|
||||
for (hitboxes, _matrices, _translation, _facing_direction, _frame) in query {
|
||||
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");
|
||||
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 mut capsule_draw_trf = Transform::IDENTITY;
|
||||
capsule_draw_trf.translation = center.to_vec3();
|
||||
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(
|
||||
&attack_capsule,
|
||||
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(
|
||||
capsule_draw_trf.to_isometry(),
|
||||
solved_hitbox.capsule.radius.to_num(),
|
||||
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);
|
||||
|
||||
+7
-1
@@ -293,10 +293,16 @@ pub struct FighterActiveAttack {
|
||||
pub script: Handle<FighterAttackScript>,
|
||||
pub active_hitboxes: Vec<usize>,
|
||||
pub solved_hitboxes: Vec<attack::FighterSolvedHitbox>,
|
||||
pub hit_fighters: Vec<Entity>,
|
||||
pub hit_groups: Vec<FighterHitGroupState>,
|
||||
pub stalled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash)]
|
||||
pub struct FighterHitGroupState {
|
||||
pub group: u32,
|
||||
pub hit_fighters: Vec<Entity>,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Default, Hash)]
|
||||
pub struct FighterHitboxes(pub Option<FighterActiveAttack>);
|
||||
|
||||
|
||||
@@ -1,23 +1,42 @@
|
||||
use bevy::log::error;
|
||||
|
||||
use crate::fighter::{
|
||||
attack::{AttackKind, FighterAttackScriptAssets, update_active_hitbox_list}, state::{
|
||||
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, fall::FallState,
|
||||
attack::{AttackKind, FighterAttackScriptAssets, update_active_attack}, state::{
|
||||
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, air_attack, fall::FallState, jump,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Hash)]
|
||||
pub struct AirAttackState {
|
||||
pub attack_kind: AttackKind,
|
||||
pub airborne_state_common: AirborneStateCommon
|
||||
pub airborne_state_common: AirborneStateCommon,
|
||||
pub is_in_autocancel_window: bool,
|
||||
}
|
||||
|
||||
impl FighterStateImpl for AirAttackState {
|
||||
const NAME: &'static str = "AirAttack";
|
||||
|
||||
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() {
|
||||
Some(FighterState::Fall(FallState::new(self.airborne_state_common)))
|
||||
Some(FighterState::Fall(FallState::new(
|
||||
self.airborne_state_common,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -29,16 +48,19 @@ impl FighterStateImpl for AirAttackState {
|
||||
.attack_scripts
|
||||
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
||||
if let Some(script) = script {
|
||||
update_active_hitbox_list(
|
||||
update_active_attack(
|
||||
script,
|
||||
&mut state_context
|
||||
state_context
|
||||
.hitboxes
|
||||
.0
|
||||
.as_mut()
|
||||
.expect("ground attack must have active hitboxes")
|
||||
.active_hitboxes,
|
||||
.expect("ground attack must have active hitboxes"),
|
||||
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);
|
||||
@@ -55,7 +77,7 @@ impl FighterStateImpl for AirAttackState {
|
||||
script,
|
||||
active_hitboxes: Vec::new(),
|
||||
solved_hitboxes: Vec::new(),
|
||||
hit_fighters: Vec::new(),
|
||||
hit_groups: Vec::new(),
|
||||
stalled: false,
|
||||
});
|
||||
}
|
||||
@@ -68,11 +90,20 @@ impl FighterStateImpl for AirAttackState {
|
||||
&mut self,
|
||||
state_context: &mut CollisionContext,
|
||||
) -> Option<FighterState> {
|
||||
air::collision_interrupt(
|
||||
let coll_interrupt = air::collision_interrupt(
|
||||
state_context,
|
||||
crate::fighter::state::land::LandingType::Normal,
|
||||
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 {
|
||||
@@ -83,25 +114,33 @@ impl FighterStateImpl for AirAttackState {
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum AirAttackStateCreateError {
|
||||
#[error("Attack script: {attack:?} not found")]
|
||||
AttackScriptNotFound{attack: AttackKind},
|
||||
AttackScriptNotFound { attack: AttackKind },
|
||||
}
|
||||
|
||||
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) {
|
||||
Ok(AirAttackState {
|
||||
airborne_state_common: airborne_state_common.clone(),
|
||||
attack_kind
|
||||
attack_kind,
|
||||
is_in_autocancel_window: false,
|
||||
})
|
||||
} else {
|
||||
Err(AirAttackStateCreateError::AttackScriptNotFound { attack: attack_kind })
|
||||
Err(AirAttackStateCreateError::AttackScriptNotFound {
|
||||
attack: attack_kind,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
pub fn check_interrupt(state_context: &InterruptContext, airborne_state_common: &AirborneStateCommon) -> Option<FighterState> {
|
||||
pub fn check_interrupt(
|
||||
state_context: &InterruptContext,
|
||||
airborne_state_common: &AirborneStateCommon,
|
||||
) -> Option<FighterState> {
|
||||
if state_context
|
||||
.input
|
||||
.has_command(crate::input::FighterCommands::Attack)
|
||||
@@ -110,16 +149,18 @@ pub fn check_interrupt(state_context: &InterruptContext, airborne_state_common:
|
||||
.input
|
||||
.get_attack_command_type()
|
||||
.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 {
|
||||
Ok(state) => {
|
||||
Some(FighterState::AirAttack(state))
|
||||
},
|
||||
Ok(state) => Some(FighterState::AirAttack(state)),
|
||||
Err(create_error) => {
|
||||
error!("{}", create_error);
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::fighter::{
|
||||
attack::{AttackKind, update_active_hitbox_list},
|
||||
attack::{AttackKind, update_active_attack},
|
||||
state::{
|
||||
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl,
|
||||
InterruptContext, UpdateContext, wait::WaitState,
|
||||
@@ -28,14 +28,13 @@ impl FighterStateImpl for GroundAttackState {
|
||||
.attack_scripts
|
||||
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
||||
if let Some(script) = script {
|
||||
update_active_hitbox_list(
|
||||
update_active_attack(
|
||||
script,
|
||||
&mut state_context
|
||||
state_context
|
||||
.hitboxes
|
||||
.0
|
||||
.as_mut()
|
||||
.expect("ground attack must have active hitboxes")
|
||||
.active_hitboxes,
|
||||
.expect("ground attack must have active hitboxes"),
|
||||
animation_frame,
|
||||
);
|
||||
}
|
||||
@@ -52,7 +51,7 @@ impl FighterStateImpl for GroundAttackState {
|
||||
script,
|
||||
active_hitboxes: Vec::new(),
|
||||
solved_hitboxes: Vec::new(),
|
||||
hit_fighters: Vec::new(),
|
||||
hit_groups: Vec::new(),
|
||||
stalled: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ pub struct InterruptContext<'a> {
|
||||
pub surface: &'a FighterSurfaceState,
|
||||
pub animation: AnimationStatus,
|
||||
pub stage_collision: &'a StageCollision,
|
||||
pub attack_scripts: &'a Assets<FighterAttackScript>,
|
||||
pub attack_script_assets: &'a FighterAttackScriptAssets,
|
||||
}
|
||||
|
||||
@@ -300,6 +301,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
baked_animations: &'a Assets<BakedFighterAnimations>,
|
||||
attributes: &'a FighterAttributes,
|
||||
stage_collision: &'a StageCollision,
|
||||
attack_scripts: &'a Assets<FighterAttackScript>,
|
||||
attack_script_assets: &'a FighterAttackScriptAssets,
|
||||
) -> InterruptContext<'a> {
|
||||
let animation = self.animation_status(animations, baked_animations);
|
||||
@@ -312,6 +314,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
surface: &self.surface,
|
||||
stage_collision,
|
||||
attack_script_assets: attack_script_assets,
|
||||
attack_scripts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,6 +474,7 @@ pub fn state_interrupt_system(
|
||||
game_settings: Res<GameSettings>,
|
||||
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||
fighter_manifests: Res<Assets<FighterManifest>>,
|
||||
attack_scripts: Res<Assets<FighterAttackScript>>,
|
||||
stage_collision: Res<StageCollision>,
|
||||
) {
|
||||
for fighter in &mut query {
|
||||
@@ -547,6 +551,7 @@ pub fn state_interrupt_system(
|
||||
&baked_animations,
|
||||
&manifest.attributes,
|
||||
&stage_collision,
|
||||
&attack_scripts,
|
||||
attack_script_assets,
|
||||
))
|
||||
};
|
||||
|
||||
+445
-112
@@ -1,28 +1,30 @@
|
||||
//! 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 clap::{Parser, ValueEnum};
|
||||
use egui_extras::Column;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
GameAssetPlugin,
|
||||
fighter::{
|
||||
animation::FighterAnimationFrame,
|
||||
attack::{AttackHitbox, AttackKind},
|
||||
baked_animation::{
|
||||
GameAssetPlugin, egui_color_convert::ColorToEgui, fighter::{
|
||||
animation::FighterAnimationFrame, attack::{AttackHitbox, AttackKind, HitStop}, baked_animation::{
|
||||
BakedAnimationAssetProcessingPlugin, BakedAnimationSamplingSet,
|
||||
BakedAnimationVisualPlugin, BakedFighterAnimations, FighterBoneIndexMap,
|
||||
FighterBoneMatrices, FixedMat4,
|
||||
},
|
||||
manifest::{FighterHurtbox, FighterManifest},
|
||||
visual::FighterAnimations,
|
||||
},
|
||||
filled_gizmos::{FilledGizmos, FilledGizmosPlugin},
|
||||
math::{
|
||||
}, manifest::{FighterHurtbox, FighterManifest}, visual::FighterAnimations,
|
||||
}, filled_gizmos::{FilledGizmos, FilledGizmosPlugin}, game_settings::GameSettings, math::{
|
||||
int::{FGi32, FGi32Ext},
|
||||
vec3::FGVec3,
|
||||
},
|
||||
scripting::{FighterAttackScript, importer::FighterScriptImportPlugin},
|
||||
}, scripting::{FighterAttackScript, importer::FighterScriptImportPlugin},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
@@ -67,7 +69,7 @@ pub struct HitboxViewerArgs {
|
||||
#[arg(long, default_value = "fighters/test/test.fighter.ron")]
|
||||
pub manifest: String,
|
||||
/// 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,
|
||||
/// Start with fighter hurtboxes hidden.
|
||||
#[arg(long)]
|
||||
@@ -76,10 +78,14 @@ pub struct HitboxViewerArgs {
|
||||
|
||||
#[derive(Resource)]
|
||||
struct ViewerAssets {
|
||||
settings: Handle<GameSettings>,
|
||||
manifest: Handle<FighterManifest>,
|
||||
manifest_path: String,
|
||||
manifest_choices: Vec<(String, Handle<FighterManifest>)>,
|
||||
model: Option<Handle<Gltf>>,
|
||||
baked: Option<Handle<BakedFighterAnimations>>,
|
||||
script: Option<Handle<FighterAttackScript>>,
|
||||
script_attack: Option<AttackKind>,
|
||||
fighter: Option<Entity>,
|
||||
error: Option<String>,
|
||||
}
|
||||
@@ -91,33 +97,57 @@ struct ViewerState {
|
||||
playing: bool,
|
||||
show_hurtboxes: bool,
|
||||
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;
|
||||
|
||||
impl Plugin for HitboxViewerPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
let args = app.world().resource::<HitboxViewerArgs>();
|
||||
let attack = args.attack.into();
|
||||
let show_hurtboxes = !args.hide_hurtboxes;
|
||||
let (attack, show_hurtboxes, manifest_path) = {
|
||||
let args = app.world().resource::<HitboxViewerArgs>();
|
||||
(
|
||||
args.attack.into(),
|
||||
!args.hide_hurtboxes,
|
||||
args.manifest.clone(),
|
||||
)
|
||||
};
|
||||
app.add_plugins((
|
||||
GameAssetPlugin,
|
||||
FighterScriptImportPlugin,
|
||||
BakedAnimationAssetProcessingPlugin,
|
||||
BakedAnimationVisualPlugin,
|
||||
FilledGizmosPlugin,
|
||||
InfiniteGridPlugin,
|
||||
))
|
||||
.insert_resource(ViewerState {
|
||||
attack,
|
||||
frame: 0,
|
||||
playing: true,
|
||||
playing: false,
|
||||
show_hurtboxes,
|
||||
elapsed: 0.0,
|
||||
manifest_path,
|
||||
})
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
(
|
||||
apply_viewer_selection,
|
||||
request_fighter_dependencies,
|
||||
spawn_preview_fighter,
|
||||
handle_keyboard,
|
||||
@@ -133,17 +163,28 @@ impl Plugin for HitboxViewerPlugin {
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<HitboxViewerArgs>) {
|
||||
let manifest_path = args.manifest.clone();
|
||||
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,
|
||||
baked: None,
|
||||
script: None,
|
||||
script_attack: None,
|
||||
fighter: None,
|
||||
error: None,
|
||||
});
|
||||
commands.spawn((
|
||||
Name::new("Hitbox viewer camera"),
|
||||
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),
|
||||
));
|
||||
commands.spawn((
|
||||
@@ -160,30 +201,102 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Hitbo
|
||||
brightness: 250.0,
|
||||
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(
|
||||
asset_server: Res<AssetServer>,
|
||||
settings: Res<Assets<GameSettings>>,
|
||||
manifests: Res<Assets<FighterManifest>>,
|
||||
mut viewer: ResMut<ViewerAssets>,
|
||||
state: Res<ViewerState>,
|
||||
mut state: ResMut<ViewerState>,
|
||||
) {
|
||||
if viewer.model.is_some() {
|
||||
return;
|
||||
if let Some(settings) = settings.get(&viewer.settings) {
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
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!(
|
||||
"{:?} has no {:?} attack script",
|
||||
manifest.id, state.attack
|
||||
));
|
||||
return;
|
||||
};
|
||||
viewer.model = Some(asset_server.load(manifest.model_path.clone()));
|
||||
viewer.baked = Some(asset_server.load(manifest.baked_animation_path.clone()));
|
||||
viewer.script = Some(asset_server.load(script_path.clone()));
|
||||
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()));
|
||||
}
|
||||
if viewer.baked.is_none() {
|
||||
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_attack = Some(state.attack);
|
||||
}
|
||||
viewer.error = None;
|
||||
}
|
||||
|
||||
@@ -349,7 +462,6 @@ fn draw_preview(
|
||||
scripts: Res<Assets<FighterAttackScript>>,
|
||||
baked_assets: Res<Assets<BakedFighterAnimations>>,
|
||||
manifests: Res<Assets<FighterManifest>>,
|
||||
mut gizmos: Gizmos,
|
||||
mut filled: FilledGizmos,
|
||||
) {
|
||||
let (Some(script_handle), Some(baked_handle)) = (&viewer.script, &viewer.baked) else {
|
||||
@@ -398,7 +510,6 @@ fn draw_preview(
|
||||
&previous_animation,
|
||||
manifest.model_scale,
|
||||
fighter_transform,
|
||||
&mut gizmos,
|
||||
&mut filled,
|
||||
);
|
||||
}
|
||||
@@ -411,7 +522,6 @@ fn draw_hitbox(
|
||||
previous_animation: &FighterAnimationFrame,
|
||||
model_scale: FGi32,
|
||||
fighter_transform: FixedMat4,
|
||||
gizmos: &mut Gizmos,
|
||||
filled: &mut FilledGizmos,
|
||||
) {
|
||||
let Some(current_bone) =
|
||||
@@ -451,13 +561,16 @@ fn draw_hitbox(
|
||||
transform.to_isometry(),
|
||||
radius,
|
||||
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),
|
||||
transform.to_isometry(),
|
||||
Color::srgb(1.0, 0.15, 0.1),
|
||||
);
|
||||
);*/
|
||||
}
|
||||
|
||||
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(
|
||||
mut contexts: EguiContexts,
|
||||
mut state: ResMut<ViewerState>,
|
||||
viewer: Res<ViewerAssets>,
|
||||
manifests: Res<Assets<FighterManifest>>,
|
||||
scripts: Res<Assets<FighterAttackScript>>,
|
||||
baked_assets: Res<Assets<BakedFighterAnimations>>,
|
||||
) -> Result {
|
||||
@@ -499,10 +806,62 @@ fn draw_ui(
|
||||
.script
|
||||
.as_ref()
|
||||
.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")
|
||||
.anchor(egui::Align2::CENTER_BOTTOM, egui::vec2(0.0, -10.0))
|
||||
.resizable(true)
|
||||
.default_size(egui::vec2(780.0, 190.0))
|
||||
.resizable(false)
|
||||
.default_size(egui::vec2(780.0, 250.0))
|
||||
.show(contexts.ctx_mut()?, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
@@ -521,8 +880,6 @@ fn draw_ui(
|
||||
}
|
||||
ui.label(format!("Frame {} / {}", state.frame, end.saturating_sub(1)));
|
||||
ui.checkbox(&mut state.show_hurtboxes, "Hurtboxes");
|
||||
ui.separator();
|
||||
ui.label(format!("{:?}", state.attack));
|
||||
});
|
||||
if let Some(error) = &viewer.error {
|
||||
ui.colored_label(egui::Color32::RED, error);
|
||||
@@ -532,82 +889,12 @@ fn draw_ui(
|
||||
ui.label("Waiting for the processed attack script…");
|
||||
return;
|
||||
};
|
||||
draw_summary_timeline(ui, &mut state, script, end);
|
||||
//draw_summary_timeline(ui, &mut state, script, end);
|
||||
draw_timeline(ui, &mut state, script, end);
|
||||
});
|
||||
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(
|
||||
ui: &mut egui::Ui,
|
||||
state: &mut ViewerState,
|
||||
@@ -616,8 +903,9 @@ fn draw_timeline(
|
||||
) {
|
||||
let row_height = 20.0;
|
||||
let label_width = 55.0;
|
||||
// +1 for the summary layer
|
||||
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(
|
||||
egui::vec2(ui.available_width(), height),
|
||||
egui::Sense::click_and_drag(),
|
||||
@@ -652,12 +940,56 @@ fn draw_timeline(
|
||||
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() {
|
||||
let row = row + 1;
|
||||
let top = rect.top() + 20.0 + row as f32 * row_height;
|
||||
painter.text(
|
||||
egui::pos2(rect.left(), top + 2.0),
|
||||
egui::Align2::LEFT_TOP,
|
||||
format!("HB {}", hitbox.id),
|
||||
format!("{}", hitbox.id),
|
||||
egui::FontId::monospace(12.0),
|
||||
egui::Color32::LIGHT_GRAY,
|
||||
);
|
||||
@@ -673,10 +1005,11 @@ fn draw_timeline(
|
||||
bar,
|
||||
2.0,
|
||||
if active {
|
||||
egui::Color32::from_rgb(245, 75, 55)
|
||||
hitbox.get_debug_draw_color()
|
||||
} 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);
|
||||
|
||||
@@ -48,6 +48,7 @@ pub mod fighter;
|
||||
pub mod filled_gizmos;
|
||||
pub mod game_settings;
|
||||
pub mod input;
|
||||
#[cfg(feature = "hitbox-viewer")]
|
||||
pub mod hitbox_viewer;
|
||||
pub mod match_loading;
|
||||
pub mod match_runtime;
|
||||
@@ -62,6 +63,7 @@ pub mod scripting;
|
||||
pub mod stage;
|
||||
pub mod texture_reference;
|
||||
pub mod ui;
|
||||
pub mod egui_color_convert;
|
||||
|
||||
#[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||
pub enum AppState {
|
||||
|
||||
+22
-8
@@ -1,6 +1,5 @@
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{fighter::attack::AttackHitbox, math::int::FGi32};
|
||||
|
||||
@@ -15,22 +14,37 @@ pub enum MoveAngleKind {
|
||||
#[derive(Asset, Reflect, Serialize, Deserialize)]
|
||||
pub struct FighterAttackScript {
|
||||
pub hitboxes: Vec<AttackHitbox>,
|
||||
pub iasa_frame: u32,
|
||||
pub wont_autocancel_window: (u32, u32),
|
||||
pub iasa_frame: Option<u32>,
|
||||
pub wont_autocancel_window: Option<(u32, u32)>,
|
||||
pub landing_duration: Option<u32>,
|
||||
}
|
||||
|
||||
impl FighterAttackScript {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
let mut ids = HashSet::with_capacity(self.hitboxes.len());
|
||||
for hitbox in &self.hitboxes {
|
||||
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 {
|
||||
return Err("autocancel window end precedes its start".into());
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+191
-46
@@ -1,6 +1,7 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use rhai::EvalAltResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
fighter::attack::{self, AttackAngle, FighterDamage, Knockback, KnockbackType},
|
||||
@@ -10,13 +11,41 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MoveCompiler {
|
||||
hitboxes: HashMap<u32, attack::AttackHitbox>,
|
||||
active_hitboxes: Vec<u32>,
|
||||
hitboxes: Vec<attack::AttackHitbox>,
|
||||
active_hitboxes: HashMap<u32, usize>,
|
||||
wont_autocancel_window: Option<(u32, u32)>,
|
||||
iasa_frame: Option<u32>,
|
||||
landing_duration: Option<u32>,
|
||||
cursor: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SharedHitboxInfo {
|
||||
damage: FighterDamage,
|
||||
angle: AttackAngle,
|
||||
knockback_type: KnockbackType,
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32,
|
||||
}
|
||||
|
||||
impl SharedHitboxInfo {
|
||||
pub fn make(
|
||||
damage: FighterDamage,
|
||||
angle: AttackAngle,
|
||||
knockback_type: KnockbackType,
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32,
|
||||
) -> Self {
|
||||
Self {
|
||||
damage,
|
||||
angle,
|
||||
knockback_type,
|
||||
knockback,
|
||||
knockback_growth,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rhai_u32(value: i64, parameter: &str) -> Result<u32, Box<EvalAltResult>> {
|
||||
u32::try_from(value).map_err(|_| {
|
||||
format!(
|
||||
@@ -43,28 +72,18 @@ impl MoveCompiler {
|
||||
}
|
||||
|
||||
fn remove_hitbox(&mut self, id: u32) -> Result<(), Box<EvalAltResult>> {
|
||||
let hb = self
|
||||
.hitboxes
|
||||
.get_mut(&id)
|
||||
let hitbox_index = *self
|
||||
.active_hitboxes
|
||||
.get(&id)
|
||||
.ok_or(format!("Can't remove hitbox with {id}: Hitbox not found"))?;
|
||||
|
||||
let active_hbox_position =
|
||||
self.active_hitboxes
|
||||
.iter()
|
||||
.position(|i| *i == id)
|
||||
.ok_or(format!(
|
||||
"Can't remove hitbox with ID {id}: Hitbox was not active"
|
||||
))?;
|
||||
|
||||
hb.end_frame = self.cursor;
|
||||
|
||||
self.active_hitboxes.remove(active_hbox_position);
|
||||
self.hitboxes[hitbox_index].end_frame = self.cursor;
|
||||
self.active_hitboxes.remove(&id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn remove_all_hitboxes(&mut self) -> Result<(), Box<EvalAltResult>> {
|
||||
// We are gonna modify the array, so take a copy...
|
||||
for hitbox_id in self.active_hitboxes.clone() {
|
||||
for hitbox_id in self.active_hitboxes.keys().copied().collect::<Vec<_>>() {
|
||||
self.remove_hitbox(hitbox_id)?;
|
||||
}
|
||||
|
||||
@@ -97,14 +116,14 @@ impl MoveCompiler {
|
||||
|
||||
fn into_attack_script(mut self) -> Result<FighterAttackScript, String> {
|
||||
self.remove_all_hitboxes().map_err(|err| err.to_string())?;
|
||||
let mut hitboxes: Vec<_> = self.hitboxes.into_values().collect();
|
||||
hitboxes.sort_unstable_by_key(|hitbox| hitbox.id);
|
||||
let iasa_frame = self.iasa_frame.unwrap_or(100000);
|
||||
let wont_autocancel_window = self.wont_autocancel_window.unwrap_or((0, 100000));
|
||||
let mut hitboxes = self.hitboxes;
|
||||
hitboxes.sort_unstable_by_key(|hitbox| (hitbox.id, hitbox.start_frame));
|
||||
|
||||
Ok(FighterAttackScript {
|
||||
hitboxes,
|
||||
iasa_frame,
|
||||
wont_autocancel_window,
|
||||
iasa_frame: self.iasa_frame,
|
||||
wont_autocancel_window: self.wont_autocancel_window,
|
||||
landing_duration: self.landing_duration,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -117,7 +136,9 @@ pub fn register_common_types(engine: &mut rhai::Engine) {
|
||||
.register_fn("knockback_normal", KnockbackType::make_normal)
|
||||
.register_fn("knockback_fixed", KnockbackType::make_fixed)
|
||||
.register_type_with_name::<AttackAngle>("Angle")
|
||||
.register_fn("angle", |str: &str| AttackAngle::make_normal(FGi32::lit(str)))
|
||||
.register_fn("angle", |str: &str| {
|
||||
AttackAngle::make_normal(FGi32::lit(str))
|
||||
})
|
||||
.register_fn("angle_sakurai", AttackAngle::make_sakurai)
|
||||
.register_type_with_name::<Knockback>("Knockback")
|
||||
.register_fn("knockback", Knockback::lit)
|
||||
@@ -128,6 +149,17 @@ pub fn register_common_types(engine: &mut rhai::Engine) {
|
||||
.register_type_with_name::<FGVec3>("Vec3")
|
||||
.register_fn("vec3", FGVec3::lit)
|
||||
.register_type_with_name::<FighterDamage>("FighterDamage")
|
||||
.register_type_with_name::<SharedHitboxInfo>("SharedHitboxInfo")
|
||||
.register_fn(
|
||||
"make_hitbox_info",
|
||||
|damage: FighterDamage,
|
||||
angle: AttackAngle,
|
||||
knockback_type: KnockbackType,
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32| {
|
||||
SharedHitboxInfo::make(damage, angle, knockback_type, knockback, knockback_growth)
|
||||
},
|
||||
)
|
||||
.register_fn("damage", FighterDamage::lit);
|
||||
}
|
||||
|
||||
@@ -200,6 +232,37 @@ 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. A hitbox without an
|
||||
// explicit group behaves as its own group, which preserves the old
|
||||
// one-hit-per-target behavior.
|
||||
{
|
||||
let mc = mc.clone();
|
||||
engine.register_fn(
|
||||
@@ -214,18 +277,11 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32|
|
||||
-> Result<(), Box<EvalAltResult>> {
|
||||
let mut b = mc.borrow_mut();
|
||||
|
||||
// Rhai does not have first class u32 support, so we have to do this.
|
||||
let id = rhai_u32(id, "hitbox id")?;
|
||||
|
||||
if b.hitboxes.contains_key(&id) {
|
||||
return Err(format!("Hitbox with ID {} already existed!", id).into());
|
||||
}
|
||||
|
||||
let hitbox = attack::AttackHitbox {
|
||||
add_hitbox(
|
||||
&mc,
|
||||
id,
|
||||
bone: bone.to_owned(),
|
||||
id,
|
||||
bone,
|
||||
damage,
|
||||
offset,
|
||||
radius,
|
||||
@@ -233,28 +289,92 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
|
||||
knockback_type,
|
||||
knockback,
|
||||
knockback_growth,
|
||||
start_frame: b.cursor,
|
||||
end_frame: b.cursor,
|
||||
};
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
b.hitboxes.insert(id, hitbox);
|
||||
b.active_hitboxes.push(id);
|
||||
|
||||
Ok(())
|
||||
{
|
||||
let mc = mc.clone();
|
||||
engine.register_fn(
|
||||
"hitbox",
|
||||
move |id: i64,
|
||||
group: i64,
|
||||
bone: &str,
|
||||
damage: FighterDamage,
|
||||
offset: FGVec3,
|
||||
radius: FGi32,
|
||||
angle: AttackAngle,
|
||||
knockback_type: KnockbackType,
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32|
|
||||
-> Result<(), Box<EvalAltResult>> {
|
||||
add_hitbox(
|
||||
&mc,
|
||||
id,
|
||||
group,
|
||||
bone,
|
||||
damage,
|
||||
offset,
|
||||
radius,
|
||||
angle,
|
||||
knockback_type,
|
||||
knockback,
|
||||
knockback_growth,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
engine.run(&text)
|
||||
};
|
||||
|
||||
result
|
||||
let script = result
|
||||
.map(|_| {
|
||||
Rc::into_inner(mc)
|
||||
.unwrap()
|
||||
.into_inner()
|
||||
.into_attack_script()
|
||||
})
|
||||
.map_err(|err| err.to_string())?
|
||||
.map_err(|err| err.to_string())??;
|
||||
script.validate().map(|()| script)
|
||||
}
|
||||
|
||||
fn add_hitbox(
|
||||
mc: &Rc<RefCell<MoveCompiler>>,
|
||||
raw_id: i64,
|
||||
raw_group: i64,
|
||||
bone: &str,
|
||||
damage: FighterDamage,
|
||||
offset: FGVec3,
|
||||
radius: FGi32,
|
||||
angle: AttackAngle,
|
||||
knockback_type: KnockbackType,
|
||||
knockback: Knockback,
|
||||
knockback_growth: FGi32,
|
||||
) -> Result<(), Box<EvalAltResult>> {
|
||||
let mut b = mc.borrow_mut();
|
||||
let id = rhai_u32(raw_id, "hitbox id")?;
|
||||
let group = rhai_u32(raw_group, "hitbox group")?;
|
||||
if b.active_hitboxes.contains_key(&id) {
|
||||
return Err(format!("Hitbox with ID {} is already active!", id).into());
|
||||
}
|
||||
let hitbox_index = b.hitboxes.len();
|
||||
let frame = b.cursor;
|
||||
b.hitboxes.push(attack::AttackHitbox {
|
||||
id,
|
||||
group,
|
||||
bone: bone.to_owned(),
|
||||
damage,
|
||||
offset,
|
||||
radius,
|
||||
angle,
|
||||
knockback_type,
|
||||
knockback,
|
||||
knockback_growth,
|
||||
start_frame: frame,
|
||||
end_frame: frame,
|
||||
});
|
||||
b.active_hitboxes.insert(id, hitbox_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -316,4 +436,29 @@ mod tests {
|
||||
assert_eq!(script.hitboxes[0].start_frame, 1);
|
||||
assert_eq!(script.hitboxes[0].end_frame, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitbox_ids_can_be_reused_after_removal() {
|
||||
let script = compile_script(&format!(
|
||||
"frame(1); {HITBOX} frame(2); remove_hitbox(0); frame(4); {HITBOX} frame(5);"
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(script.hitboxes.len(), 2);
|
||||
assert_eq!(script.hitboxes[0].start_frame, 1);
|
||||
assert_eq!(script.hitboxes[0].end_frame, 2);
|
||||
assert_eq!(script.hitboxes[1].start_frame, 4);
|
||||
assert_eq!(script.hitboxes[1].end_frame, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_hitbox_group_is_compiled() {
|
||||
let script = compile_script(
|
||||
r#"frame(1); hitbox(7, 3, "hand_l", damage("1"), vec3("0", "0", "0"), fg32("1"), angle("0"), knockback_normal(), knockback("1"), fg32("1")); frame(2);"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(script.hitboxes[0].id, 7);
|
||||
assert_eq!(script.hitboxes[0].group, 3);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user