22 Commits

Author SHA1 Message Date
a7239b3e83 fix ci
Some checks are pending
ci / ci ([self-hosted linux]) (push) Waiting to run
ci / ci ([self-hosted macos]) (push) Waiting to run
2025-12-22 22:18:55 -05:00
16b08e2547 use gitea compatible artifact upload
Some checks failed
ci / ci ([self-hosted linux]) (push) Has been cancelled
ci / ci ([self-hosted macos]) (push) Has been cancelled
2025-12-22 22:07:04 -05:00
6ed54dfdc7 fix animation (feet)
Some checks failed
ci / ci ([self-hosted linux]) (push) Has been cancelled
ci / ci ([self-hosted macos]) (push) Has been cancelled
2025-12-22 08:36:36 -05:00
PROMETHIA-27
dd01b03526 fix depenetration panic (#98) 2025-12-22 07:22:27 -05:00
extrawurst
da5c0f8fb7 fix cash in multiplayer (#97)
* replicate it
* use collision observer to simplify
* pass entity of player that receives cash for a duplicate head
2025-12-21 21:44:43 -05:00
375b8a5b46 fix cash ui 2025-12-21 17:52:24 -05:00
89e6102b06 fix player rotation 2025-12-21 16:01:36 -05:00
e22fa8d134 more clear splitting of visual stuff in client mod 2025-12-21 15:27:11 -05:00
181b617620 make backwork in multiplayer 2025-12-21 13:53:57 -05:00
56ca801992 move heads_ui in client only module 2025-12-21 13:10:50 -05:00
16cd95ae02 cleaunup 2025-12-21 12:55:29 -05:00
fcb13eed31 UiActiveHeads can be a local player resource only 2025-12-21 12:46:32 -05:00
extrawurst
0735c429ca fixes head switching (#96) 2025-12-21 12:43:13 -05:00
PROMETHIA-27
c3c5ae6dfb Target UI multiplayer (#95) 2025-12-21 12:01:50 -05:00
extrawurst
cc7e2aae70 fix lookdir not syncing (#94)
Co-authored-by: PROMETHIA-27 <electriccobras@gmail.com>
2025-12-20 20:25:36 -05:00
PROMETHIA-27
dbcd822b50 Camera update multiplayer (#93) 2025-12-20 19:47:45 -05:00
ac8c834f2f allow multiple players moving 2025-12-20 19:07:51 -05:00
f35275ab9f sitch to bevy_persistent to allow two clients
bevy_pkv was holding a file lock to prevent that
2025-12-20 13:21:30 -05:00
3901ee1174 fix clippy warning 2025-12-20 13:20:59 -05:00
extrawurst
7d280af821 add renet_steam support (#92) 2025-12-20 13:19:13 -05:00
7ea9046414 remove obsolete ignore 2025-12-19 20:04:06 -05:00
PROMETHIA-27
f6fa9ce1e4 implement player id allocation (#90)
* implement player id allocation

* move `bevy/debug` to `dbg`
2025-12-19 17:41:16 -05:00
112 changed files with 1607 additions and 997 deletions

View File

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

View File

@@ -14,8 +14,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: cargo-bins/cargo-binstall@main
if: runner.os == 'linux'
- uses: extractions/setup-just@v1
- uses: dtolnay/rust-toolchain@master
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 ./
tar -czf steamos-debug.tar.gz hedz_reloaded hedz_reloaded_server
- uses: actions/upload-artifact@v4
- uses: christopherhx/gitea-upload-artifact@v4
with:
name: steamos-debug.tar.gz
path: ./steamos-debug.tar.gz

View File

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

1
.gitignore vendored
View File

@@ -5,4 +5,3 @@ build/steamos/hedz_reloaded
build/steamos/.env
build/macos/src/HEDZReloaded.app/Contents/MacOS
build/macos/src/Applications
server.log

386
Cargo.lock generated
View File

@@ -102,6 +102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"getrandom 0.3.4",
"once_cell",
"version_check",
"zerocopy",
@@ -512,6 +513,19 @@ dependencies = [
"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]]
name = "bevy-steamworks"
version = "0.15.0"
@@ -567,7 +581,7 @@ dependencies = [
"bevy_utils",
"blake3",
"derive_more",
"downcast-rs",
"downcast-rs 2.0.2",
"either",
"petgraph",
"ron 0.10.1",
@@ -627,7 +641,7 @@ dependencies = [
"cfg-if",
"console_error_panic_hook",
"ctrlc",
"downcast-rs",
"downcast-rs 2.0.2",
"log",
"thiserror 2.0.17",
"variadics_please",
@@ -658,7 +672,7 @@ dependencies = [
"crossbeam-channel",
"derive_more",
"disqualified",
"downcast-rs",
"downcast-rs 2.0.2",
"either",
"futures-io",
"futures-lite",
@@ -730,6 +744,7 @@ dependencies = [
"bevy_reflect",
"bevy_transform",
"coreaudio-sys",
"cpal",
"rodio",
"tracing",
]
@@ -761,7 +776,7 @@ dependencies = [
"bevy_utils",
"bevy_window",
"derive_more",
"downcast-rs",
"downcast-rs 2.0.2",
"serde",
"smallvec",
"thiserror 2.0.17",
@@ -1164,6 +1179,7 @@ dependencies = [
"bevy_pbr",
"bevy_picking",
"bevy_platform",
"bevy_post_process",
"bevy_ptr",
"bevy_reflect",
"bevy_render",
@@ -1359,25 +1375,6 @@ dependencies = [
"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]]
name = "bevy_platform"
version = "0.17.3"
@@ -1399,6 +1396,36 @@ dependencies = [
"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]]
name = "bevy_ptr"
version = "0.17.3"
@@ -1418,7 +1445,7 @@ dependencies = [
"bevy_utils",
"derive_more",
"disqualified",
"downcast-rs",
"downcast-rs 2.0.2",
"erased-serde",
"foldhash 0.2.0",
"glam 0.30.9",
@@ -1477,7 +1504,7 @@ dependencies = [
"bitflags 2.10.0",
"bytemuck",
"derive_more",
"downcast-rs",
"downcast-rs 2.0.2",
"encase",
"fixedbitset",
"image",
@@ -1519,6 +1546,7 @@ dependencies = [
"bevy_time",
"renet",
"renet_netcode",
"renet_steam",
]
[[package]]
@@ -2126,6 +2154,18 @@ dependencies = [
"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]]
name = "cc"
version = "1.2.49"
@@ -2780,10 +2820,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11277822c27bde750de02c5dc5159b91e88bf2661a2c1d98106f2fb1c5c6f590"
[[package]]
name = "directories"
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
@@ -2859,6 +2899,12 @@ dependencies = [
"litrs",
]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "downcast-rs"
version = "2.0.2"
@@ -3472,6 +3518,34 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "glow"
version = "0.16.0"
@@ -3611,7 +3685,7 @@ dependencies = [
[[package]]
name = "happy_feet"
version = "0.1.0"
source = "git+https://github.com/rustunit/happy_feet.git?rev=919657fa#919657fa3330b3a78026c239c17122b3e3beefd7"
source = "git+https://github.com/PROMETHIA-27/happy_feet.git?rev=e3a4660e0b68f9bf4d6facb0fb4d93f2d7848b27#e3a4660e0b68f9bf4d6facb0fb4d93f2d7848b27"
dependencies = [
"avian3d",
"bevy",
@@ -3680,18 +3754,19 @@ dependencies = [
"avian3d",
"bevy",
"bevy-inspector-egui",
"bevy-persistent",
"bevy-steamworks",
"bevy_asset_loader",
"bevy_ballistic",
"bevy_common_assets",
"bevy_debug_log",
"bevy_pkv",
"bevy_replicon",
"bevy_replicon_renet",
"bevy_sprite3d",
"bevy_trenchbroom",
"bevy_trenchbroom_avian",
"clap",
"dirs",
"happy_feet",
"nil 0.14.0",
"rand 0.8.5",
@@ -4991,7 +5066,7 @@ dependencies = [
"approx",
"arrayvec",
"bitflags 2.10.0",
"downcast-rs",
"downcast-rs 2.0.2",
"either",
"ena",
"foldhash 0.2.0",
@@ -5022,7 +5097,7 @@ dependencies = [
"approx",
"arrayvec",
"bitflags 2.10.0",
"downcast-rs",
"downcast-rs 2.0.2",
"either",
"ena",
"foldhash 0.2.0",
@@ -5308,6 +5383,15 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "quote"
version = "1.0.42"
@@ -5458,15 +5542,6 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb"
[[package]]
name = "redb"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae323eb086579a3769daa2c753bb96deb95993c534711e0dbe881b5192906a06"
dependencies = [
"libc",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
@@ -5564,6 +5639,18 @@ dependencies = [
"renetcode",
]
[[package]]
name = "renet_steam"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f6018afe469d3d2d49fab8fd1cecc46c588ac61498cad879d5781c44b277421"
dependencies = [
"bevy_ecs",
"log",
"renet",
"steamworks",
]
[[package]]
name = "renetcode"
version = "1.0.0"
@@ -5574,28 +5661,6 @@ dependencies = [
"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]]
name = "robust"
version = "1.2.0"
@@ -5770,12 +5835,31 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "self_cell"
version = "1.2.1"
@@ -5940,6 +6024,31 @@ dependencies = [
"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]]
name = "smol_str"
version = "0.2.2"
@@ -6019,6 +6128,12 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42862065c9e685d08cc3d9f6c609d4b46bd9684ec7e9420688eb979213469582"
[[package]]
name = "strict-num"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "strsim"
version = "0.10.0"
@@ -6238,6 +6353,31 @@ dependencies = [
"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]]
name = "tinystr"
version = "0.8.2"
@@ -6712,6 +6852,114 @@ dependencies = [
"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]]
name = "web-sys"
version = "0.3.83"
@@ -7390,6 +7638,7 @@ version = "0.30.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732"
dependencies = [
"ahash",
"android-activity",
"atomic-waker",
"bitflags 2.10.0",
@@ -7404,6 +7653,7 @@ dependencies = [
"dpi",
"js-sys",
"libc",
"memmap2",
"ndk 0.9.0",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
@@ -7415,11 +7665,17 @@ dependencies = [
"raw-window-handle",
"redox_syscall 0.4.1",
"rustix 0.38.44",
"sctk-adwaita",
"smithay-client-toolkit",
"smol_str",
"tracing",
"unicode-segmentation",
"wasm-bindgen",
"wasm-bindgen-futures",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-plasma",
"web-sys",
"web-time",
"windows-sys 0.52.0",
@@ -7490,6 +7746,12 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "xcursor"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
[[package]]
name = "xkbcommon-dl"
version = "0.4.2"

View File

@@ -51,24 +51,23 @@ bevy = { version = "0.17.0", default-features = false, features = [
"track_location",
] }
bevy-inspector-egui = "0.34"
bevy-persistent = { version = "0.9", features = ["ron"] }
bevy-steamworks = "0.15.0"
bevy_asset_loader = "=0.24.0-rc.1"
bevy_ballistic = { git = "https://github.com/rustunit/bevy_ballistic.git", rev = "b08ffec" }
bevy_common_assets = { version = "0.14.0", features = ["ron"] }
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_renet = "0.13.0"
# TODO: i dont think we need this in dedicated server mode
bevy_replicon_renet = { version = "0.13.0", features = ["renet_steam"] }
bevy_sprite3d = "7.0.0"
bevy_trenchbroom = { version = "0.10", default-features = false, features = [
"physics-integration",
] }
bevy_trenchbroom_avian = "0.10"
clap = { version = "=4.5.47", features = ["derive"] }
happy_feet = { git = "https://github.com/rustunit/happy_feet.git", rev = "919657fa", features = [
dirs = "6.0.0"
happy_feet = { git = "https://github.com/PROMETHIA-27/happy_feet.git", rev = "e3a4660e0b68f9bf4d6facb0fb4d93f2d7848b27", features = [
"serde",
] }
nil = "0.14.0"
@@ -76,6 +75,7 @@ rand = "=0.8.5"
ron = "0.8"
serde = { version = "1.0.219", features = ["derive"] }
shared = { path = "crates/shared" }
# TODO: i dont think we need this in dedicated server mode
steamworks = "0.12"
[profile.dev.package."*"]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,39 +0,0 @@
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,13 +6,7 @@ use crate::{
heads_database::HeadsDatabase,
};
use bevy::prelude::*;
#[cfg(feature = "client")]
use bevy_replicon::prelude::ClientTriggerExt;
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)]
#[reflect(Component)]
@@ -40,120 +34,15 @@ impl Backpack {
}
}
#[derive(Event, Serialize, Deserialize)]
#[derive(Event, Debug, Serialize, Deserialize)]
pub struct BackpackSwapEvent(pub usize);
pub fn plugin(app: &mut App) {
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);
}
#[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(
trigger: On<HeadCollected>,
mut cmds: Commands,
@@ -165,7 +54,7 @@ fn on_head_collect(
let (mut backpack, active_heads) = query.get_mut(entity)?;
if backpack.contains(head) || active_heads.contains(head) {
cmds.trigger(CashCollectEvent);
cmds.trigger(CashCollectEvent { entity });
} else {
backpack.insert(head, heads_db.as_ref());
}

View File

@@ -1,40 +0,0 @@
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,18 +1,3 @@
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 serde::{Deserialize, Serialize};
@@ -21,173 +6,3 @@ pub struct CameraTarget;
#[derive(Component, Reflect, Debug, Serialize, Deserialize, PartialEq)]
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,12 +1,17 @@
use crate::{
GameState, HEDZ_GREEN, global_observer, loading_assets::UIAssets, protocol::PlaySound,
GameState, global_observer,
physics_layers::GameLayer,
player::Player,
protocol::{GltfSceneRoot, PlaySound, is_server},
server_observer,
tb_entities::CashSpawn,
};
use avian3d::prelude::Rotation;
use avian3d::prelude::*;
use bevy::prelude::*;
use bevy_replicon::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Component, Reflect, Default)]
#[derive(Component, Reflect, Default, Deserialize, Serialize)]
#[reflect(Component)]
#[require(Transform)]
pub struct Cash;
@@ -20,32 +25,64 @@ pub struct CashInventory {
pub cash: i32,
}
#[derive(Event)]
pub struct CashCollectEvent;
pub fn plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(
Update,
(rotate, update_ui).run_if(in_state(GameState::Playing)),
);
server_observer!(app, on_cash_collect);
#[derive(EntityEvent)]
pub struct CashCollectEvent {
pub entity: Entity,
}
fn on_cash_collect(
_trigger: On<CashCollectEvent>,
pub fn plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Playing), setup.run_if(is_server));
app.add_systems(Update, rotate.run_if(in_state(GameState::Playing)));
server_observer!(app, on_cash_collected);
}
fn setup(mut commands: Commands, query: Query<Entity, With<CashSpawn>>) {
for entity in query.iter() {
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 cash: Single<&mut CashInventory>,
mut query_player: Query<&mut CashInventory, With<Player>>,
) {
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
if let Ok(mut cash) = query_player.get_mut(trigger.entity) {
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::CashCollect,
});
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
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>>) {
@@ -55,37 +92,3 @@ fn rotate(time: Res<Time>, mut query: Query<&mut Rotation, With<Cash>>) {
.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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,8 @@
use crate::{GameState, control::ControllerSet};
use crate::{
GameState,
control::{ControllerSet, Inputs},
player::{LocalPlayer, Player, PlayerBodyMesh},
};
use bevy::prelude::*;
use bevy_replicon::client::ClientSystems;
@@ -22,4 +26,22 @@ pub fn plugin(app: &mut App) {
.before(ClientSystems::Receive)
.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

@@ -0,0 +1,104 @@
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,
abilities::Healing,
loading_assets::{AudioAssets, GameAssets},
utils::{billboards::Billboard, observers::global_observer},
utils::{Billboard, observers::global_observer},
};
use bevy::prelude::*;
use rand::{Rng, thread_rng};

View File

@@ -1,6 +1,6 @@
use crate::{
GameState,
config::NetworkingConfig,
config::NetConfig,
protocol::{
ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping, messages::DespawnTbMapEntity,
},
@@ -17,18 +17,17 @@ use bevy_replicon::{
};
use bevy_replicon_renet::{
RenetChannelsExt,
netcode::{ClientAuthentication, NetcodeClientTransport, NetcodeError},
renet::{ConnectionConfig, RenetClient},
};
use bevy_steamworks::Client;
use bevy_trenchbroom::geometry::Brushes;
use std::{
net::{Ipv4Addr, UdpSocket},
time::SystemTime,
};
pub mod aim;
pub mod audio;
pub mod backpack;
mod backpack;
pub mod camera;
pub mod control;
pub mod cutscene;
pub mod debug;
pub mod enemy;
pub mod heal_effect;
@@ -37,25 +36,30 @@ mod settings;
pub mod setup;
pub mod steam;
pub mod ui;
mod utils;
pub fn plugin(app: &mut App) {
app.add_plugins((
backpack::plugin,
aim::plugin,
audio::plugin,
control::plugin,
debug::plugin,
enemy::plugin,
heal_effect::plugin,
player::plugin,
setup::plugin,
audio::plugin,
steam::plugin,
ui::plugin,
settings::plugin,
backpack::plugin,
camera::plugin,
utils::billboards::plugin,
cutscene::plugin,
));
app.add_systems(
OnEnter(GameState::Connecting),
connect_to_server.run_if(|config: Res<NetworkingConfig>| config.server.is_some()),
connect_to_server.run_if(|config: Res<NetConfig>| config.is_client()),
);
app.add_systems(Update, despawn_absent_map_entities);
app.add_systems(
@@ -89,8 +93,9 @@ fn on_disconnect() {
fn connect_to_server(
mut commands: Commands,
config: Res<NetworkingConfig>,
config: Res<NetConfig>,
channels: Res<RepliconChannels>,
steam_client: Option<Res<Client>>,
) -> Result {
let server_channels_config = channels.server_configs();
let client_channels_config = channels.client_configs();
@@ -102,32 +107,47 @@ fn connect_to_server(
});
commands.insert_resource(client);
commands.insert_resource(client_transport(&config)?);
if let NetConfig::SteamClient(host_steam_id) = &*config {
let Some(steam_client) = steam_client else {
return Err("Steam client not found".into());
};
info!("connecting to steam host: {host_steam_id:?}");
let transport = bevy_replicon_renet::steam::SteamClientTransport::new(
(**steam_client).clone(),
host_steam_id,
)?;
commands.insert_resource(transport);
} else if let NetConfig::NetcodeClient(host_addr) = &*config {
use std::time::SystemTime;
info!("connecting to netcode host: {host_addr:?}");
let current_time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let client_id = current_time.as_millis() as u64;
let socket = std::net::UdpSocket::bind((std::net::Ipv4Addr::UNSPECIFIED, 0))?;
let authentication = bevy_replicon_renet::netcode::ClientAuthentication::Unsecure {
client_id,
protocol_id: 0,
server_addr: *host_addr,
user_data: None,
};
let transport = bevy_replicon_renet::netcode::NetcodeClientTransport::new(
current_time,
authentication,
socket,
)?;
commands.insert_resource(transport);
}
Ok(())
}
fn client_transport(config: &NetworkingConfig) -> Result<NetcodeClientTransport, NetcodeError> {
let current_time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let client_id = current_time.as_millis() as u64;
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?;
let server_addr = config
.server
.flatten()
.unwrap_or_else(|| "127.0.0.1:31111".parse().unwrap());
let authentication = ClientAuthentication::Unsecure {
client_id,
protocol_id: 0,
server_addr,
user_data: None,
};
info!("attempting connection to {server_addr}");
NetcodeClientTransport::new(current_time, authentication, socket)
}
#[allow(clippy::type_complexity)]
fn migrate_remote_entities(
query: Query<(Entity, &TbMapEntityId), (Added<TbMapEntityId>, With<ConfirmHistory>)>,

View File

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

View File

@@ -1,10 +1,22 @@
use bevy::prelude::*;
use bevy_pkv::prelude::*;
use crate::{client::audio::SoundSettings, utils::Debounce};
use bevy::prelude::*;
use bevy_persistent::{Persistent, StorageFormat};
pub fn plugin(app: &mut App) {
app.insert_resource(PkvStore::new("Rustunit", "HEDZ"));
app.insert_resource(
Persistent::<SoundSettings>::builder()
.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(Startup, load_settings);
@@ -12,7 +24,7 @@ pub fn plugin(app: &mut App) {
fn persist_settings(
settings: Res<SoundSettings>,
mut pkv: ResMut<PkvStore>,
mut persistent: ResMut<Persistent<SoundSettings>>,
mut debounce: Debounce<1000>,
) -> Result {
if settings.is_changed() {
@@ -20,16 +32,12 @@ fn persist_settings(
}
if debounce.finished() {
pkv.set("audio", &*settings)?;
persistent.set(*settings)?;
}
Ok(())
}
fn load_settings(mut settings: ResMut<SoundSettings>, pkv: Res<PkvStore>) -> Result {
if let Ok(loaded) = pkv.get::<SoundSettings>("audio") {
*settings = loaded;
}
Ok(())
fn load_settings(persistent: Res<Persistent<SoundSettings>>, mut settings: ResMut<SoundSettings>) {
*settings = *persistent.get();
}

View File

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

View File

@@ -61,6 +61,14 @@ fn test_steam_system(steam_client: Res<Client>) {
},
);
let id = steam_client.user().steam_id();
info!("Steam ID: {:?}", id);
steam_client
.friends()
.set_rich_presence("connect", Some(id.raw().to_string().as_str()));
for friend in steam_client.friends().get_friends(FriendFlags::IMMEDIATE) {
info!(
"Steam Friend: {:?} - {}({:?})",

View File

@@ -1,15 +1,47 @@
use crate::{
GameState, HEDZ_GREEN,
backpack::backpack_ui::{
BACKPACK_HEAD_SLOTS, BackpackCountText, BackpackMarker, BackpackUiState, HeadDamage,
HeadImage, HeadSelector,
},
heads::HeadsImages,
client::ui::heads_ui::{HeadsImages, UiHeadState},
loading_assets::UIAssets,
};
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) {
app.register_type::<BackpackUiState>();
app.init_resource::<BackpackUiState>();
app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(
FixedUpdate,
@@ -152,10 +184,14 @@ fn spawn_head_ui(
}
fn update_visibility(
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
state: Res<BackpackUiState>,
mut backpack: Single<&mut Visibility, (With<BackpackMarker>, Without<BackpackCountText>)>,
mut count: Single<&mut Visibility, (Without<BackpackMarker>, With<BackpackCountText>)>,
) {
if !state.is_changed() {
return;
}
**backpack = if state.open {
Visibility::Visible
} else {
@@ -170,10 +206,14 @@ fn update_visibility(
}
fn update_count(
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
state: Res<BackpackUiState>,
text: Option<Single<Entity, With<BackpackCountText>>>,
mut writer: TextUiWriter,
) {
if !state.is_changed() {
return;
}
let Some(text) = text else {
return;
};
@@ -182,12 +222,16 @@ fn update_count(
}
fn update(
state: Single<&BackpackUiState, Changed<BackpackUiState>>,
state: Res<BackpackUiState>,
heads_images: Res<HeadsImages>,
mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without<HeadSelector>>,
mut head_damage: Query<(&HeadDamage, &mut Node), Without<HeadSelector>>,
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() {
if let Some(head) = &state.heads[*head] {
*vis = Visibility::Inherited;

View File

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

View File

@@ -1,7 +1,15 @@
mod backpack_ui;
mod cash_ui;
mod heads_ui;
mod pause;
pub use backpack_ui::{BACKPACK_HEAD_SLOTS, BackpackUiState};
use bevy::prelude::*;
pub use heads_ui::{HeadsImages, UiHeadState};
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(cash_ui::plugin);
}

View File

@@ -1,22 +1,12 @@
use crate::camera::MainCamera;
use crate::{client::camera::MainCamera, utils::Billboard};
use bevy::prelude::*;
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) {
if !app.is_plugin_added::<Sprite3dPlugin>() {
app.add_plugins(Sprite3dPlugin);
}
app.register_type::<Billboard>();
app.add_systems(Update, (face_camera, face_camera_no_parent));
}

View File

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

View File

@@ -1,25 +1,90 @@
use bevy::prelude::*;
use clap::Parser;
use std::net::SocketAddr;
use steamworks::SteamId;
pub fn plugin(app: &mut App) {
let config = NetworkingConfig::parse();
let config: NetConfig = config.into();
info!("net config: {:?}", config);
app.insert_resource(config);
}
#[derive(Resource, Parser, Debug)]
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct NetworkingConfig {
/// The IP/port to connect to.
/// If `None`, host a local server.
/// If Some(None), connect to the default server (`127.0.0.1:31111`)
/// Otherwise, connect to the given server.
/// Does nothing on the server.
struct NetworkingConfig {
/// Steam id of the host to connect to
#[arg(long)]
pub server: Option<Option<SocketAddr>>,
/// Whether or not to open a port when opening the client, for other clients
/// to connect. Does nothing if `server` is set.
pub steam_host_id: Option<String>,
/// Act as steam host
#[arg(long)]
pub host: bool,
pub steam_host: bool,
/// Act as host using netcode, so we have to define our port
#[arg(long)]
pub netcode_host: Option<Option<u16>>,
/// Host address we connect to as a client
#[arg(long)]
pub netcode_client: Option<Option<String>>,
}
#[derive(Resource, Debug)]
pub enum NetConfig {
Singleplayer,
SteamHost,
NetcodeHost { port: u16 },
SteamClient(SteamId),
NetcodeClient(SocketAddr),
}
impl NetConfig {
pub fn is_client(&self) -> bool {
matches!(
self,
NetConfig::SteamClient(_) | NetConfig::NetcodeClient(_)
)
}
pub fn is_host(&self) -> bool {
matches!(self, NetConfig::SteamHost | NetConfig::NetcodeHost { .. })
}
pub fn is_singleplayer(&self) -> bool {
!self.is_client() && !self.is_host()
}
}
impl From<NetworkingConfig> for NetConfig {
fn from(config: NetworkingConfig) -> Self {
match (
config.steam_host,
config.steam_host_id,
config.netcode_host,
config.netcode_client,
) {
(false, None, None, None) => Self::Singleplayer,
(true, None, None, None) => Self::SteamHost,
(false, Some(id), None, None) => Self::SteamClient(parse_steam_id(id)),
(false, None, Some(port), None) => Self::NetcodeHost {
port: port.unwrap_or(31111),
},
(false, None, None, Some(addr)) => Self::NetcodeClient(parse_addr(addr)),
_ => panic!("Invalid configuration"),
}
}
}
fn parse_addr(addr: Option<String>) -> SocketAddr {
addr.and_then(|addr| addr.parse().ok())
.unwrap_or_else(|| "127.0.0.1:31111".parse().unwrap())
}
fn parse_steam_id(id: String) -> SteamId {
let id: u64 = id.parse().unwrap();
SteamId::from_raw(id)
}

View File

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

View File

@@ -1,7 +1,7 @@
use super::ControllerSet;
use crate::{
GameState,
control::{Inputs, controller_common::MovementSpeedFactor},
control::{Inputs, SelectedController, controller_common::MovementSpeedFactor},
};
use bevy::prelude::*;
use happy_feet::prelude::MoveInput;
@@ -19,8 +19,17 @@ impl Plugin for CharacterControllerPlugin {
}
}
pub fn apply_controls(character: Single<(&mut MoveInput, &MovementSpeedFactor, &Inputs)>) {
let (mut char_input, factor, inputs) = character.into_inner();
char_input.set(inputs.look_dir * factor.0);
pub fn apply_controls(
mut query: Query<(
&mut MoveInput,
&MovementSpeedFactor,
&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,7 +1,10 @@
use crate::{
GameState,
animation::AnimationFlags,
control::{ControllerSet, ControllerSettings, Inputs, controller_common::MovementSpeedFactor},
control::{
ControllerSet, ControllerSettings, Inputs, SelectedController,
controller_common::MovementSpeedFactor,
},
protocol::is_server,
};
#[cfg(feature = "client")]
@@ -41,7 +44,7 @@ fn rotate_view(
) {
let (inputs, children) = controller.into_inner();
if inputs.view_mode {
if inputs.view_mode.is_free() {
return;
}
@@ -56,7 +59,7 @@ fn rotate_view(
}
fn apply_controls(
character: Single<(
mut query: Query<(
&mut MoveInput,
&mut Grounding,
&mut KinematicVelocity,
@@ -64,25 +67,39 @@ fn apply_controls(
&ControllerSettings,
&MovementSpeedFactor,
&Inputs,
&SelectedController,
)>,
) {
let (mut move_input, mut grounding, mut velocity, mut flags, settings, move_factor, inputs) =
character.into_inner();
for (
mut move_input,
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);
let mut direction = inputs.move_dir.extend(0.0).xzy();
let look_dir_right = inputs.look_dir.cross(Vec3::Y);
direction = (inputs.look_dir * direction.z) + (look_dir_right * direction.x);
let y_projection = direction.project_onto(ground_normal);
direction -= y_projection;
direction = direction.normalize_or_zero();
let mut direction = inputs.move_dir.extend(0.0).xzy();
let look_dir_right = inputs.look_dir.cross(Vec3::Y);
direction = (inputs.look_dir * direction.z) + (look_dir_right * direction.x);
let y_projection = direction.project_onto(ground_normal);
direction -= y_projection;
direction = direction.normalize_or_zero();
move_input.set(direction * move_factor.0);
move_input.set(direction * move_factor.0);
if inputs.jump && grounding.is_grounded() {
flags.jumping = true;
flags.jump_count += 1;
happy_feet::movement::jump(settings.jump_force, &mut velocity, &mut grounding, Dir3::Y)
if inputs.jump && grounding.is_grounded() {
flags.jumping = true;
flags.jump_count += 1;
happy_feet::movement::jump(settings.jump_force, &mut velocity, &mut grounding, Dir3::Y)
}
}
}

View File

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

View File

@@ -1,107 +1,5 @@
use crate::{
GameState,
camera::{CameraState, MainCamera},
global_observer,
tb_entities::{CameraTarget, CutsceneCamera, CutsceneCameraMovementEnd},
};
use bevy::prelude::*;
use bevy_trenchbroom::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Event, Serialize, Deserialize)]
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,9 +6,7 @@ use crate::{
protocol::{GltfSceneRoot, NetworkEnv, PlaySound},
server_observer,
tb_entities::SecretHead,
utils::{
billboards::Billboard, one_shot_force::OneShotImpulse, squish_animation::SquishAnimation,
},
utils::{Billboard, one_shot_force::OneShotImpulse, squish_animation::SquishAnimation},
};
use avian3d::prelude::*;
use bevy::{ecs::relationship::RelatedSpawner, prelude::*};

Some files were not shown because too many files have changed in this diff Show More