10 Commits

Author SHA1 Message Date
a620f18a55 run netcode host for dedicated server 2025-12-20 12:12:40 -05:00
38f7f5c30a dx 2025-12-20 12:11:05 -05:00
e9f53c11e9 allow local netcode multiplayer
* fix crashing when steam client not available
* allow more than 1 connected player
* disable settings persistence when in dbg
2025-12-20 12:08:52 -05:00
12c3cdc87b allow to choosing renet_netcode vs steam 2025-12-20 11:33:23 -05:00
930753170f cleanup 2025-12-19 22:30:21 -05:00
2c20b1efea fix plugin for server 2025-12-19 22:18:19 -05:00
1a632e729e set_rich_presence, more logging 2025-12-19 21:56:36 -05:00
22674822cc wrong condition 2025-12-19 20:31:10 -05:00
0f5a21995a Merge branch 'master' 2025-12-19 20:12:41 -05:00
4fb37e27c5 switch out renet netcode with renet_steam 2025-12-19 20:03:45 -05:00
56 changed files with 918 additions and 1358 deletions

View File

@@ -30,7 +30,7 @@ jobs:
cp target/x86_64-unknown-linux-gnu/release/hedz_reloaded ./ cp target/x86_64-unknown-linux-gnu/release/hedz_reloaded ./
tar -czf steamos.tar.gz hedz_reloaded tar -czf steamos.tar.gz hedz_reloaded
- uses: christopherhx/gitea-upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: steamos.tar.gz name: steamos.tar.gz
path: ./steamos.tar.gz path: ./steamos.tar.gz

View File

@@ -35,7 +35,7 @@ jobs:
cp target/x86_64-pc-windows-msvc/release/hedz_reloaded.exe ./ cp target/x86_64-pc-windows-msvc/release/hedz_reloaded.exe ./
tar -czf win.tar.gz hedz_reloaded.exe tar -czf win.tar.gz hedz_reloaded.exe
- uses: christopherhx/gitea-upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: win.tar.gz name: win.tar.gz
path: ./win.tar.gz path: ./win.tar.gz

View File

@@ -14,6 +14,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: cargo-bins/cargo-binstall@main - uses: cargo-bins/cargo-binstall@main
if: runner.os == 'linux'
- uses: extractions/setup-just@v1 - uses: extractions/setup-just@v1
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:

View File

@@ -38,7 +38,7 @@ jobs:
cp target/x86_64-unknown-linux-gnu/debug/hedz_reloaded target/x86_64-unknown-linux-gnu/debug/hedz_reloaded_server ./ cp target/x86_64-unknown-linux-gnu/debug/hedz_reloaded target/x86_64-unknown-linux-gnu/debug/hedz_reloaded_server ./
tar -czf steamos-debug.tar.gz hedz_reloaded hedz_reloaded_server tar -czf steamos-debug.tar.gz hedz_reloaded hedz_reloaded_server
- uses: christopherhx/gitea-upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: steamos-debug.tar.gz name: steamos-debug.tar.gz
path: ./steamos-debug.tar.gz path: ./steamos-debug.tar.gz

View File

@@ -32,7 +32,7 @@ jobs:
tar -czf hedz-macos.tar.gz hedz_reloaded tar -czf hedz-macos.tar.gz hedz_reloaded
ls -lisah hedz-macos.tar.gz ls -lisah hedz-macos.tar.gz
- uses: christopherhx/gitea-upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: hedz-macos name: hedz-macos
path: ./hedz-macos.tar.gz path: ./hedz-macos.tar.gz
@@ -118,7 +118,7 @@ jobs:
cp target/x86_64-unknown-linux-gnu/release/hedz_reloaded ./ cp target/x86_64-unknown-linux-gnu/release/hedz_reloaded ./
tar -czf steamos.tar.gz hedz_reloaded tar -czf steamos.tar.gz hedz_reloaded
- uses: christopherhx/gitea-upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: steamos.tar.gz name: steamos.tar.gz
path: ./steamos.tar.gz path: ./steamos.tar.gz

373
Cargo.lock generated
View File

@@ -102,7 +102,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"getrandom 0.3.4",
"once_cell", "once_cell",
"version_check", "version_check",
"zerocopy", "zerocopy",
@@ -513,19 +512,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "bevy-persistent"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e58d92d32bb99fa22ed46aeabe5212f5d1bc8952ebf9c49b5271fc06a1359f8"
dependencies = [
"bevy",
"gloo-storage",
"ron 0.11.0",
"serde",
"thiserror 2.0.17",
]
[[package]] [[package]]
name = "bevy-steamworks" name = "bevy-steamworks"
version = "0.15.0" version = "0.15.0"
@@ -581,7 +567,7 @@ dependencies = [
"bevy_utils", "bevy_utils",
"blake3", "blake3",
"derive_more", "derive_more",
"downcast-rs 2.0.2", "downcast-rs",
"either", "either",
"petgraph", "petgraph",
"ron 0.10.1", "ron 0.10.1",
@@ -641,7 +627,7 @@ dependencies = [
"cfg-if", "cfg-if",
"console_error_panic_hook", "console_error_panic_hook",
"ctrlc", "ctrlc",
"downcast-rs 2.0.2", "downcast-rs",
"log", "log",
"thiserror 2.0.17", "thiserror 2.0.17",
"variadics_please", "variadics_please",
@@ -672,7 +658,7 @@ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"derive_more", "derive_more",
"disqualified", "disqualified",
"downcast-rs 2.0.2", "downcast-rs",
"either", "either",
"futures-io", "futures-io",
"futures-lite", "futures-lite",
@@ -744,7 +730,6 @@ dependencies = [
"bevy_reflect", "bevy_reflect",
"bevy_transform", "bevy_transform",
"coreaudio-sys", "coreaudio-sys",
"cpal",
"rodio", "rodio",
"tracing", "tracing",
] ]
@@ -776,7 +761,7 @@ dependencies = [
"bevy_utils", "bevy_utils",
"bevy_window", "bevy_window",
"derive_more", "derive_more",
"downcast-rs 2.0.2", "downcast-rs",
"serde", "serde",
"smallvec", "smallvec",
"thiserror 2.0.17", "thiserror 2.0.17",
@@ -1179,7 +1164,6 @@ dependencies = [
"bevy_pbr", "bevy_pbr",
"bevy_picking", "bevy_picking",
"bevy_platform", "bevy_platform",
"bevy_post_process",
"bevy_ptr", "bevy_ptr",
"bevy_reflect", "bevy_reflect",
"bevy_render", "bevy_render",
@@ -1375,6 +1359,25 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "bevy_pkv"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "356a9c6fdc13faf7897103b43a8b84aafe24e1bbf1599df1fb00dc4e9b7055db"
dependencies = [
"bevy_app",
"bevy_ecs",
"cfg_aliases",
"directories",
"redb",
"rmp-serde",
"serde",
"serde_json",
"thiserror 2.0.17",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "bevy_platform" name = "bevy_platform"
version = "0.17.3" version = "0.17.3"
@@ -1396,36 +1399,6 @@ dependencies = [
"web-time", "web-time",
] ]
[[package]]
name = "bevy_post_process"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b857972f5d56b43b0dce2c843b75b64d5fbbd0f6177f6ecccd75e7e41f72deb"
dependencies = [
"bevy_app",
"bevy_asset",
"bevy_camera",
"bevy_color",
"bevy_core_pipeline",
"bevy_derive",
"bevy_ecs",
"bevy_image",
"bevy_math",
"bevy_platform",
"bevy_reflect",
"bevy_render",
"bevy_shader",
"bevy_transform",
"bevy_utils",
"bevy_window",
"bitflags 2.10.0",
"nonmax",
"radsort",
"smallvec",
"thiserror 2.0.17",
"tracing",
]
[[package]] [[package]]
name = "bevy_ptr" name = "bevy_ptr"
version = "0.17.3" version = "0.17.3"
@@ -1445,7 +1418,7 @@ dependencies = [
"bevy_utils", "bevy_utils",
"derive_more", "derive_more",
"disqualified", "disqualified",
"downcast-rs 2.0.2", "downcast-rs",
"erased-serde", "erased-serde",
"foldhash 0.2.0", "foldhash 0.2.0",
"glam 0.30.9", "glam 0.30.9",
@@ -1504,7 +1477,7 @@ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"bytemuck", "bytemuck",
"derive_more", "derive_more",
"downcast-rs 2.0.2", "downcast-rs",
"encase", "encase",
"fixedbitset", "fixedbitset",
"image", "image",
@@ -2154,18 +2127,6 @@ dependencies = [
"thiserror 1.0.69", "thiserror 1.0.69",
] ]
[[package]]
name = "calloop-wayland-source"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
dependencies = [
"calloop",
"rustix 0.38.44",
"wayland-backend",
"wayland-client",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.49" version = "1.2.49"
@@ -2820,10 +2781,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11277822c27bde750de02c5dc5159b91e88bf2661a2c1d98106f2fb1c5c6f590" checksum = "11277822c27bde750de02c5dc5159b91e88bf2661a2c1d98106f2fb1c5c6f590"
[[package]] [[package]]
name = "dirs" name = "directories"
version = "6.0.0" version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
dependencies = [ dependencies = [
"dirs-sys", "dirs-sys",
] ]
@@ -2899,12 +2860,6 @@ dependencies = [
"litrs", "litrs",
] ]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]] [[package]]
name = "downcast-rs" name = "downcast-rs"
version = "2.0.2" version = "2.0.2"
@@ -3518,34 +3473,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "gloo-storage"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
dependencies = [
"gloo-utils",
"js-sys",
"serde",
"serde_json",
"thiserror 1.0.69",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-utils"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "glow" name = "glow"
version = "0.16.0" version = "0.16.0"
@@ -3685,7 +3612,7 @@ dependencies = [
[[package]] [[package]]
name = "happy_feet" name = "happy_feet"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/PROMETHIA-27/happy_feet.git?rev=e3a4660e0b68f9bf4d6facb0fb4d93f2d7848b27#e3a4660e0b68f9bf4d6facb0fb4d93f2d7848b27" source = "git+https://github.com/rustunit/happy_feet.git?rev=919657fa#919657fa3330b3a78026c239c17122b3e3beefd7"
dependencies = [ dependencies = [
"avian3d", "avian3d",
"bevy", "bevy",
@@ -3754,19 +3681,18 @@ dependencies = [
"avian3d", "avian3d",
"bevy", "bevy",
"bevy-inspector-egui", "bevy-inspector-egui",
"bevy-persistent",
"bevy-steamworks", "bevy-steamworks",
"bevy_asset_loader", "bevy_asset_loader",
"bevy_ballistic", "bevy_ballistic",
"bevy_common_assets", "bevy_common_assets",
"bevy_debug_log", "bevy_debug_log",
"bevy_pkv",
"bevy_replicon", "bevy_replicon",
"bevy_replicon_renet", "bevy_replicon_renet",
"bevy_sprite3d", "bevy_sprite3d",
"bevy_trenchbroom", "bevy_trenchbroom",
"bevy_trenchbroom_avian", "bevy_trenchbroom_avian",
"clap", "clap",
"dirs",
"happy_feet", "happy_feet",
"nil 0.14.0", "nil 0.14.0",
"rand 0.8.5", "rand 0.8.5",
@@ -5066,7 +4992,7 @@ dependencies = [
"approx", "approx",
"arrayvec", "arrayvec",
"bitflags 2.10.0", "bitflags 2.10.0",
"downcast-rs 2.0.2", "downcast-rs",
"either", "either",
"ena", "ena",
"foldhash 0.2.0", "foldhash 0.2.0",
@@ -5097,7 +5023,7 @@ dependencies = [
"approx", "approx",
"arrayvec", "arrayvec",
"bitflags 2.10.0", "bitflags 2.10.0",
"downcast-rs 2.0.2", "downcast-rs",
"either", "either",
"ena", "ena",
"foldhash 0.2.0", "foldhash 0.2.0",
@@ -5383,15 +5309,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.37.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.42" version = "1.0.42"
@@ -5542,6 +5459,15 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb" checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb"
[[package]]
name = "redb"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae323eb086579a3769daa2c753bb96deb95993c534711e0dbe881b5192906a06"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.4.1" version = "0.4.1"
@@ -5661,6 +5587,28 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "rmp"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rmp-serde"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
dependencies = [
"byteorder",
"rmp",
"serde",
]
[[package]] [[package]]
name = "robust" name = "robust"
version = "1.2.0" version = "1.2.0"
@@ -5835,31 +5783,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sctk-adwaita"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec"
dependencies = [
"ab_glyph",
"log",
"memmap2",
"smithay-client-toolkit",
"tiny-skia",
]
[[package]] [[package]]
name = "self_cell" name = "self_cell"
version = "1.2.1" version = "1.2.1"
@@ -6024,31 +5953,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "smithay-client-toolkit"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
dependencies = [
"bitflags 2.10.0",
"calloop",
"calloop-wayland-source",
"cursor-icon",
"libc",
"log",
"memmap2",
"rustix 0.38.44",
"thiserror 1.0.69",
"wayland-backend",
"wayland-client",
"wayland-csd-frame",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-wlr",
"wayland-scanner",
"xkeysym",
]
[[package]] [[package]]
name = "smol_str" name = "smol_str"
version = "0.2.2" version = "0.2.2"
@@ -6128,12 +6032,6 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42862065c9e685d08cc3d9f6c609d4b46bd9684ec7e9420688eb979213469582" checksum = "42862065c9e685d08cc3d9f6c609d4b46bd9684ec7e9420688eb979213469582"
[[package]]
name = "strict-num"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@@ -6353,31 +6251,6 @@ dependencies = [
"time-core", "time-core",
] ]
[[package]]
name = "tiny-skia"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
dependencies = [
"arrayref",
"arrayvec",
"bytemuck",
"cfg-if",
"log",
"tiny-skia-path",
]
[[package]]
name = "tiny-skia-path"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
dependencies = [
"arrayref",
"bytemuck",
"strict-num",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.2" version = "0.8.2"
@@ -6852,114 +6725,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "wayland-backend"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35"
dependencies = [
"cc",
"downcast-rs 1.2.1",
"rustix 1.1.2",
"scoped-tls",
"smallvec",
"wayland-sys",
]
[[package]]
name = "wayland-client"
version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d"
dependencies = [
"bitflags 2.10.0",
"rustix 1.1.2",
"wayland-backend",
"wayland-scanner",
]
[[package]]
name = "wayland-csd-frame"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
dependencies = [
"bitflags 2.10.0",
"cursor-icon",
"wayland-backend",
]
[[package]]
name = "wayland-cursor"
version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29"
dependencies = [
"rustix 1.1.2",
"wayland-client",
"xcursor",
]
[[package]]
name = "wayland-protocols"
version = "0.32.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901"
dependencies = [
"bitflags 2.10.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-plasma"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
dependencies = [
"bitflags 2.10.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec"
dependencies = [
"bitflags 2.10.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.31.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3"
dependencies = [
"proc-macro2",
"quick-xml",
"quote",
]
[[package]]
name = "wayland-sys"
version = "0.31.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142"
dependencies = [
"dlib",
"log",
"pkg-config",
]
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.83" version = "0.3.83"
@@ -7638,7 +7403,6 @@ version = "0.30.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732"
dependencies = [ dependencies = [
"ahash",
"android-activity", "android-activity",
"atomic-waker", "atomic-waker",
"bitflags 2.10.0", "bitflags 2.10.0",
@@ -7653,7 +7417,6 @@ dependencies = [
"dpi", "dpi",
"js-sys", "js-sys",
"libc", "libc",
"memmap2",
"ndk 0.9.0", "ndk 0.9.0",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-app-kit 0.2.2", "objc2-app-kit 0.2.2",
@@ -7665,17 +7428,11 @@ dependencies = [
"raw-window-handle", "raw-window-handle",
"redox_syscall 0.4.1", "redox_syscall 0.4.1",
"rustix 0.38.44", "rustix 0.38.44",
"sctk-adwaita",
"smithay-client-toolkit",
"smol_str", "smol_str",
"tracing", "tracing",
"unicode-segmentation", "unicode-segmentation",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-plasma",
"web-sys", "web-sys",
"web-time", "web-time",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@@ -7746,12 +7503,6 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "xcursor"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
[[package]] [[package]]
name = "xkbcommon-dl" name = "xkbcommon-dl"
version = "0.4.2" version = "0.4.2"

View File

@@ -51,12 +51,15 @@ bevy = { version = "0.17.0", default-features = false, features = [
"track_location", "track_location",
] } ] }
bevy-inspector-egui = "0.34" bevy-inspector-egui = "0.34"
bevy-persistent = { version = "0.9", features = ["ron"] }
bevy-steamworks = "0.15.0" bevy-steamworks = "0.15.0"
bevy_asset_loader = "=0.24.0-rc.1" bevy_asset_loader = "=0.24.0-rc.1"
bevy_ballistic = { git = "https://github.com/rustunit/bevy_ballistic.git", rev = "b08ffec" } bevy_ballistic = { git = "https://github.com/rustunit/bevy_ballistic.git", rev = "b08ffec" }
bevy_common_assets = { version = "0.14.0", features = ["ron"] } bevy_common_assets = { version = "0.14.0", features = ["ron"] }
bevy_debug_log = { git = "https://github.com/rustunit/bevy_debug_log.git", rev = "86051a0" } bevy_debug_log = { git = "https://github.com/rustunit/bevy_debug_log.git", rev = "86051a0" }
bevy_pkv = { version = "0.14", default-features = false, features = [
"bevy",
"redb",
] }
bevy_replicon = "0.37.1" bevy_replicon = "0.37.1"
# TODO: i dont think we need this in dedicated server mode # TODO: i dont think we need this in dedicated server mode
bevy_replicon_renet = { version = "0.13.0", features = ["renet_steam"] } bevy_replicon_renet = { version = "0.13.0", features = ["renet_steam"] }
@@ -66,8 +69,7 @@ bevy_trenchbroom = { version = "0.10", default-features = false, features = [
] } ] }
bevy_trenchbroom_avian = "0.10" bevy_trenchbroom_avian = "0.10"
clap = { version = "=4.5.47", features = ["derive"] } clap = { version = "=4.5.47", features = ["derive"] }
dirs = "6.0.0" happy_feet = { git = "https://github.com/rustunit/happy_feet.git", rev = "919657fa", features = [
happy_feet = { git = "https://github.com/PROMETHIA-27/happy_feet.git", rev = "e3a4660e0b68f9bf4d6facb0fb4d93f2d7848b27", features = [
"serde", "serde",
] } ] }
nil = "0.14.0" nil = "0.14.0"

View File

@@ -26,19 +26,18 @@ dbg = ["avian3d/debug-plugin", "bevy/debug", "dep:bevy-inspector-egui"]
avian3d = { workspace = true } avian3d = { workspace = true }
bevy = { workspace = true } bevy = { workspace = true }
bevy-inspector-egui = { workspace = true, optional = true } bevy-inspector-egui = { workspace = true, optional = true }
bevy-persistent = { workspace = true }
bevy-steamworks = { workspace = true } bevy-steamworks = { workspace = true }
bevy_asset_loader = { workspace = true } bevy_asset_loader = { workspace = true }
bevy_ballistic = { workspace = true } bevy_ballistic = { workspace = true }
bevy_common_assets = { workspace = true } bevy_common_assets = { workspace = true }
bevy_debug_log = { workspace = true } bevy_debug_log = { workspace = true }
bevy_pkv = { workspace = true }
bevy_replicon = { workspace = true } bevy_replicon = { workspace = true }
bevy_replicon_renet = { workspace = true } bevy_replicon_renet = { workspace = true }
bevy_sprite3d = { workspace = true } bevy_sprite3d = { workspace = true }
bevy_trenchbroom = { workspace = true } bevy_trenchbroom = { workspace = true }
bevy_trenchbroom_avian = { workspace = true } bevy_trenchbroom_avian = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
dirs = { workspace = true }
happy_feet = { workspace = true } happy_feet = { workspace = true }
nil = { workspace = true } nil = { workspace = true }
rand = { workspace = true } rand = { workspace = true }

View File

@@ -1,11 +1,8 @@
use super::TriggerArrow; use super::TriggerArrow;
use crate::{ use crate::{
GameState, global_observer, GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
heads_database::HeadsDatabase, hitpoints::Hit, loading_assets::GameAssets, physics_layers::GameLayer,
hitpoints::Hit, utils::sprite_3d_animation::AnimationTimer,
loading_assets::GameAssets,
physics_layers::GameLayer,
utils::{Billboard, sprite_3d_animation::AnimationTimer},
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::{light::NotShadowCaster, prelude::*}; use bevy::{light::NotShadowCaster, prelude::*};

View File

@@ -1,14 +1,8 @@
use super::TriggerGun; use super::TriggerGun;
use crate::{ use crate::{
GameState, GameState, abilities::ProjectileId, billboards::Billboard, global_observer,
abilities::ProjectileId, heads_database::HeadsDatabase, hitpoints::Hit, loading_assets::GameAssets,
global_observer, physics_layers::GameLayer, tb_entities::EnemySpawn, utils::sprite_3d_animation::AnimationTimer,
heads_database::HeadsDatabase,
hitpoints::Hit,
loading_assets::GameAssets,
physics_layers::GameLayer,
tb_entities::EnemySpawn,
utils::{Billboard, sprite_3d_animation::AnimationTimer},
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::{light::NotShadowCaster, prelude::*}; use bevy::{light::NotShadowCaster, prelude::*};

View File

@@ -17,7 +17,7 @@ use crate::{
physics_layers::GameLayer, physics_layers::GameLayer,
player::Player, player::Player,
protocol::PlaySound, protocol::PlaySound,
utils::{Billboard, explosions::Explosion, sprite_3d_animation::AnimationTimer}, utils::{billboards::Billboard, explosions::Explosion, sprite_3d_animation::AnimationTimer},
}; };
use bevy::{light::NotShadowCaster, prelude::*}; use bevy::{light::NotShadowCaster, prelude::*};
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, Signature, ToClients}; use bevy_replicon::prelude::{SendMode, ServerTriggerExt, Signature, ToClients};

View File

@@ -1,6 +1,4 @@
use crate::{ use crate::{GameState, global_observer, loading_assets::UIAssets, utils::billboards::Billboard};
GameState, aim::MarkerEvent, global_observer, loading_assets::UIAssets, utils::Billboard,
};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_sprite3d::Sprite3d; use bevy_sprite3d::Sprite3d;
use ops::sin; use ops::sin;
@@ -9,6 +7,12 @@ use ops::sin;
#[reflect(Component)] #[reflect(Component)]
struct TargetMarker; struct TargetMarker;
#[derive(Event)]
pub enum MarkerEvent {
Spawn(Entity),
Despawn,
}
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_systems(Update, move_marker.run_if(in_state(GameState::Playing))); app.add_systems(Update, move_marker.run_if(in_state(GameState::Playing)));
global_observer!(app, marker_event); global_observer!(app, marker_event);

View File

@@ -1,15 +1,13 @@
mod marker;
mod target_ui;
use crate::{ use crate::{
GameState, GameState, control::Inputs, head::ActiveHead, heads_database::HeadsDatabase,
control::Inputs, hitpoints::Hitpoints, physics_layers::GameLayer, player::Player, tb_entities::EnemySpawn,
head::ActiveHead,
heads_database::HeadsDatabase,
hitpoints::Hitpoints,
physics_layers::GameLayer,
player::{LocalPlayer, Player},
tb_entities::EnemySpawn,
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::prelude::*; use bevy::prelude::*;
use marker::MarkerEvent;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::f32::consts::PI; use std::f32::consts::PI;
@@ -23,12 +21,7 @@ pub struct AimTarget(pub Option<Entity>);
pub struct AimState { pub struct AimState {
pub range: f32, pub range: f32,
pub max_angle: f32, pub max_angle: f32,
} pub spawn_marker: bool,
#[derive(Event)]
pub enum MarkerEvent {
Spawn(Entity),
Despawn,
} }
impl Default for AimState { impl Default for AimState {
@@ -36,6 +29,7 @@ impl Default for AimState {
Self { Self {
range: 80., range: 80.,
max_angle: PI / 8., max_angle: PI / 8.,
spawn_marker: true,
} }
} }
} }
@@ -44,12 +38,20 @@ pub fn plugin(app: &mut App) {
app.register_type::<AimState>(); app.register_type::<AimState>();
app.register_type::<AimTarget>(); app.register_type::<AimTarget>();
app.register_required_components::<ActiveHead, AimState>(); app.add_plugins(target_ui::plugin);
app.add_plugins(marker::plugin);
app.add_systems( app.add_systems(
Update, Update,
(update_player_aim, update_npc_aim, head_change).run_if(in_state(GameState::Playing)), (update_player_aim, update_npc_aim, head_change).run_if(in_state(GameState::Playing)),
); );
app.add_systems(Update, add_aim);
}
fn add_aim(mut commands: Commands, query: Query<Entity, Added<ActiveHead>>) {
for e in query.iter() {
commands.entity(e).insert(AimState::default());
}
} }
fn head_change( fn head_change(
@@ -68,19 +70,12 @@ fn update_player_aim(
mut commands: Commands, mut commands: Commands,
potential_targets: Query<(Entity, &Transform), With<Hitpoints>>, potential_targets: Query<(Entity, &Transform), With<Hitpoints>>,
mut player_aim: Query< mut player_aim: Query<
( (Entity, &AimState, &mut AimTarget, &GlobalTransform, &Inputs),
Entity,
&AimState,
&mut AimTarget,
&GlobalTransform,
&Inputs,
Has<LocalPlayer>,
),
With<Player>, With<Player>,
>, >,
spatial_query: SpatialQuery, spatial_query: SpatialQuery,
) { ) {
for (player, state, mut aim_target, global_tf, inputs, is_local) in player_aim.iter_mut() { for (player, state, mut aim_target, global_tf, inputs) in player_aim.iter_mut() {
let (player_pos, player_forward) = (global_tf.translation(), inputs.look_dir); let (player_pos, player_forward) = (global_tf.translation(), inputs.look_dir);
let mut new_target = None; let mut new_target = None;
@@ -119,14 +114,13 @@ fn update_player_aim(
} }
if new_target != aim_target.0 { if new_target != aim_target.0 {
if is_local { if state.spawn_marker {
if let Some(target) = new_target { if let Some(target) = new_target {
commands.trigger(MarkerEvent::Spawn(target)); commands.trigger(MarkerEvent::Spawn(target));
} else { } else {
commands.trigger(MarkerEvent::Despawn); commands.trigger(MarkerEvent::Despawn);
} }
} }
aim_target.0 = new_target; aim_target.0 = new_target;
} }
} }

View File

@@ -1,12 +1,12 @@
use super::AimTarget;
use crate::{ use crate::{
GameState, GameState,
aim::AimTarget, backpack::UiHeadState,
client::ui::{HeadsImages, UiHeadState}, heads::{ActiveHeads, HeadsImages},
heads::ActiveHeads,
hitpoints::Hitpoints, hitpoints::Hitpoints,
loading_assets::UIAssets, loading_assets::UIAssets,
npc::Npc, npc::Npc,
player::LocalPlayer, player::Player,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -18,14 +18,12 @@ struct HeadImage;
#[reflect(Component)] #[reflect(Component)]
struct HeadDamage; struct HeadDamage;
#[derive(Component, Default, PartialEq)] #[derive(Resource, Default, PartialEq)]
pub struct TargetUi { struct TargetUi {
head: Option<UiHeadState>, head: Option<UiHeadState>,
} }
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.register_required_components::<LocalPlayer, TargetUi>();
app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(Update, (sync, update).run_if(in_state(GameState::Playing))); app.add_systems(Update, (sync, update).run_if(in_state(GameState::Playing)));
} }
@@ -46,6 +44,8 @@ fn setup(mut commands: Commands, assets: Res<UIAssets>) {
assets.head_damage.clone(), assets.head_damage.clone(),
)], )],
)); ));
commands.insert_resource(TargetUi::default());
} }
fn spawn_head_ui(bg: Handle<Image>, regular: Handle<Image>, damage: Handle<Image>) -> impl Bundle { fn spawn_head_ui(bg: Handle<Image>, regular: Handle<Image>, damage: Handle<Image>) -> impl Bundle {
@@ -110,7 +110,7 @@ fn spawn_head_ui(bg: Handle<Image>, regular: Handle<Image>, damage: Handle<Image
} }
fn update( fn update(
target: Single<&TargetUi, (Changed<TargetUi>, With<LocalPlayer>)>, target: Res<TargetUi>,
heads_images: Res<HeadsImages>, heads_images: Res<HeadsImages>,
mut head_image: Query< mut head_image: Query<
(&mut Visibility, &mut ImageNode), (&mut Visibility, &mut ImageNode),
@@ -118,6 +118,7 @@ fn update(
>, >,
mut head_damage: Query<&mut Node, (With<HeadDamage>, Without<HeadImage>)>, mut head_damage: Query<&mut Node, (With<HeadDamage>, Without<HeadImage>)>,
) { ) {
if target.is_changed() {
if let Ok((mut vis, mut image)) = head_image.single_mut() { if let Ok((mut vis, mut image)) = head_image.single_mut() {
if let Some(head) = target.head { if let Some(head) = target.head {
*vis = Visibility::Visible; *vis = Visibility::Visible;
@@ -130,16 +131,17 @@ fn update(
if let Ok(mut node) = head_damage.single_mut() { if let Ok(mut node) = head_damage.single_mut() {
node.height = Val::Percent(target.head.map(|head| head.damage()).unwrap_or(0.) * 100.); node.height = Val::Percent(target.head.map(|head| head.damage()).unwrap_or(0.) * 100.);
} }
}
} }
fn sync( fn sync(
mut target: Single<&mut TargetUi, With<LocalPlayer>>, mut target: ResMut<TargetUi>,
player_target: Single<&AimTarget, With<LocalPlayer>>, player_target: Query<&AimTarget, With<Player>>,
target_data: Query<(&Hitpoints, &ActiveHeads), With<Npc>>, target_data: Query<(&Hitpoints, &ActiveHeads), With<Npc>>,
) { ) {
let mut new_state = None; let mut new_state = None;
if let Some(target) = player_target.0 if let Some(e) = player_target.iter().next().and_then(|target| target.0)
&& let Ok((hp, heads)) = target_data.get(target) && let Ok((hp, heads)) = target_data.get(e)
{ {
let head = heads.current().expect("target must have a head on"); let head = heads.current().expect("target must have a head on");
new_state = Some(UiHeadState { new_state = Some(UiHeadState {

View File

@@ -0,0 +1,39 @@
use super::UiHeadState;
use bevy::prelude::*;
pub static BACKPACK_HEAD_SLOTS: usize = 5;
#[derive(Component, Default)]
pub struct BackpackMarker;
#[derive(Component, Default)]
pub struct BackpackCountText;
#[derive(Component, Default)]
pub struct HeadSelector(pub usize);
#[derive(Component, Default)]
pub struct HeadImage(pub usize);
#[derive(Component, Default)]
pub struct HeadDamage(pub usize);
#[derive(Component, Default, Debug, Reflect)]
#[reflect(Component, Default)]
pub struct BackpackUiState {
pub heads: [Option<UiHeadState>; 5],
pub scroll: usize,
pub count: usize,
pub current_slot: usize,
pub open: bool,
}
impl BackpackUiState {
pub fn relative_current_slot(&self) -> usize {
self.current_slot.saturating_sub(self.scroll)
}
}
pub fn plugin(app: &mut App) {
app.register_type::<BackpackUiState>();
}

View File

@@ -6,7 +6,13 @@ use crate::{
heads_database::HeadsDatabase, heads_database::HeadsDatabase,
}; };
use bevy::prelude::*; use bevy::prelude::*;
#[cfg(feature = "client")]
use bevy_replicon::prelude::ClientTriggerExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub use ui_head_state::UiHeadState;
pub mod backpack_ui;
pub mod ui_head_state;
#[derive(Component, Default, Reflect, Serialize, Deserialize, PartialEq)] #[derive(Component, Default, Reflect, Serialize, Deserialize, PartialEq)]
#[reflect(Component)] #[reflect(Component)]
@@ -34,15 +40,120 @@ impl Backpack {
} }
} }
#[derive(Event, Debug, Serialize, Deserialize)] #[derive(Event, Serialize, Deserialize)]
pub struct BackpackSwapEvent(pub usize); pub struct BackpackSwapEvent(pub usize);
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.register_type::<Backpack>(); app.register_type::<Backpack>();
app.add_plugins(backpack_ui::plugin);
#[cfg(feature = "client")]
app.add_systems(FixedUpdate, (backpack_inputs, sync_on_change));
global_observer!(app, on_head_collect); global_observer!(app, on_head_collect);
} }
#[cfg(feature = "client")]
fn backpack_inputs(
backpacks: Single<
(&Backpack, &mut backpack_ui::BackpackUiState),
With<crate::player::LocalPlayer>,
>,
mut backpack_inputs: MessageReader<crate::control::BackpackButtonPress>,
mut commands: Commands,
time: Res<Time>,
) {
use crate::{control::BackpackButtonPress, protocol::PlaySound};
let (backpack, mut state) = backpacks.into_inner();
for input in backpack_inputs.read() {
match input {
BackpackButtonPress::Toggle => {
if state.count == 0 {
return;
}
state.open = !state.open;
commands.trigger(PlaySound::Backpack { open: state.open });
}
BackpackButtonPress::Swap => {
if !state.open {
return;
}
commands.client_trigger(BackpackSwapEvent(state.current_slot));
}
BackpackButtonPress::Left => {
if !state.open {
return;
}
if state.current_slot > 0 {
state.current_slot -= 1;
commands.trigger(PlaySound::Selection);
sync_backpack_ui(backpack, &mut state, time.elapsed_secs());
}
}
BackpackButtonPress::Right => {
if !state.open {
return;
}
if state.current_slot < state.count.saturating_sub(1) {
state.current_slot += 1;
commands.trigger(PlaySound::Selection);
sync_backpack_ui(backpack, &mut state, time.elapsed_secs());
}
}
}
}
}
#[cfg(feature = "client")]
fn sync_on_change(
backpack: Query<Ref<Backpack>>,
mut state: Single<&mut backpack_ui::BackpackUiState>,
time: Res<Time>,
) {
for backpack in backpack.iter() {
if backpack.is_changed() || backpack.reloading() {
sync_backpack_ui(&backpack, &mut state, time.elapsed_secs());
}
}
}
#[cfg(feature = "client")]
fn sync_backpack_ui(backpack: &Backpack, state: &mut backpack_ui::BackpackUiState, time: f32) {
use crate::backpack::backpack_ui::BACKPACK_HEAD_SLOTS;
state.count = backpack.heads.len();
state.scroll = state
.scroll
.min(state.count.saturating_sub(BACKPACK_HEAD_SLOTS));
if state.current_slot >= state.scroll + BACKPACK_HEAD_SLOTS {
state.scroll = state.current_slot.saturating_sub(BACKPACK_HEAD_SLOTS - 1);
}
if state.current_slot < state.scroll {
state.scroll = state.current_slot;
}
for i in 0..BACKPACK_HEAD_SLOTS {
if let Some(head) = backpack.heads.get(i + state.scroll) {
use crate::backpack::ui_head_state::UiHeadState;
state.heads[i] = Some(UiHeadState::new(*head, time));
} else {
state.heads[i] = None;
}
}
}
fn on_head_collect( fn on_head_collect(
trigger: On<HeadCollected>, trigger: On<HeadCollected>,
mut cmds: Commands, mut cmds: Commands,
@@ -54,7 +165,7 @@ fn on_head_collect(
let (mut backpack, active_heads) = query.get_mut(entity)?; let (mut backpack, active_heads) = query.get_mut(entity)?;
if backpack.contains(head) || active_heads.contains(head) { if backpack.contains(head) || active_heads.contains(head) {
cmds.trigger(CashCollectEvent { entity }); cmds.trigger(CashCollectEvent);
} else { } else {
backpack.insert(head, heads_db.as_ref()); backpack.insert(head, heads_db.as_ref());
} }

View File

@@ -0,0 +1,40 @@
use crate::heads::HeadState;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Reflect, Default, Serialize, Deserialize)]
pub struct UiHeadState {
pub head: usize,
pub health: f32,
pub ammo: f32,
pub reloading: Option<f32>,
}
impl UiHeadState {
pub fn damage(&self) -> f32 {
1. - self.health
}
pub fn ammo_used(&self) -> f32 {
1. - self.ammo
}
pub fn reloading(&self) -> Option<f32> {
self.reloading
}
pub fn new(value: HeadState, time: f32) -> Self {
let reloading = if value.has_ammo() {
None
} else {
Some((time - value.last_use) / value.reload_duration)
};
Self {
head: value.head,
ammo: value.ammo as f32 / value.ammo_max as f32,
health: value.health as f32 / value.health_max as f32,
reloading,
}
}
}

View File

@@ -1,3 +1,18 @@
use crate::GameState;
#[cfg(feature = "client")]
use crate::control::Inputs;
#[cfg(feature = "client")]
use crate::physics_layers::GameLayer;
#[cfg(feature = "client")]
use crate::player::LocalPlayer;
#[cfg(feature = "client")]
use crate::{control::LookDirMovement, loading_assets::UIAssets};
#[cfg(feature = "client")]
use avian3d::prelude::SpatialQuery;
#[cfg(feature = "client")]
use avian3d::prelude::{
Collider, LayerMask, PhysicsLayer as _, ShapeCastConfig, SpatialQueryFilter,
};
use bevy::prelude::*; use bevy::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -6,3 +21,173 @@ pub struct CameraTarget;
#[derive(Component, Reflect, Debug, Serialize, Deserialize, PartialEq)] #[derive(Component, Reflect, Debug, Serialize, Deserialize, PartialEq)]
pub struct CameraArmRotation; pub struct CameraArmRotation;
/// Requested camera rotation based on various input sources (keyboard, gamepad)
#[derive(Component, Reflect, Debug, Default, Deref, DerefMut)]
#[reflect(Component)]
pub struct CameraRotationInput(pub Vec2);
#[derive(Resource, Reflect, Debug, Default)]
#[reflect(Resource)]
pub struct CameraState {
pub cutscene: bool,
pub look_around: bool,
}
#[derive(Component, Reflect, Debug, Default)]
struct CameraUi;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct MainCamera {
pub enabled: bool,
dir: Dir3,
distance: f32,
target_offset: Vec3,
}
impl MainCamera {
fn new(arm: Vec3) -> Self {
let (dir, distance) = Dir3::new_and_length(arm).expect("invalid arm length");
Self {
enabled: true,
dir,
distance,
target_offset: Vec3::new(0., 2., 0.),
}
}
}
pub fn plugin(app: &mut App) {
app.register_type::<CameraRotationInput>();
app.register_type::<CameraState>();
app.register_type::<MainCamera>();
app.init_resource::<CameraState>();
app.add_systems(OnEnter(GameState::Playing), startup);
#[cfg(feature = "client")]
app.add_systems(
PostUpdate,
(update, update_ui, update_look_around, rotate_view).run_if(in_state(GameState::Playing)),
);
}
fn startup(mut commands: Commands) {
commands.spawn((
Camera3d::default(),
MainCamera::new(Vec3::new(0., 1.8, 15.)),
CameraRotationInput::default(),
));
}
#[cfg(feature = "client")]
fn update_look_around(
inputs: Single<&Inputs, With<LocalPlayer>>,
mut cam_state: ResMut<CameraState>,
) {
let look_around = inputs.view_mode;
if look_around != cam_state.look_around {
cam_state.look_around = look_around;
}
}
#[cfg(feature = "client")]
fn update_ui(
mut commands: Commands,
cam_state: Res<CameraState>,
assets: Res<UIAssets>,
query: Query<Entity, With<CameraUi>>,
) {
if cam_state.is_changed() {
let show_ui = cam_state.look_around || cam_state.cutscene;
if show_ui {
commands.spawn((
CameraUi,
Node {
margin: UiRect::top(Val::Px(20.))
.with_left(Val::Auto)
.with_right(Val::Auto),
justify_content: JustifyContent::Center,
..default()
},
children![(
Node {
display: Display::Block,
position_type: PositionType::Absolute,
..default()
},
ImageNode::new(assets.camera.clone()),
)],
));
} else {
for entity in query.iter() {
commands.entity(entity).despawn();
}
}
}
}
#[cfg(feature = "client")]
fn update(
mut cam: Query<
(&MainCamera, &mut Transform, &CameraRotationInput),
(Without<CameraTarget>, Without<CameraArmRotation>),
>,
target_q: Single<&Transform, (With<CameraTarget>, Without<CameraArmRotation>)>,
arm_rotation: Single<&Transform, With<CameraArmRotation>>,
spatial_query: SpatialQuery,
cam_state: Res<CameraState>,
) {
if cam_state.cutscene {
return;
}
let arm_tf = arm_rotation;
let Ok((camera, mut cam_transform, cam_rotation_input)) = cam.single_mut() else {
return;
};
if !camera.enabled {
return;
}
let target = target_q.translation + camera.target_offset;
let direction = arm_tf.rotation * Quat::from_rotation_y(cam_rotation_input.x) * camera.dir;
let max_distance = camera.distance;
let filter = SpatialQueryFilter::from_mask(LayerMask(GameLayer::Level.to_bits()));
let cam_pos = if let Some(first_hit) = spatial_query.cast_shape(
&Collider::sphere(0.5),
target,
Quat::IDENTITY,
direction,
&ShapeCastConfig::from_max_distance(max_distance),
&filter,
) {
let distance = first_hit.distance;
target + (direction * distance)
} else {
target + (direction * camera.distance)
};
*cam_transform = Transform::from_translation(cam_pos).looking_at(target, Vec3::Y);
}
#[cfg(feature = "client")]
fn rotate_view(
inputs: Single<&Inputs, With<LocalPlayer>>,
look_dir: Res<LookDirMovement>,
mut cam: Single<&mut CameraRotationInput>,
) {
if !inputs.view_mode {
cam.x = 0.0;
return;
}
cam.0 += look_dir.0 * -0.001;
}

View File

@@ -1,17 +1,12 @@
use crate::{ use crate::{
GameState, global_observer, GameState, HEDZ_GREEN, global_observer, loading_assets::UIAssets, protocol::PlaySound,
physics_layers::GameLayer,
player::Player,
protocol::{GltfSceneRoot, PlaySound, is_server},
server_observer, server_observer,
tb_entities::CashSpawn,
}; };
use avian3d::prelude::*; use avian3d::prelude::Rotation;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_replicon::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Component, Reflect, Default, Deserialize, Serialize)] #[derive(Component, Reflect, Default)]
#[reflect(Component)] #[reflect(Component)]
#[require(Transform)] #[require(Transform)]
pub struct Cash; pub struct Cash;
@@ -25,64 +20,32 @@ pub struct CashInventory {
pub cash: i32, pub cash: i32,
} }
#[derive(EntityEvent)] #[derive(Event)]
pub struct CashCollectEvent { pub struct CashCollectEvent;
pub entity: Entity,
}
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Playing), setup.run_if(is_server)); app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(Update, rotate.run_if(in_state(GameState::Playing))); app.add_systems(
Update,
(rotate, update_ui).run_if(in_state(GameState::Playing)),
);
server_observer!(app, on_cash_collected); server_observer!(app, on_cash_collect);
} }
fn setup(mut commands: Commands, query: Query<Entity, With<CashSpawn>>) { fn on_cash_collect(
for entity in query.iter() { _trigger: On<CashCollectEvent>,
commands
.entity(entity)
.insert((
Name::new("cash"),
GltfSceneRoot::Cash,
Cash,
Collider::cuboid(2., 3.0, 2.),
CollisionLayers::new(GameLayer::CollectibleSensors, LayerMask::ALL),
RigidBody::Kinematic,
CollisionEventsEnabled,
Sensor,
Replicated,
))
.observe(on_cash_collision);
}
}
fn on_cash_collected(
trigger: On<CashCollectEvent>,
mut commands: Commands, mut commands: Commands,
mut query_player: Query<&mut CashInventory, With<Player>>, mut cash: Single<&mut CashInventory>,
) { ) {
if let Ok(mut cash) = query_player.get_mut(trigger.entity) { use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
commands.server_trigger(ToClients { commands.server_trigger(ToClients {
mode: SendMode::Broadcast, mode: SendMode::Broadcast,
message: PlaySound::CashCollect, message: PlaySound::CashCollect,
}); });
cash.cash += 100; cash.cash += 100;
}
}
fn on_cash_collision(
trigger: On<CollisionStart>,
mut commands: Commands,
query_player: Query<&Player>,
) {
let collectable = trigger.event().collider1;
let collider = trigger.event().collider2;
if query_player.contains(collider) {
commands.trigger(CashCollectEvent { entity: collider });
commands.entity(collectable).despawn();
}
} }
fn rotate(time: Res<Time>, mut query: Query<&mut Rotation, With<Cash>>) { fn rotate(time: Res<Time>, mut query: Query<&mut Rotation, With<Cash>>) {
@@ -92,3 +55,37 @@ fn rotate(time: Res<Time>, mut query: Query<&mut Rotation, With<Cash>>) {
.mul_quat(Quat::from_rotation_y(time.delta_secs())); .mul_quat(Quat::from_rotation_y(time.delta_secs()));
} }
} }
fn update_ui(
cash: Single<&CashInventory, Changed<CashInventory>>,
text: Query<Entity, With<CashText>>,
mut writer: TextUiWriter,
) {
let Some(text) = text.iter().next() else {
return;
};
*writer.text(text, 0) = cash.cash.to_string();
}
fn setup(mut commands: Commands, assets: Res<UIAssets>) {
commands.spawn((
Name::new("cash-ui"),
Text::new("0"),
TextShadow::default(),
CashText,
TextFont {
font: assets.font.clone(),
font_size: 34.0,
..default()
},
TextColor(HEDZ_GREEN.into()),
TextLayout::new_with_justify(Justify::Center),
Node {
position_type: PositionType::Absolute,
bottom: Val::Px(40.0),
left: Val::Px(100.0),
..default()
},
));
}

View File

@@ -1,8 +0,0 @@
use bevy::prelude::*;
pub mod marker;
pub mod target_ui;
pub fn plugin(app: &mut App) {
app.add_plugins((marker::plugin, target_ui::plugin));
}

View File

@@ -1,98 +0,0 @@
use crate::{
backpack::{Backpack, BackpackSwapEvent},
client::ui::{BACKPACK_HEAD_SLOTS, BackpackUiState, UiHeadState},
control::BackpackButtonPress,
player::LocalPlayer,
protocol::PlaySound,
};
use bevy::prelude::*;
use bevy_replicon::prelude::ClientTriggerExt;
pub fn plugin(app: &mut App) {
app.add_systems(FixedUpdate, (backpack_inputs, sync_on_change));
}
fn backpack_inputs(
backpack: Single<&Backpack, With<LocalPlayer>>,
mut state: ResMut<BackpackUiState>,
mut backpack_inputs: MessageReader<crate::control::BackpackButtonPress>,
mut commands: Commands,
time: Res<Time>,
) {
for input in backpack_inputs.read() {
match input {
BackpackButtonPress::Toggle => {
if state.count == 0 {
return;
}
state.open = !state.open;
commands.trigger(PlaySound::Backpack { open: state.open });
}
BackpackButtonPress::Swap => {
if !state.open {
return;
}
commands.client_trigger(BackpackSwapEvent(state.current_slot));
}
BackpackButtonPress::Left => {
if !state.open {
return;
}
if state.current_slot > 0 {
state.current_slot -= 1;
commands.trigger(PlaySound::Selection);
sync_backpack_ui(&backpack, &mut state, time.elapsed_secs());
}
}
BackpackButtonPress::Right => {
if !state.open {
return;
}
if state.current_slot < state.count.saturating_sub(1) {
state.current_slot += 1;
commands.trigger(PlaySound::Selection);
sync_backpack_ui(&backpack, &mut state, time.elapsed_secs());
}
}
}
}
}
fn sync_on_change(
backpack: Single<Ref<Backpack>, With<LocalPlayer>>,
mut state: ResMut<BackpackUiState>,
time: Res<Time>,
) {
if backpack.is_changed() || backpack.reloading() {
sync_backpack_ui(&backpack, &mut state, time.elapsed_secs());
}
}
fn sync_backpack_ui(backpack: &Backpack, state: &mut BackpackUiState, time: f32) {
state.count = backpack.heads.len();
state.scroll = state
.scroll
.min(state.count.saturating_sub(BACKPACK_HEAD_SLOTS));
if state.current_slot >= state.scroll + BACKPACK_HEAD_SLOTS {
state.scroll = state.current_slot.saturating_sub(BACKPACK_HEAD_SLOTS - 1);
}
if state.current_slot < state.scroll {
state.scroll = state.current_slot;
}
for i in 0..BACKPACK_HEAD_SLOTS {
if let Some(head) = backpack.heads.get(i + state.scroll) {
state.heads[i] = Some(UiHeadState::new(*head, time));
} else {
state.heads[i] = None;
}
}
}

View File

@@ -1,47 +1,15 @@
use crate::{ use crate::{
GameState, HEDZ_GREEN, GameState, HEDZ_GREEN,
client::ui::heads_ui::{HeadsImages, UiHeadState}, backpack::backpack_ui::{
BACKPACK_HEAD_SLOTS, BackpackCountText, BackpackMarker, BackpackUiState, HeadDamage,
HeadImage, HeadSelector,
},
heads::HeadsImages,
loading_assets::UIAssets, loading_assets::UIAssets,
}; };
use bevy::{ecs::spawn::SpawnIter, prelude::*}; use bevy::{ecs::spawn::SpawnIter, prelude::*};
pub static BACKPACK_HEAD_SLOTS: usize = 5;
#[derive(Component, Default)]
pub struct BackpackMarker;
#[derive(Component, Default)]
pub struct BackpackCountText;
#[derive(Component, Default)]
pub struct HeadSelector(pub usize);
#[derive(Component, Default)]
pub struct HeadImage(pub usize);
#[derive(Component, Default)]
pub struct HeadDamage(pub usize);
#[derive(Resource, Default, Debug, Reflect)]
#[reflect(Resource)]
pub struct BackpackUiState {
pub heads: [Option<UiHeadState>; 5],
pub scroll: usize,
pub count: usize,
pub current_slot: usize,
pub open: bool,
}
impl BackpackUiState {
pub fn relative_current_slot(&self) -> usize {
self.current_slot.saturating_sub(self.scroll)
}
}
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.register_type::<BackpackUiState>();
app.init_resource::<BackpackUiState>();
app.add_systems(OnEnter(GameState::Playing), setup); app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems( app.add_systems(
FixedUpdate, FixedUpdate,
@@ -184,14 +152,10 @@ fn spawn_head_ui(
} }
fn update_visibility( fn update_visibility(
state: Res<BackpackUiState>, state: Single<&BackpackUiState, Changed<BackpackUiState>>,
mut backpack: Single<&mut Visibility, (With<BackpackMarker>, Without<BackpackCountText>)>, mut backpack: Single<&mut Visibility, (With<BackpackMarker>, Without<BackpackCountText>)>,
mut count: Single<&mut Visibility, (Without<BackpackMarker>, With<BackpackCountText>)>, mut count: Single<&mut Visibility, (Without<BackpackMarker>, With<BackpackCountText>)>,
) { ) {
if !state.is_changed() {
return;
}
**backpack = if state.open { **backpack = if state.open {
Visibility::Visible Visibility::Visible
} else { } else {
@@ -206,14 +170,10 @@ fn update_visibility(
} }
fn update_count( fn update_count(
state: Res<BackpackUiState>, state: Single<&BackpackUiState, Changed<BackpackUiState>>,
text: Option<Single<Entity, With<BackpackCountText>>>, text: Option<Single<Entity, With<BackpackCountText>>>,
mut writer: TextUiWriter, mut writer: TextUiWriter,
) { ) {
if !state.is_changed() {
return;
}
let Some(text) = text else { let Some(text) = text else {
return; return;
}; };
@@ -222,16 +182,12 @@ fn update_count(
} }
fn update( fn update(
state: Res<BackpackUiState>, state: Single<&BackpackUiState, Changed<BackpackUiState>>,
heads_images: Res<HeadsImages>, heads_images: Res<HeadsImages>,
mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without<HeadSelector>>, mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without<HeadSelector>>,
mut head_damage: Query<(&HeadDamage, &mut Node), Without<HeadSelector>>, mut head_damage: Query<(&HeadDamage, &mut Node), Without<HeadSelector>>,
mut head_selector: Query<(&HeadSelector, &mut Visibility), Without<HeadImage>>, mut head_selector: Query<(&HeadSelector, &mut Visibility), Without<HeadImage>>,
) { ) {
if !state.is_changed() {
return;
}
for (HeadImage(head), mut vis, mut image) in head_image.iter_mut() { for (HeadImage(head), mut vis, mut image) in head_image.iter_mut() {
if let Some(head) = &state.heads[*head] { if let Some(head) = &state.heads[*head] {
*vis = Visibility::Inherited; *vis = Visibility::Inherited;

View File

@@ -0,0 +1,7 @@
pub mod backpack_ui;
use bevy::prelude::*;
pub fn plugin(app: &mut App) {
app.add_plugins(backpack_ui::plugin);
}

View File

@@ -1,192 +0,0 @@
use crate::{
GameState,
camera::{CameraArmRotation, CameraTarget},
control::{Inputs, LookDirMovement, ViewMode},
loading_assets::UIAssets,
physics_layers::GameLayer,
player::LocalPlayer,
};
use avian3d::prelude::{
Collider, LayerMask, PhysicsLayer as _, ShapeCastConfig, SpatialQuery, SpatialQueryFilter,
};
use bevy::prelude::*;
/// Requested camera rotation based on various input sources (keyboard, gamepad)
#[derive(Component, Reflect, Debug, Default, Deref, DerefMut)]
#[reflect(Component)]
pub struct CameraRotationInput(pub Vec2);
#[derive(Resource, Reflect, Debug, Default)]
#[reflect(Resource)]
pub struct CameraState {
pub cutscene: bool,
pub view_mode: ViewMode,
}
#[derive(Component, Reflect, Debug, Default)]
struct CameraUi;
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct MainCamera {
pub enabled: bool,
dir: Dir3,
distance: f32,
target_offset: Vec3,
}
impl MainCamera {
fn new(arm: Vec3) -> Self {
let (dir, distance) = Dir3::new_and_length(arm).expect("invalid arm length");
Self {
enabled: true,
dir,
distance,
target_offset: Vec3::new(0., 2., 0.),
}
}
}
pub fn plugin(app: &mut App) {
app.register_type::<CameraRotationInput>();
app.register_type::<CameraState>();
app.register_type::<MainCamera>();
app.init_resource::<CameraState>();
app.add_systems(OnEnter(GameState::Playing), startup);
app.add_systems(
PostUpdate,
(update, update_ui, update_look_around, rotate_view).run_if(in_state(GameState::Playing)),
);
}
fn startup(mut commands: Commands) {
commands.spawn((
Camera3d::default(),
MainCamera::new(Vec3::new(0., 1.8, 15.)),
CameraRotationInput::default(),
));
}
#[cfg(feature = "client")]
fn update_look_around(
inputs: Single<&Inputs, With<LocalPlayer>>,
mut cam_state: ResMut<CameraState>,
) {
let view_mode = inputs.view_mode;
if view_mode != cam_state.view_mode {
cam_state.view_mode = view_mode;
}
}
#[cfg(feature = "client")]
fn update_ui(
mut commands: Commands,
cam_state: Res<CameraState>,
assets: Res<UIAssets>,
query: Query<Entity, With<CameraUi>>,
) {
if cam_state.is_changed() {
let show_free_cam_ui = cam_state.view_mode.is_free() || cam_state.cutscene;
if show_free_cam_ui {
commands.spawn((
CameraUi,
Node {
margin: UiRect::top(Val::Px(20.))
.with_left(Val::Auto)
.with_right(Val::Auto),
justify_content: JustifyContent::Center,
..default()
},
children![(
Node {
display: Display::Block,
position_type: PositionType::Absolute,
..default()
},
ImageNode::new(assets.camera.clone()),
)],
));
} else {
for entity in query.iter() {
commands.entity(entity).despawn();
}
}
}
}
fn update(
cam: Single<
(&MainCamera, &mut Transform, &CameraRotationInput),
(Without<CameraTarget>, Without<CameraArmRotation>),
>,
target_q: Single<
(&Transform, &Children),
(
With<CameraTarget>,
With<LocalPlayer>,
Without<CameraArmRotation>,
),
>,
arm_rotation: Query<&Transform, With<CameraArmRotation>>,
spatial_query: SpatialQuery,
cam_state: Res<CameraState>,
) {
if cam_state.cutscene {
return;
}
let (camera, mut cam_transform, cam_rotation_input) = cam.into_inner();
let (target_q, children) = target_q.into_inner();
let arm_tf = children
.iter()
.find_map(|child| arm_rotation.get(child).ok())
.unwrap();
if !camera.enabled {
return;
}
let target = target_q.translation + camera.target_offset;
let direction = arm_tf.rotation * Quat::from_rotation_y(cam_rotation_input.x) * camera.dir;
let max_distance = camera.distance;
let filter = SpatialQueryFilter::from_mask(LayerMask(GameLayer::Level.to_bits()));
let cam_pos = if let Some(first_hit) = spatial_query.cast_shape(
&Collider::sphere(0.5),
target,
Quat::IDENTITY,
direction,
&ShapeCastConfig::from_max_distance(max_distance),
&filter,
) {
let distance = first_hit.distance;
target + (direction * distance)
} else {
target + (direction * camera.distance)
};
*cam_transform = Transform::from_translation(cam_pos).looking_at(target, Vec3::Y);
}
#[cfg(feature = "client")]
fn rotate_view(
inputs: Single<&Inputs, With<LocalPlayer>>,
look_dir: Res<LookDirMovement>,
mut cam: Single<&mut CameraRotationInput>,
) {
if !inputs.view_mode.is_free() {
cam.x = 0.0;
return;
}
cam.0 += look_dir.0 * -0.001;
}

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
GameState, GameState,
control::{ControllerSet, Inputs, LookDirMovement, SelectedController}, control::{ControllerSet, Inputs, LookDirMovement},
player::{LocalPlayer, PlayerBodyMesh}, player::{LocalPlayer, PlayerBodyMesh},
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -19,20 +19,14 @@ pub fn plugin(app: &mut App) {
fn rotate_rig( fn rotate_rig(
inputs: Single<&Inputs, With<LocalPlayer>>, inputs: Single<&Inputs, With<LocalPlayer>>,
look_dir: Res<LookDirMovement>, look_dir: Res<LookDirMovement>,
local_player: Single<(&Children, &SelectedController), With<LocalPlayer>>, local_player: Single<&Children, With<LocalPlayer>>,
mut player_mesh: Query<&mut Transform, With<PlayerBodyMesh>>, mut player_mesh: Query<&mut Transform, With<PlayerBodyMesh>>,
) { ) {
if inputs.view_mode.is_free() { if inputs.view_mode {
return; return;
} }
let (local_player_children, selected_controller) = *local_player; local_player.iter().find(|&child| {
if !matches!(selected_controller, SelectedController::Flying) {
return;
}
local_player_children.iter().find(|&child| {
if let Ok(mut rig_transform) = player_mesh.get_mut(child) { if let Ok(mut rig_transform) = player_mesh.get_mut(child) {
let look_dir = look_dir.0; let look_dir = look_dir.0;

View File

@@ -3,7 +3,7 @@ use crate::{
client::control::CharacterInputEnabled, client::control::CharacterInputEnabled,
control::{ control::{
BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs, BackpackButtonPress, CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs,
LookDirMovement, SelectLeftPressed, SelectRightPressed, ViewMode, LookDirMovement, SelectLeftPressed, SelectRightPressed,
}, },
player::{LocalPlayer, PlayerBodyMesh}, player::{LocalPlayer, PlayerBodyMesh},
}; };
@@ -85,14 +85,9 @@ fn reset_control_state_on_disable(
fn get_lookdir( fn get_lookdir(
mut inputs: Single<&mut LocalInputs>, mut inputs: Single<&mut LocalInputs>,
player: Single<&Children, With<LocalPlayer>>, rig_transform: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
rig_transform: Query<&GlobalTransform, With<PlayerBodyMesh>>,
) { ) {
let rig_transform = player inputs.0.look_dir = if let Some(ref rig_transform) = rig_transform {
.iter()
.find_map(|child| rig_transform.get(child).ok());
inputs.0.look_dir = if let Some(rig_transform) = rig_transform {
rig_transform.forward().as_vec3() rig_transform.forward().as_vec3()
} else { } else {
Vec3::NEG_Z Vec3::NEG_Z
@@ -146,11 +141,8 @@ fn gamepad_controls(
inputs.0.move_dir += move_dir.clamp_length_max(1.0); inputs.0.move_dir += move_dir.clamp_length_max(1.0);
inputs.0.jump |= gamepad.pressed(GamepadButton::South); inputs.0.jump |= gamepad.pressed(GamepadButton::South);
inputs.0.view_mode |= gamepad.pressed(GamepadButton::LeftTrigger2);
inputs.0.trigger |= gamepad.pressed(GamepadButton::RightTrigger2); inputs.0.trigger |= gamepad.pressed(GamepadButton::RightTrigger2);
inputs
.0
.view_mode
.merge_input(gamepad.pressed(GamepadButton::LeftTrigger2));
if gamepad.just_pressed(GamepadButton::DPadUp) { if gamepad.just_pressed(GamepadButton::DPadUp) {
backpack_inputs.write(BackpackButtonPress::Toggle); backpack_inputs.write(BackpackButtonPress::Toggle);
@@ -215,8 +207,8 @@ fn keyboard_controls(
inputs.0.move_dir = direction; inputs.0.move_dir = direction;
inputs.0.jump = keyboard.pressed(KeyCode::Space); inputs.0.jump = keyboard.pressed(KeyCode::Space);
inputs.0.view_mode = keyboard.pressed(KeyCode::Tab);
inputs.0.trigger = mouse.pressed(MouseButton::Left); inputs.0.trigger = mouse.pressed(MouseButton::Left);
inputs.0.view_mode = ViewMode::from_input(keyboard.pressed(KeyCode::Tab));
if keyboard.just_pressed(KeyCode::KeyB) { if keyboard.just_pressed(KeyCode::KeyB) {
backpack_inputs.write(BackpackButtonPress::Toggle); backpack_inputs.write(BackpackButtonPress::Toggle);

View File

@@ -1,8 +1,4 @@
use crate::{ use crate::{GameState, control::ControllerSet};
GameState,
control::{ControllerSet, Inputs},
player::{LocalPlayer, Player, PlayerBodyMesh},
};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_replicon::client::ClientSystems; use bevy_replicon::client::ClientSystems;
@@ -26,22 +22,4 @@ pub fn plugin(app: &mut App) {
.before(ClientSystems::Receive) .before(ClientSystems::Receive)
.run_if(in_state(GameState::Playing)), .run_if(in_state(GameState::Playing)),
); );
app.add_systems(
FixedUpdate,
rotate_others.run_if(in_state(GameState::Playing)),
);
}
fn rotate_others(
players: Query<(&Inputs, &Children), (With<Player>, Without<LocalPlayer>)>,
mut rig: Query<(&mut Transform, &PlayerBodyMesh)>,
) {
for (input, children) in players.iter() {
for child in children.iter() {
if let Ok((mut rig, _)) = rig.get_mut(child) {
*rig = rig.looking_to(input.look_dir, Vec3::Y);
}
}
}
} }

View File

@@ -1,104 +0,0 @@
use crate::{
GameState,
client::camera::{CameraState, MainCamera},
cutscene::StartCutscene,
global_observer,
tb_entities::{CameraTarget, CutsceneCamera, CutsceneCameraMovementEnd},
};
use bevy::prelude::*;
use bevy_trenchbroom::prelude::*;
#[derive(Resource, Debug, Default)]
enum CutsceneState {
#[default]
None,
Playing {
timer: Timer,
camera_start: Transform,
camera_end: Transform,
},
}
pub fn plugin(app: &mut App) {
app.init_resource::<CutsceneState>();
app.add_systems(Update, update.run_if(in_state(GameState::Playing)));
global_observer!(app, on_start_cutscene);
}
fn on_start_cutscene(
trigger: On<StartCutscene>,
mut cam_state: ResMut<CameraState>,
mut cutscene_state: ResMut<CutsceneState>,
cutscenes: Query<(&Transform, &CutsceneCamera, &Target), Without<MainCamera>>,
cutscene_movement: Query<
(&Transform, &CutsceneCameraMovementEnd, &Target),
Without<MainCamera>,
>,
cam_target: Query<(&Transform, &CameraTarget), Without<MainCamera>>,
) {
let cutscene = trigger.event().0.clone();
cam_state.cutscene = true;
// asumes `name` and `targetname` are equal
let Some((t, _, target)) = cutscenes
.iter()
.find(|(_, cutscene_camera, _)| cutscene == cutscene_camera.name)
else {
return;
};
let move_end = cutscene_movement
.iter()
.find(|(_, _, target)| cutscene == target.target.clone().unwrap_or_default())
.map(|(t, _, _)| *t)
.unwrap_or_else(|| *t);
let Some((target, _)) = cam_target.iter().find(|(_, camera_target)| {
camera_target.targetname == target.target.clone().unwrap_or_default()
}) else {
return;
};
*cutscene_state = CutsceneState::Playing {
timer: Timer::from_seconds(2.0, TimerMode::Once),
camera_start: t.looking_at(target.translation, Vec3::Y),
camera_end: move_end.looking_at(target.translation, Vec3::Y),
};
}
fn update(
mut cam_state: ResMut<CameraState>,
mut cutscene_state: ResMut<CutsceneState>,
mut cam: Query<&mut Transform, With<MainCamera>>,
time: Res<Time>,
) {
if let CutsceneState::Playing {
timer,
camera_start,
camera_end,
} = &mut *cutscene_state
{
cam_state.cutscene = true;
timer.tick(time.delta());
let t = Transform::from_translation(
camera_start
.translation
.lerp(camera_end.translation, timer.fraction()),
)
.with_rotation(
camera_start
.rotation
.lerp(camera_end.rotation, timer.fraction()),
);
let _ = cam.single_mut().map(|mut cam| *cam = t);
if timer.is_finished() {
cam_state.cutscene = false;
*cutscene_state = CutsceneState::None;
}
}
}

View File

@@ -2,7 +2,7 @@ use crate::{
GameState, GameState,
abilities::Healing, abilities::Healing,
loading_assets::{AudioAssets, GameAssets}, loading_assets::{AudioAssets, GameAssets},
utils::{Billboard, observers::global_observer}, utils::{billboards::Billboard, observers::global_observer},
}; };
use bevy::prelude::*; use bevy::prelude::*;
use rand::{Rng, thread_rng}; use rand::{Rng, thread_rng};

View File

@@ -22,12 +22,9 @@ use bevy_replicon_renet::{
use bevy_steamworks::Client; use bevy_steamworks::Client;
use bevy_trenchbroom::geometry::Brushes; use bevy_trenchbroom::geometry::Brushes;
pub mod aim;
pub mod audio; pub mod audio;
mod backpack; pub mod backpack;
pub mod camera;
pub mod control; pub mod control;
pub mod cutscene;
pub mod debug; pub mod debug;
pub mod enemy; pub mod enemy;
pub mod heal_effect; pub mod heal_effect;
@@ -36,25 +33,20 @@ mod settings;
pub mod setup; pub mod setup;
pub mod steam; pub mod steam;
pub mod ui; pub mod ui;
mod utils;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_plugins(( app.add_plugins((
aim::plugin, backpack::plugin,
audio::plugin,
control::plugin, control::plugin,
debug::plugin, debug::plugin,
enemy::plugin, enemy::plugin,
heal_effect::plugin, heal_effect::plugin,
player::plugin, player::plugin,
setup::plugin, setup::plugin,
audio::plugin,
steam::plugin, steam::plugin,
ui::plugin, ui::plugin,
settings::plugin, settings::plugin,
backpack::plugin,
camera::plugin,
utils::billboards::plugin,
cutscene::plugin,
)); ));
app.add_systems( app.add_systems(
@@ -133,7 +125,7 @@ fn connect_to_server(
let authentication = bevy_replicon_renet::netcode::ClientAuthentication::Unsecure { let authentication = bevy_replicon_renet::netcode::ClientAuthentication::Unsecure {
client_id, client_id,
protocol_id: 0, protocol_id: 0,
server_addr: *host_addr, server_addr: host_addr.clone(),
user_data: None, user_data: None,
}; };
let transport = bevy_replicon_renet::netcode::NetcodeClientTransport::new( let transport = bevy_replicon_renet::netcode::NetcodeClientTransport::new(

View File

@@ -2,7 +2,7 @@ use crate::{
global_observer, global_observer,
heads_database::{HeadControls, HeadsDatabase}, heads_database::{HeadControls, HeadsDatabase},
loading_assets::AudioAssets, loading_assets::AudioAssets,
player::{LocalPlayer, Player, PlayerBodyMesh}, player::{LocalPlayer, PlayerBodyMesh},
protocol::{ClientHeadChanged, PlaySound, PlayerId, messages::AssignClientPlayer}, protocol::{ClientHeadChanged, PlaySound, PlayerId, messages::AssignClientPlayer},
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -59,38 +59,23 @@ pub enum PlayerAssignmentState {
Confirmed, Confirmed,
} }
// TODO: currently a networked message.
// can be done by just using local change detection on `ActiveHead`?
fn on_client_update_head_mesh( fn on_client_update_head_mesh(
trigger: On<ClientHeadChanged>, trigger: On<ClientHeadChanged>,
mut commands: Commands, mut commands: Commands,
player: Query<(&Children, &PlayerId), With<Player>>, body_mesh: Single<(Entity, &Children), With<PlayerBodyMesh>>,
body_mesh: Query<(Entity, &Children), With<PlayerBodyMesh>>,
head_db: Res<HeadsDatabase>, head_db: Res<HeadsDatabase>,
audio_assets: Res<AudioAssets>, audio_assets: Res<AudioAssets>,
sfx: Query<&AudioPlayer>, sfx: Query<&AudioPlayer>,
) -> Result { ) -> Result {
let (player_children, _) = player let head = trigger.0 as usize;
.iter() let (body_mesh, mesh_children) = *body_mesh;
.find(|(_, player_id)| **player_id == trigger.player)
.unwrap();
let (body_mesh, body_mesh_children) = player_children
.iter()
.find_map(|child| body_mesh.get(child).ok())
.unwrap();
let head = trigger.head;
let head_str = head_db.head_key(head); let head_str = head_db.head_key(head);
commands.trigger(PlaySound::Head(head_str.to_string())); commands.trigger(PlaySound::Head(head_str.to_string()));
//TODO: make part of full character mesh later //TODO: make part of full character mesh later
for child in body_mesh_children for child in mesh_children.iter().filter(|child| sfx.contains(*child)) {
.iter()
.filter(|child| sfx.contains(*child))
{
commands.entity(child).despawn(); commands.entity(child).despawn();
} }
if head_db.head_stats(head).controls == HeadControls::Plane { if head_db.head_stats(head).controls == HeadControls::Plane {

View File

@@ -1,30 +1,19 @@
use crate::{client::audio::SoundSettings, utils::Debounce};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_persistent::{Persistent, StorageFormat}; use bevy_pkv::prelude::*;
use crate::{client::audio::SoundSettings, utils::Debounce};
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.insert_resource( #[cfg(not(feature = "dbg"))]
Persistent::<SoundSettings>::builder() app.insert_resource(PkvStore::new("Rustunit", "HEDZ"));
.name("audio")
.format(StorageFormat::Ron)
.path(
dirs::config_dir()
.unwrap()
.join("com.rustunit.hedzreloaded")
.join("audio.ron"),
)
.default(SoundSettings::default())
.build()
.unwrap(),
);
app.add_systems(Update, persist_settings); app.add_systems(Update, persist_settings.run_if(resource_exists::<PkvStore>));
app.add_systems(Startup, load_settings); app.add_systems(Startup, load_settings.run_if(resource_exists::<PkvStore>));
} }
fn persist_settings( fn persist_settings(
settings: Res<SoundSettings>, settings: Res<SoundSettings>,
mut persistent: ResMut<Persistent<SoundSettings>>, mut pkv: ResMut<PkvStore>,
mut debounce: Debounce<1000>, mut debounce: Debounce<1000>,
) -> Result { ) -> Result {
if settings.is_changed() { if settings.is_changed() {
@@ -32,12 +21,16 @@ fn persist_settings(
} }
if debounce.finished() { if debounce.finished() {
persistent.set(*settings)?; pkv.set("audio", &*settings)?;
} }
Ok(()) Ok(())
} }
fn load_settings(persistent: Res<Persistent<SoundSettings>>, mut settings: ResMut<SoundSettings>) { fn load_settings(mut settings: ResMut<SoundSettings>, pkv: Res<PkvStore>) -> Result {
*settings = *persistent.get(); if let Ok(loaded) = pkv.get::<SoundSettings>("audio") {
*settings = loaded;
}
Ok(())
} }

View File

@@ -1,4 +1,4 @@
use crate::{DebugVisuals, client::camera::MainCamera}; use crate::{DebugVisuals, camera::MainCamera};
use bevy::{core_pipeline::tonemapping::Tonemapping, prelude::*, render::view::ColorGrading}; use bevy::{core_pipeline::tonemapping::Tonemapping, prelude::*, render::view::ColorGrading};
use bevy_trenchbroom::TrenchBroomServer; use bevy_trenchbroom::TrenchBroomServer;

View File

@@ -1,47 +0,0 @@
use crate::{
GameState, HEDZ_GREEN, cash::CashInventory, loading_assets::UIAssets, player::LocalPlayer,
};
use bevy::prelude::*;
#[derive(Component, Reflect, Default)]
#[reflect(Component)]
struct CashText;
pub fn plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(Update, update_ui.run_if(in_state(GameState::Playing)));
}
fn update_ui(
cash: Single<&CashInventory, (Changed<CashInventory>, With<LocalPlayer>)>,
text: Query<Entity, With<CashText>>,
mut writer: TextUiWriter,
) {
let Some(text) = text.iter().next() else {
return;
};
*writer.text(text, 0) = cash.cash.to_string();
}
fn setup(mut commands: Commands, assets: Res<UIAssets>) {
commands.spawn((
Name::new("cash-ui"),
Text::new("0"),
TextShadow::default(),
CashText,
TextFont {
font: assets.font.clone(),
font_size: 34.0,
..default()
},
TextColor(HEDZ_GREEN.into()),
TextLayout::new_with_justify(Justify::Center),
Node {
position_type: PositionType::Absolute,
bottom: Val::Px(40.0),
left: Val::Px(100.0),
..default()
},
));
}

View File

@@ -1,15 +1,7 @@
mod backpack_ui;
mod cash_ui;
mod heads_ui;
mod pause; mod pause;
pub use backpack_ui::{BACKPACK_HEAD_SLOTS, BackpackUiState};
use bevy::prelude::*; use bevy::prelude::*;
pub use heads_ui::{HeadsImages, UiHeadState};
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_plugins(heads_ui::plugin);
app.add_plugins(backpack_ui::plugin);
app.add_plugins(pause::plugin); app.add_plugins(pause::plugin);
app.add_plugins(cash_ui::plugin);
} }

View File

@@ -1 +0,0 @@
pub mod billboards;

View File

@@ -1,6 +1,7 @@
use std::net::SocketAddr;
use bevy::prelude::*; use bevy::prelude::*;
use clap::Parser; use clap::Parser;
use std::net::SocketAddr;
use steamworks::SteamId; use steamworks::SteamId;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
@@ -80,7 +81,8 @@ impl From<NetworkingConfig> for NetConfig {
} }
fn parse_addr(addr: Option<String>) -> SocketAddr { fn parse_addr(addr: Option<String>) -> SocketAddr {
addr.and_then(|addr| addr.parse().ok()) addr.map(|addr| addr.parse().ok())
.flatten()
.unwrap_or_else(|| "127.0.0.1:31111".parse().unwrap()) .unwrap_or_else(|| "127.0.0.1:31111".parse().unwrap())
} }

View File

@@ -72,22 +72,12 @@ fn set_animation_flags(
pub fn reset_upon_switch( pub fn reset_upon_switch(
mut c: Commands, mut c: Commands,
mut event_controller_switch: MessageReader<ControllerSwitchEvent>, mut event_controller_switch: MessageReader<ControllerSwitchEvent>,
selected_controller: Res<SelectedController>,
mut rig_transforms: Query<&mut Transform, With<PlayerBodyMesh>>, mut rig_transforms: Query<&mut Transform, With<PlayerBodyMesh>>,
mut controllers: Query< mut controllers: Query<(&mut KinematicVelocity, &Children, &Inputs), With<Player>>,
(
&mut KinematicVelocity,
&Children,
&Inputs,
&SelectedController,
),
With<Player>,
>,
) { ) {
for &ControllerSwitchEvent { controller } in event_controller_switch.read() { for &ControllerSwitchEvent { controller } in event_controller_switch.read() {
let (mut velocity, children, inputs, selected_controller) = let (mut velocity, children, inputs) = controllers.get_mut(controller).unwrap();
controllers.get_mut(controller).unwrap();
info!("resetting controller");
velocity.0 = Vec3::ZERO; velocity.0 = Vec3::ZERO;
@@ -175,7 +165,6 @@ impl Default for MovementSpeedFactor {
MoveInput, MoveInput,
MovementSpeedFactor, MovementSpeedFactor,
TransformInterpolation, TransformInterpolation,
SelectedController::Running,
CharacterMovement = RUNNING_MOVEMENT_CONFIG.movement, CharacterMovement = RUNNING_MOVEMENT_CONFIG.movement,
ControllerSettings = RUNNING_MOVEMENT_CONFIG.settings, ControllerSettings = RUNNING_MOVEMENT_CONFIG.settings,
CharacterGravity = RUNNING_MOVEMENT_CONFIG.gravity, CharacterGravity = RUNNING_MOVEMENT_CONFIG.gravity,

View File

@@ -1,7 +1,7 @@
use super::ControllerSet; use super::ControllerSet;
use crate::{ use crate::{
GameState, GameState,
control::{Inputs, SelectedController, controller_common::MovementSpeedFactor}, control::{Inputs, controller_common::MovementSpeedFactor},
}; };
use bevy::prelude::*; use bevy::prelude::*;
use happy_feet::prelude::MoveInput; use happy_feet::prelude::MoveInput;
@@ -19,17 +19,8 @@ impl Plugin for CharacterControllerPlugin {
} }
} }
pub fn apply_controls( pub fn apply_controls(character: Single<(&mut MoveInput, &MovementSpeedFactor, &Inputs)>) {
mut query: Query<( let (mut char_input, factor, inputs) = character.into_inner();
&mut MoveInput,
&MovementSpeedFactor, char_input.set(inputs.look_dir * factor.0);
&Inputs,
&SelectedController,
)>,
) {
for (mut move_input, factor, inputs, selected_controller) in query.iter_mut() {
if *selected_controller == SelectedController::Flying {
move_input.set(inputs.look_dir * factor.0);
}
}
} }

View File

@@ -1,10 +1,7 @@
use crate::{ use crate::{
GameState, GameState,
animation::AnimationFlags, animation::AnimationFlags,
control::{ control::{ControllerSet, ControllerSettings, Inputs, controller_common::MovementSpeedFactor},
ControllerSet, ControllerSettings, Inputs, SelectedController,
controller_common::MovementSpeedFactor,
},
protocol::is_server, protocol::is_server,
}; };
#[cfg(feature = "client")] #[cfg(feature = "client")]
@@ -44,7 +41,7 @@ fn rotate_view(
) { ) {
let (inputs, children) = controller.into_inner(); let (inputs, children) = controller.into_inner();
if inputs.view_mode.is_free() { if inputs.view_mode {
return; return;
} }
@@ -59,7 +56,7 @@ fn rotate_view(
} }
fn apply_controls( fn apply_controls(
mut query: Query<( character: Single<(
&mut MoveInput, &mut MoveInput,
&mut Grounding, &mut Grounding,
&mut KinematicVelocity, &mut KinematicVelocity,
@@ -67,23 +64,10 @@ fn apply_controls(
&ControllerSettings, &ControllerSettings,
&MovementSpeedFactor, &MovementSpeedFactor,
&Inputs, &Inputs,
&SelectedController,
)>, )>,
) { ) {
for ( let (mut move_input, mut grounding, mut velocity, mut flags, settings, move_factor, inputs) =
mut move_input, character.into_inner();
mut grounding,
mut velocity,
mut flags,
settings,
move_factor,
inputs,
selected_controller,
) in query.iter_mut()
{
if *selected_controller != SelectedController::Running {
continue;
}
let ground_normal = *grounding.normal().unwrap_or(Dir3::Y); let ground_normal = *grounding.normal().unwrap_or(Dir3::Y);
@@ -101,5 +85,4 @@ fn apply_controls(
flags.jump_count += 1; flags.jump_count += 1;
happy_feet::movement::jump(settings.jump_force, &mut velocity, &mut grounding, Dir3::Y) happy_feet::movement::jump(settings.jump_force, &mut velocity, &mut grounding, Dir3::Y)
} }
}
} }

View File

@@ -20,8 +20,7 @@ pub enum ControllerSet {
ApplyControlsRun, ApplyControlsRun,
} }
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Resource, Debug, Clone, Copy, PartialEq, Default)]
#[reflect(Component)]
pub enum SelectedController { pub enum SelectedController {
Flying, Flying,
#[default] #[default]
@@ -36,9 +35,8 @@ pub fn plugin(app: &mut App) {
#[cfg(feature = "client")] #[cfg(feature = "client")]
app.register_type::<LocalInputs>(); app.register_type::<LocalInputs>();
app.register_type::<SelectedController>();
app.init_resource::<LookDirMovement>(); app.init_resource::<LookDirMovement>();
app.init_resource::<SelectedController>();
app.add_message::<ControllerSwitchEvent>() app.add_message::<ControllerSwitchEvent>()
.add_message::<BackpackButtonPress>(); .add_message::<BackpackButtonPress>();
@@ -50,8 +48,8 @@ pub fn plugin(app: &mut App) {
app.configure_sets( app.configure_sets(
FixedUpdate, FixedUpdate,
( (
ControllerSet::ApplyControlsFly, ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController::Flying)),
ControllerSet::ApplyControlsRun, ControllerSet::ApplyControlsRun.run_if(resource_equals(SelectedController::Running)),
) )
.chain() .chain()
.run_if(in_state(GameState::Playing)), .run_if(in_state(GameState::Playing)),
@@ -66,35 +64,6 @@ pub fn plugin(app: &mut App) {
app.add_systems(Update, head_change.run_if(in_state(GameState::Playing))); app.add_systems(Update, head_change.run_if(in_state(GameState::Playing)));
} }
#[derive(Reflect, Default, Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
pub enum ViewMode {
#[default]
Default,
FreeMode,
}
impl ViewMode {
pub fn from_input(button: bool) -> Self {
if button {
Self::FreeMode
} else {
Self::Default
}
}
pub fn merge_input(&mut self, button: bool) {
let new = Self::from_input(button);
*self = match (*self, new) {
(Self::FreeMode, _) | (_, Self::FreeMode) => Self::FreeMode,
_ => Self::Default,
};
}
pub fn is_free(&self) -> bool {
matches!(self, Self::FreeMode)
}
}
/// The continuous inputs of a client for a tick. The instant inputs are sent via messages like `BackpackTogglePressed`. /// The continuous inputs of a client for a tick. The instant inputs are sent via messages like `BackpackTogglePressed`.
#[derive(Component, Clone, Copy, Debug, Serialize, Deserialize, Reflect)] #[derive(Component, Clone, Copy, Debug, Serialize, Deserialize, Reflect)]
#[reflect(Component, Default)] #[reflect(Component, Default)]
@@ -106,7 +75,7 @@ pub struct Inputs {
pub look_dir: Vec3, pub look_dir: Vec3,
pub jump: bool, pub jump: bool,
/// Determines if the camera can rotate freely around the player /// Determines if the camera can rotate freely around the player
pub view_mode: ViewMode, pub view_mode: bool,
pub trigger: bool, pub trigger: bool,
} }
@@ -185,24 +154,23 @@ fn collect_player_inputs(
} }
fn head_change( fn head_change(
mut commands: Commands, //TODO: needs a 'LocalPlayer' at some point for multiplayer
query: Query<(Entity, &ActiveHead, &SelectedController), (Changed<ActiveHead>, With<Player>)>, query: Query<(Entity, &ActiveHead), (Changed<ActiveHead>, With<Player>)>,
heads_db: Res<HeadsDatabase>, heads_db: Res<HeadsDatabase>,
mut selected_controller: ResMut<SelectedController>,
mut event_controller_switch: MessageWriter<ControllerSwitchEvent>, mut event_controller_switch: MessageWriter<ControllerSwitchEvent>,
) { ) {
for (entity, head, selected_controller) in query.iter() { for (entity, head) in query.iter() {
let stats = heads_db.head_stats(head.0); let stats = heads_db.head_stats(head.0);
let controller = match stats.controls { let controller = match stats.controls {
HeadControls::Plane => SelectedController::Flying, HeadControls::Plane => SelectedController::Flying,
HeadControls::Walk => SelectedController::Running, HeadControls::Walk => SelectedController::Running,
}; };
info!("player head changed: {} ({:?})", head.0, controller);
if *selected_controller != controller { if *selected_controller != controller {
event_controller_switch.write(ControllerSwitchEvent { controller: entity }); event_controller_switch.write(ControllerSwitchEvent { controller: entity });
commands.entity(entity).insert(controller); *selected_controller = controller;
} }
} }
} }

View File

@@ -1,5 +1,107 @@
use crate::{
GameState,
camera::{CameraState, MainCamera},
global_observer,
tb_entities::{CameraTarget, CutsceneCamera, CutsceneCameraMovementEnd},
};
use bevy::prelude::*; use bevy::prelude::*;
use bevy_trenchbroom::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Event, Serialize, Deserialize)] #[derive(Clone, Debug, Event, Serialize, Deserialize)]
pub struct StartCutscene(pub String); pub struct StartCutscene(pub String);
#[derive(Resource, Debug, Default)]
enum CutsceneState {
#[default]
None,
Playing {
timer: Timer,
camera_start: Transform,
camera_end: Transform,
},
}
pub fn plugin(app: &mut App) {
app.init_resource::<CutsceneState>();
app.add_systems(Update, update.run_if(in_state(GameState::Playing)));
global_observer!(app, on_start_cutscene);
}
fn on_start_cutscene(
trigger: On<StartCutscene>,
mut cam_state: ResMut<CameraState>,
mut cutscene_state: ResMut<CutsceneState>,
cutscenes: Query<(&Transform, &CutsceneCamera, &Target), Without<MainCamera>>,
cutscene_movement: Query<
(&Transform, &CutsceneCameraMovementEnd, &Target),
Without<MainCamera>,
>,
cam_target: Query<(&Transform, &CameraTarget), Without<MainCamera>>,
) {
let cutscene = trigger.event().0.clone();
cam_state.cutscene = true;
// asumes `name` and `targetname` are equal
let Some((t, _, target)) = cutscenes
.iter()
.find(|(_, cutscene_camera, _)| cutscene == cutscene_camera.name)
else {
return;
};
let move_end = cutscene_movement
.iter()
.find(|(_, _, target)| cutscene == target.target.clone().unwrap_or_default())
.map(|(t, _, _)| *t)
.unwrap_or_else(|| *t);
let Some((target, _)) = cam_target.iter().find(|(_, camera_target)| {
camera_target.targetname == target.target.clone().unwrap_or_default()
}) else {
return;
};
*cutscene_state = CutsceneState::Playing {
timer: Timer::from_seconds(2.0, TimerMode::Once),
camera_start: t.looking_at(target.translation, Vec3::Y),
camera_end: move_end.looking_at(target.translation, Vec3::Y),
};
}
fn update(
mut cam_state: ResMut<CameraState>,
mut cutscene_state: ResMut<CutsceneState>,
mut cam: Query<&mut Transform, With<MainCamera>>,
time: Res<Time>,
) {
if let CutsceneState::Playing {
timer,
camera_start,
camera_end,
} = &mut *cutscene_state
{
cam_state.cutscene = true;
timer.tick(time.delta());
let t = Transform::from_translation(
camera_start
.translation
.lerp(camera_end.translation, timer.fraction()),
)
.with_rotation(
camera_start
.rotation
.lerp(camera_end.rotation, timer.fraction()),
);
let _ = cam.single_mut().map(|mut cam| *cam = t);
if timer.is_finished() {
cam_state.cutscene = false;
*cutscene_state = CutsceneState::None;
}
}
}

View File

@@ -6,7 +6,9 @@ use crate::{
protocol::{GltfSceneRoot, NetworkEnv, PlaySound}, protocol::{GltfSceneRoot, NetworkEnv, PlaySound},
server_observer, server_observer,
tb_entities::SecretHead, tb_entities::SecretHead,
utils::{Billboard, one_shot_force::OneShotImpulse, squish_animation::SquishAnimation}, utils::{
billboards::Billboard, one_shot_force::OneShotImpulse, squish_animation::SquishAnimation,
},
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::{ecs::relationship::RelatedSpawner, prelude::*}; use bevy::{ecs::relationship::RelatedSpawner, prelude::*};

View File

@@ -1,19 +1,13 @@
use super::{ActiveHeads, HEAD_SLOTS};
#[cfg(feature = "client")]
use crate::heads::HeadsImages;
use crate::{ use crate::{
GameState, GameState, backpack::UiHeadState, loading_assets::UIAssets, player::Player, protocol::is_server,
heads::{ActiveHeads, HEAD_COUNT, HEAD_SLOTS, HeadState},
heads_database::HeadsDatabase,
loading_assets::UIAssets,
player::LocalPlayer,
}; };
use bevy::{ecs::spawn::SpawnIter, prelude::*}; use bevy::{ecs::spawn::SpawnIter, prelude::*};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::f32::consts::PI; use std::f32::consts::PI;
#[derive(Resource, Default)]
pub struct HeadsImages {
pub heads: Vec<Handle<Image>>,
}
#[derive(Component, Reflect, Default)] #[derive(Component, Reflect, Default)]
#[reflect(Component)] #[reflect(Component)]
struct HeadSelector(pub usize); struct HeadSelector(pub usize);
@@ -26,78 +20,29 @@ struct HeadImage(pub usize);
#[reflect(Component)] #[reflect(Component)]
struct HeadDamage(pub usize); struct HeadDamage(pub usize);
#[derive(Resource, Default, Reflect, Serialize, Deserialize, PartialEq)] #[derive(Component, Default, Reflect, Serialize, Deserialize, PartialEq)]
#[reflect(Resource)] #[reflect(Component)]
struct UiActiveHeads { pub struct UiActiveHeads {
heads: [Option<UiHeadState>; 5], heads: [Option<UiHeadState>; 5],
selected_slot: usize, selected_slot: usize,
} }
#[derive(Clone, Copy, Debug, PartialEq, Reflect, Default, Serialize, Deserialize)]
pub struct UiHeadState {
pub head: usize,
pub health: f32,
pub ammo: f32,
pub reloading: Option<f32>,
}
impl UiHeadState {
pub fn damage(&self) -> f32 {
1. - self.health
}
pub fn ammo_used(&self) -> f32 {
1. - self.ammo
}
pub fn reloading(&self) -> Option<f32> {
self.reloading
}
pub fn new(value: HeadState, time: f32) -> Self {
let reloading = if value.has_ammo() {
None
} else {
Some((time - value.last_use) / value.reload_duration)
};
Self {
head: value.head,
ammo: value.ammo as f32 / value.ammo_max as f32,
health: value.health as f32 / value.health_max as f32,
reloading,
}
}
}
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.register_type::<HeadDamage>(); app.register_type::<HeadDamage>();
app.register_type::<UiActiveHeads>(); app.register_type::<UiActiveHeads>();
app.init_resource::<UiActiveHeads>(); app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(
app.add_systems(OnEnter(GameState::Playing), (setup, setup_heads_images)); FixedUpdate,
app.add_systems(FixedUpdate, sync.run_if(in_state(GameState::Playing))); sync.run_if(in_state(GameState::Playing).and(is_server)),
);
#[cfg(feature = "client")]
app.add_systems( app.add_systems(
FixedUpdate, FixedUpdate,
(update, update_ammo, update_health).run_if(in_state(GameState::Playing)), (update, update_ammo, update_health).run_if(in_state(GameState::Playing)),
); );
} }
fn setup_heads_images(
mut commands: Commands,
asset_server: Res<AssetServer>,
heads: Res<HeadsDatabase>,
) {
// TODO: load via asset loader
let heads = (0usize..HEAD_COUNT)
.map(|i| asset_server.load(format!("ui/heads/{}.png", heads.head_key(i))))
.collect();
commands.insert_resource(HeadsImages { heads });
}
fn setup(mut commands: Commands, assets: Res<UIAssets>) { fn setup(mut commands: Commands, assets: Res<UIAssets>) {
commands.spawn(( commands.spawn((
Name::new("heads-ui"), Name::new("heads-ui"),
@@ -231,7 +176,7 @@ fn spawn_head_ui(
#[cfg(feature = "client")] #[cfg(feature = "client")]
fn update( fn update(
res: Res<UiActiveHeads>, res: Single<&UiActiveHeads>,
heads_images: Res<HeadsImages>, heads_images: Res<HeadsImages>,
mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without<HeadSelector>>, mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without<HeadSelector>>,
mut head_selector: Query<(&HeadSelector, &mut Visibility), Without<HeadImage>>, mut head_selector: Query<(&HeadSelector, &mut Visibility), Without<HeadImage>>,
@@ -255,14 +200,10 @@ fn update(
#[cfg(feature = "client")] #[cfg(feature = "client")]
fn update_ammo( fn update_ammo(
res: Res<UiActiveHeads>, res: Single<&UiActiveHeads, Changed<UiActiveHeads>>,
heads: Query<&HeadImage>, heads: Query<&HeadImage>,
mut gradients: Query<(&mut BackgroundGradient, &ChildOf)>, mut gradients: Query<(&mut BackgroundGradient, &ChildOf)>,
) { ) {
if !res.is_changed() {
return;
}
for (mut gradient, child_of) in gradients.iter_mut() { for (mut gradient, child_of) in gradients.iter_mut() {
let Ok(HeadImage(head)) = heads.get(child_of.parent()) else { let Ok(HeadImage(head)) = heads.get(child_of.parent()) else {
continue; continue;
@@ -288,22 +229,26 @@ fn update_ammo(
} }
#[cfg(feature = "client")] #[cfg(feature = "client")]
fn update_health(res: Res<UiActiveHeads>, mut query: Query<(&mut Node, &HeadDamage)>) { fn update_health(
if res.is_changed() { res: Single<&UiActiveHeads, Changed<UiActiveHeads>>,
mut query: Query<(&mut Node, &HeadDamage)>,
) {
for (mut node, HeadDamage(head)) in query.iter_mut() { for (mut node, HeadDamage(head)) in query.iter_mut() {
node.height = node.height = Val::Percent(res.heads[*head].map(|head| head.damage()).unwrap_or(0.) * 100.);
Val::Percent(res.heads[*head].map(|head| head.damage()).unwrap_or(0.) * 100.);
}
} }
} }
fn sync( fn sync(
active_heads: Single<Ref<ActiveHeads>, With<LocalPlayer>>, active_heads: Query<Ref<ActiveHeads>, With<Player>>,
mut state: ResMut<UiActiveHeads>, mut state: Single<&mut UiActiveHeads>,
time: Res<Time>, time: Res<Time>,
) { ) {
let Ok(active_heads) = active_heads.single() else {
return;
};
if active_heads.is_changed() || active_heads.reloading() { if active_heads.is_changed() || active_heads.reloading() {
state.selected_slot = active_heads.slot(); state.selected_slot = active_heads.selected_slot;
for i in 0..HEAD_SLOTS { for i in 0..HEAD_SLOTS {
state.heads[i] = active_heads state.heads[i] = active_heads

View File

@@ -13,9 +13,16 @@ use bevy::prelude::*;
use bevy_replicon::prelude::FromClient; use bevy_replicon::prelude::FromClient;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub mod heads_ui;
pub static HEAD_COUNT: usize = 18; pub static HEAD_COUNT: usize = 18;
pub static HEAD_SLOTS: usize = 5; pub static HEAD_SLOTS: usize = 5;
#[derive(Resource, Default)]
pub struct HeadsImages {
pub heads: Vec<Handle<Image>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Reflect, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Reflect, Serialize, Deserialize)]
pub struct HeadState { pub struct HeadState {
pub head: usize, pub head: usize,
@@ -67,10 +74,6 @@ impl ActiveHeads {
self.heads[self.current_slot] self.heads[self.current_slot]
} }
pub fn slot(&self) -> usize {
self.current_slot
}
pub fn use_ammo(&mut self, time: f32) { pub fn use_ammo(&mut self, time: f32) {
let Some(head) = &mut self.heads[self.current_slot] else { let Some(head) = &mut self.heads[self.current_slot] else {
error!("cannot use ammo of empty head"); error!("cannot use ammo of empty head");
@@ -174,15 +177,15 @@ impl ActiveHeads {
} }
} }
#[derive(EntityEvent)] #[derive(Event)]
pub struct HeadChanged { pub struct HeadChanged(pub usize);
pub entity: Entity,
pub head: usize,
}
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_plugins(heads_ui::plugin);
app.register_type::<ActiveHeads>(); app.register_type::<ActiveHeads>();
app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems( app.add_systems(
FixedUpdate, FixedUpdate,
( (
@@ -195,6 +198,15 @@ pub fn plugin(app: &mut App) {
global_observer!(app, on_swap_backpack); global_observer!(app, on_swap_backpack);
} }
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, heads: Res<HeadsDatabase>) {
// TODO: load via asset loader
let heads = (0usize..HEAD_COUNT)
.map(|i| asset_server.load(format!("ui/heads/{}.png", heads.head_key(i))))
.collect();
commands.insert_resource(HeadsImages { heads });
}
fn sync_hp(mut query: Query<(&mut ActiveHeads, &Hitpoints)>) { fn sync_hp(mut query: Query<(&mut ActiveHeads, &Hitpoints)>) {
for (mut active_heads, hp) in query.iter_mut() { for (mut active_heads, hp) in query.iter_mut() {
if active_heads.hp().get() != hp.get() { if active_heads.hp().get() != hp.get() {
@@ -205,10 +217,11 @@ fn sync_hp(mut query: Query<(&mut ActiveHeads, &Hitpoints)>) {
fn reload( fn reload(
mut commands: Commands, mut commands: Commands,
mut active: Query<(&mut ActiveHeads, &mut AnimationFlags), With<Player>>, mut active: Query<&mut ActiveHeads>,
time: Res<Time>, time: Res<Time>,
mut flags: Single<&mut AnimationFlags, With<Player>>,
) { ) {
for (mut active, mut flags) in active.iter_mut() { for mut active in active.iter_mut() {
if !active.reloading() { if !active.reloading() {
continue; continue;
} }
@@ -236,7 +249,6 @@ fn reload(
fn on_select_active_head( fn on_select_active_head(
mut commands: Commands, mut commands: Commands,
mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>, mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>,
// TODO: unify into just one message
mut select_lefts: MessageReader<FromClient<SelectLeftPressed>>, mut select_lefts: MessageReader<FromClient<SelectLeftPressed>>,
mut select_rights: MessageReader<FromClient<SelectRightPressed>>, mut select_rights: MessageReader<FromClient<SelectRightPressed>>,
controllers: ClientToController, controllers: ClientToController,
@@ -258,10 +270,9 @@ fn on_select_active_head(
active_heads.current_slot = active_heads.selected_slot; active_heads.current_slot = active_heads.selected_slot;
hp.set_health(active_heads.current().unwrap().health); hp.set_health(active_heads.current().unwrap().health);
commands.trigger(HeadChanged { commands.trigger(HeadChanged(
entity: player, active_heads.heads[active_heads.current_slot].unwrap().head,
head: active_heads.heads[active_heads.current_slot].unwrap().head, ));
});
} }
} }
@@ -282,25 +293,21 @@ fn on_select_active_head(
active_heads.current_slot = active_heads.selected_slot; active_heads.current_slot = active_heads.selected_slot;
hp.set_health(active_heads.current().unwrap().health); hp.set_health(active_heads.current().unwrap().health);
commands.trigger(HeadChanged { commands.trigger(HeadChanged(
entity: player, active_heads.heads[active_heads.current_slot].unwrap().head,
head: active_heads.heads[active_heads.current_slot].unwrap().head, ));
});
} }
} }
} }
fn on_swap_backpack( fn on_swap_backpack(
trigger: On<FromClient<BackpackSwapEvent>>, trigger: On<FromClient<BackpackSwapEvent>>,
clients: ClientToController,
mut commands: Commands, mut commands: Commands,
mut query: Query<(Entity, &mut ActiveHeads, &mut Hitpoints, &mut Backpack), With<Player>>, mut query: Query<(&mut ActiveHeads, &mut Hitpoints, &mut Backpack), With<Player>>,
) { ) {
let player = clients.get_controller(trigger.client_id);
let backpack_slot = trigger.event().0; let backpack_slot = trigger.event().0;
let Ok((player, mut active_heads, mut hp, mut backpack)) = query.get_mut(player) else { let Ok((mut active_heads, mut hp, mut backpack)) = query.single_mut() else {
return; return;
}; };
@@ -319,8 +326,7 @@ fn on_swap_backpack(
hp.set_health(active_heads.current().unwrap().health); hp.set_health(active_heads.current().unwrap().health);
commands.trigger(HeadChanged { commands.trigger(HeadChanged(
entity: player, active_heads.heads[active_heads.selected_slot].unwrap().head,
head: active_heads.heads[active_heads.selected_slot].unwrap().head, ));
});
} }

View File

@@ -1,10 +1,11 @@
use crate::{ use crate::{
billboards::Billboard,
global_observer, global_observer,
physics_layers::GameLayer, physics_layers::GameLayer,
player::Player, player::Player,
protocol::{GltfSceneRoot, PlaySound}, protocol::{GltfSceneRoot, PlaySound},
squish_animation::SquishAnimation, squish_animation::SquishAnimation,
utils::{Billboard, one_shot_force::OneShotImpulse}, utils::one_shot_force::OneShotImpulse,
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::prelude::*; use bevy::prelude::*;

View File

@@ -52,7 +52,7 @@ use bevy_trenchbroom::{
TrenchBroomPlugins, config::TrenchBroomConfig, prelude::TrenchBroomPhysicsPlugin, TrenchBroomPlugins, config::TrenchBroomConfig, prelude::TrenchBroomPhysicsPlugin,
}; };
use bevy_trenchbroom_avian::AvianPhysicsBackend; use bevy_trenchbroom_avian::AvianPhysicsBackend;
use utils::squish_animation; use utils::{billboards, squish_animation};
pub const HEDZ_GREEN: Srgba = Srgba::rgb(0.0, 1.0, 0.0); pub const HEDZ_GREEN: Srgba = Srgba::rgb(0.0, 1.0, 0.0);
pub const HEDZ_PURPLE: Srgba = Srgba::rgb(91. / 256., 4. / 256., 138. / 256.); pub const HEDZ_PURPLE: Srgba = Srgba::rgb(91. / 256., 4. / 256., 138. / 256.);
@@ -119,13 +119,16 @@ pub fn plugin(app: &mut App) {
app.add_plugins(gates::plugin); app.add_plugins(gates::plugin);
app.add_plugins(platforms::plugin); app.add_plugins(platforms::plugin);
app.add_plugins(movables::plugin); app.add_plugins(movables::plugin);
app.add_plugins(utils::billboards::plugin);
app.add_plugins(aim::plugin); app.add_plugins(aim::plugin);
app.add_plugins(npc::plugin); app.add_plugins(npc::plugin);
app.add_plugins(keys::plugin); app.add_plugins(keys::plugin);
app.add_plugins(utils::squish_animation::plugin); app.add_plugins(utils::squish_animation::plugin);
app.add_plugins(camera::plugin);
#[cfg(feature = "client")] #[cfg(feature = "client")]
app.add_plugins(client::plugin); app.add_plugins(client::plugin);
app.add_plugins(control::plugin); app.add_plugins(control::plugin);
app.add_plugins(cutscene::plugin);
app.add_plugins(backpack::plugin); app.add_plugins(backpack::plugin);
app.add_plugins(loading_assets::LoadingPlugin); app.add_plugins(loading_assets::LoadingPlugin);
app.add_plugins(loading_map::plugin); app.add_plugins(loading_map::plugin);

View File

@@ -12,7 +12,7 @@ use crate::{
loading_assets::GameAssets, loading_assets::GameAssets,
protocol::{PlaySound, is_server}, protocol::{PlaySound, is_server},
tb_entities::EnemySpawn, tb_entities::EnemySpawn,
utils::Billboard, utils::billboards::Billboard,
}; };
use bevy::{light::NotShadowCaster, prelude::*}; use bevy::{light::NotShadowCaster, prelude::*};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@@ -1,21 +1,22 @@
use crate::{ use crate::{
GameState, GameState,
abilities::PlayerTriggerState, abilities::PlayerTriggerState,
backpack::Backpack, backpack::{Backpack, backpack_ui::BackpackUiState},
camera::{CameraArmRotation, CameraTarget}, camera::{CameraArmRotation, CameraTarget},
cash::CashInventory, cash::{Cash, CashCollectEvent, CashInventory},
character::{AnimatedCharacter, HedzCharacter}, character::{AnimatedCharacter, HedzCharacter},
control::{Inputs, LocalInputs, controller_common::PlayerCharacterController}, control::{Inputs, LocalInputs, controller_common::PlayerCharacterController},
global_observer, global_observer,
head::ActiveHead, head::ActiveHead,
head_drop::HeadDrops, head_drop::HeadDrops,
heads::{ActiveHeads, HeadChanged, HeadState}, heads::{ActiveHeads, HeadChanged, HeadState, heads_ui::UiActiveHeads},
heads_database::HeadsDatabase, heads_database::HeadsDatabase,
hitpoints::{Hitpoints, Kill}, hitpoints::{Hitpoints, Kill},
npc::SpawnCharacter, npc::SpawnCharacter,
protocol::{ClientHeadChanged, OwnedByClient, PlaySound, PlayerId}, protocol::{ClientHeadChanged, OwnedByClient, PlaySound, PlayerId},
tb_entities::SpawnPoint, tb_entities::SpawnPoint,
}; };
use avian3d::prelude::*;
use bevy::{ use bevy::{
input::common_conditions::input_just_pressed, input::common_conditions::input_just_pressed,
prelude::*, prelude::*,
@@ -23,7 +24,6 @@ use bevy::{
}; };
use bevy_replicon::prelude::{ClientId, Replicated, SendMode, ServerTriggerExt, ToClients}; use bevy_replicon::prelude::{ClientId, Replicated, SendMode, ServerTriggerExt, ToClients};
use happy_feet::debug::DebugInput; use happy_feet::debug::DebugInput;
use rand::Rng;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Component, Default, Serialize, Deserialize, PartialEq)] #[derive(Component, Default, Serialize, Deserialize, PartialEq)]
@@ -32,7 +32,7 @@ pub struct Player;
#[derive(Component, Debug, Reflect)] #[derive(Component, Debug, Reflect)]
#[reflect(Component)] #[reflect(Component)]
#[require(LocalInputs)] #[require(LocalInputs, BackpackUiState)]
pub struct LocalPlayer; pub struct LocalPlayer;
#[derive(Component, Default, Serialize, Deserialize, PartialEq)] #[derive(Component, Default, Serialize, Deserialize, PartialEq)]
@@ -52,6 +52,7 @@ pub fn plugin(app: &mut App) {
app.add_systems( app.add_systems(
Update, Update,
( (
collect_cash,
setup_animations_marker_for_player, setup_animations_marker_for_player,
toggle_cursor_system.run_if(input_just_pressed(KeyCode::Escape)), toggle_cursor_system.run_if(input_just_pressed(KeyCode::Escape)),
) )
@@ -70,15 +71,7 @@ pub fn spawn(
) -> Option<Entity> { ) -> Option<Entity> {
let spawn = query.iter().next()?; let spawn = query.iter().next()?;
// This offset helps prevent players from getting stuck inside each other on spawn and causing a perpetual let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
// motion machine.
let random_offset = Vec3::new(
rand::thread_rng().gen_range(-0.01..0.01),
0.0,
rand::thread_rng().gen_range(-0.01..0.01),
);
let transform =
Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.) + random_offset);
let id = commands let id = commands
.spawn(( .spawn((
@@ -102,6 +95,8 @@ pub fn spawn(
id, id,
), ),
Backpack::default(), Backpack::default(),
BackpackUiState::default(),
UiActiveHeads::default(),
Inputs::default(), Inputs::default(),
Replicated, Replicated,
)) ))
@@ -137,16 +132,9 @@ pub fn spawn(
fn on_kill( fn on_kill(
trigger: On<Kill>, trigger: On<Kill>,
mut commands: Commands, mut commands: Commands,
mut query: Query<( mut query: Query<(&Transform, &ActiveHead, &mut ActiveHeads, &mut Hitpoints)>,
Entity,
&Transform,
&ActiveHead,
&mut ActiveHeads,
&mut Hitpoints,
)>,
) { ) {
let Ok((player, transform, active, mut heads, mut hp)) = query.get_mut(trigger.event().entity) let Ok((transform, active, mut heads, mut hp)) = query.get_mut(trigger.event().entity) else {
else {
return; return;
}; };
@@ -155,51 +143,31 @@ fn on_kill(
if let Some(new_head) = heads.loose_current() { if let Some(new_head) = heads.loose_current() {
hp.set_health(heads.current().unwrap().health); hp.set_health(heads.current().unwrap().health);
commands.trigger(HeadChanged { commands.trigger(HeadChanged(new_head));
entity: player,
head: new_head,
});
} }
} }
fn on_update_head_mesh( fn on_update_head_mesh(
trigger: On<HeadChanged>, trigger: On<HeadChanged>,
mut commands: Commands, mut commands: Commands,
player_id: Query<&PlayerId, With<Player>>, mesh_children: Single<&Children, With<PlayerBodyMesh>>,
children: Query<&Children>,
player_body_mesh: Query<&PlayerBodyMesh>,
animated_characters: Query<&AnimatedCharacter>, animated_characters: Query<&AnimatedCharacter>,
mut active_head: Query<&mut ActiveHead>, mut player: Single<&mut ActiveHead, With<Player>>,
) -> Result { ) -> Result {
let player_id = *(player_id.get(trigger.entity)?); let animated_char = mesh_children
let player_body_mesh = children
.get(trigger.entity)?
.iter() .iter()
.find(|child| player_body_mesh.get(*child).is_ok()) .find(|child| animated_characters.contains(*child))
.unwrap(); .ok_or("tried to update head mesh before AnimatedCharacter was readded")?;
let animated_character = children player.0 = trigger.0;
.get(player_body_mesh)?
.iter()
.find(|child| animated_characters.get(*child).is_ok())
.unwrap();
{
let mut active_head = active_head.get_mut(trigger.entity)?;
active_head.0 = trigger.head;
}
commands commands
.entity(animated_character) .entity(animated_char)
.insert(AnimatedCharacter::new(trigger.head)); .insert(AnimatedCharacter::new(trigger.0));
commands.server_trigger(ToClients { commands.server_trigger(ToClients {
mode: SendMode::Broadcast, mode: SendMode::Broadcast,
message: ClientHeadChanged { message: ClientHeadChanged(trigger.0 as u64),
player: player_id,
head: trigger.head,
},
}); });
Ok(()) Ok(())
@@ -231,6 +199,33 @@ fn toggle_cursor_system(mut window: Single<&mut CursorOptions, With<PrimaryWindo
toggle_grab_cursor(&mut window); toggle_grab_cursor(&mut window);
} }
fn collect_cash(
mut commands: Commands,
mut collision_message_reader: MessageReader<CollisionStart>,
query_player: Query<&Player>,
query_cash: Query<&Cash>,
) {
for CollisionStart {
collider1: e1,
collider2: e2,
..
} in collision_message_reader.read()
{
let collect = if query_player.contains(*e1) && query_cash.contains(*e2) {
Some(*e2)
} else if query_player.contains(*e2) && query_cash.contains(*e1) {
Some(*e1)
} else {
None
};
if let Some(cash) = collect {
commands.trigger(CashCollectEvent);
commands.entity(cash).despawn();
}
}
}
fn setup_animations_marker_for_player( fn setup_animations_marker_for_player(
mut commands: Commands, mut commands: Commands,
animation_handles: Query<Entity, Added<AnimationGraphHandle>>, animation_handles: Query<Entity, Added<AnimationGraphHandle>>,

View File

@@ -188,7 +188,6 @@ pub enum GltfSceneRoot {
Projectile(String), Projectile(String),
HeadDrop(String), HeadDrop(String),
Key, Key,
Cash,
} }
pub fn spawn_gltf_scene_roots( pub fn spawn_gltf_scene_roots(
@@ -220,7 +219,6 @@ pub fn spawn_gltf_scene_roots(
get_scene(gltf, 0) get_scene(gltf, 0)
} }
GltfSceneRoot::Key => assets.mesh_key.clone(), GltfSceneRoot::Key => assets.mesh_key.clone(),
GltfSceneRoot::Cash => assets.mesh_cash.clone(),
}; };
commands commands

View File

@@ -1,13 +1,8 @@
use crate::protocol::PlayerId;
use bevy::prelude::*; use bevy::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// TODO: remove in favour of client side change detection
#[derive(Clone, Event, Serialize, Deserialize, PartialEq)] #[derive(Clone, Event, Serialize, Deserialize, PartialEq)]
pub struct ClientHeadChanged { pub struct ClientHeadChanged(pub u64);
pub player: PlayerId,
pub head: usize,
}
#[derive(Event, Clone, Debug, Serialize, Deserialize)] #[derive(Event, Clone, Debug, Serialize, Deserialize)]
pub enum PlaySound { pub enum PlaySound {

View File

@@ -6,7 +6,7 @@ use crate::{
animation::AnimationFlags, animation::AnimationFlags,
backpack::{Backpack, BackpackSwapEvent}, backpack::{Backpack, BackpackSwapEvent},
camera::{CameraArmRotation, CameraTarget}, camera::{CameraArmRotation, CameraTarget},
cash::{Cash, CashInventory}, cash::CashInventory,
character::{AnimatedCharacter, HedzCharacter}, character::{AnimatedCharacter, HedzCharacter},
control::{ control::{
CashHealPressed, ClientInputs, ControllerSettings, Inputs, SelectLeftPressed, CashHealPressed, ClientInputs, ControllerSettings, Inputs, SelectLeftPressed,
@@ -16,14 +16,15 @@ use crate::{
cutscene::StartCutscene, cutscene::StartCutscene,
global_observer, global_observer,
head::ActiveHead, head::ActiveHead,
heads::ActiveHeads, heads::{ActiveHeads, heads_ui::UiActiveHeads},
hitpoints::Hitpoints, hitpoints::Hitpoints,
npc::Npc, npc::Npc,
platforms::ActivePlatform, platforms::ActivePlatform,
player::{Player, PlayerBodyMesh}, player::{Player, PlayerBodyMesh},
tick::GameTick, tick::GameTick,
utils::{ utils::{
Billboard, auto_rotate::AutoRotation, squish_animation::SquishAnimation, trail::SpawnTrail, auto_rotate::AutoRotation, billboards::Billboard, squish_animation::SquishAnimation,
trail::SpawnTrail,
}, },
}; };
use avian3d::prelude::{ use avian3d::prelude::{
@@ -106,7 +107,7 @@ pub fn plugin(app: &mut App) {
.replicate::<SquishAnimation>() .replicate::<SquishAnimation>()
.replicate_once::<Transform>() .replicate_once::<Transform>()
.replicate_once::<SpawnTrail>() .replicate_once::<SpawnTrail>()
.replicate_once::<Cash>() .replicate::<UiActiveHeads>()
.replicate_as::<Visibility, SerVisibility>(); .replicate_as::<Visibility, SerVisibility>();
app.replicate_once::<ThrownProjectile>() app.replicate_once::<ThrownProjectile>()

View File

@@ -1,5 +1,6 @@
use crate::{ use crate::{
GameState, GameState,
cash::Cash,
loading_assets::GameAssets, loading_assets::GameAssets,
physics_layers::GameLayer, physics_layers::GameLayer,
protocol::{ protocol::{
@@ -151,8 +152,30 @@ impl EnemySpawn {
#[point_class(base(Transform), model({ "path": "models/cash.glb" }))] #[point_class(base(Transform), model({ "path": "models/cash.glb" }))]
#[derive(Default)] #[derive(Default)]
#[component(on_add = Self::on_add)]
pub struct CashSpawn {} pub struct CashSpawn {}
impl CashSpawn {
fn on_add(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
let Some(assets) = world.get_resource::<GameAssets>() else {
return;
};
let mesh = assets.mesh_cash.clone();
world.commands().entity(entity).insert((
Name::new("cash"),
SceneRoot(mesh),
Cash,
Collider::cuboid(2., 3.0, 2.),
CollisionLayers::new(GameLayer::CollectibleSensors, LayerMask::ALL),
RigidBody::Static,
CollisionEventsEnabled,
Sensor,
));
}
}
#[point_class(base(Transform), model({ "path": "models/head_drop.glb" }))] #[point_class(base(Transform), model({ "path": "models/head_drop.glb" }))]
#[derive(Default)] #[derive(Default)]
pub struct SecretHead { pub struct SecretHead {

View File

@@ -1,12 +1,22 @@
use crate::{client::camera::MainCamera, utils::Billboard}; use crate::camera::MainCamera;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_sprite3d::Sprite3dPlugin; use bevy_sprite3d::Sprite3dPlugin;
use serde::{Deserialize, Serialize};
#[derive(Component, Reflect, Default, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component)]
pub enum Billboard {
#[default]
All,
XZ,
}
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
if !app.is_plugin_added::<Sprite3dPlugin>() { if !app.is_plugin_added::<Sprite3dPlugin>() {
app.add_plugins(Sprite3dPlugin); app.add_plugins(Sprite3dPlugin);
} }
app.register_type::<Billboard>();
app.add_systems(Update, (face_camera, face_camera_no_parent)); app.add_systems(Update, (face_camera, face_camera_no_parent));
} }

View File

@@ -1,4 +1,5 @@
pub mod auto_rotate; pub mod auto_rotate;
pub mod billboards;
pub mod cooldown; pub mod cooldown;
pub mod debounce; pub mod debounce;
pub mod explosions; pub mod explosions;
@@ -13,18 +14,7 @@ use bevy::prelude::*;
pub use cooldown::Cooldown; pub use cooldown::Cooldown;
pub use debounce::Debounce; pub use debounce::Debounce;
pub(crate) use observers::global_observer; pub(crate) use observers::global_observer;
use serde::{Deserialize, Serialize};
#[derive(Component, Reflect, Default, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component)]
pub enum Billboard {
#[default]
All,
XZ,
}
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.register_type::<Billboard>();
app.add_plugins(one_shot_force::plugin); app.add_plugins(one_shot_force::plugin);
} }