k4d3 commited on
Commit
48ca417
1 Parent(s): eec1434

Signed-off-by: Balazs Horvath <[email protected]>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -35
  2. .github/FUNDING.yml +1 -0
  3. .github/workflows/rust.yml +48 -0
  4. .gitignore +11 -0
  5. .gitmodules +3 -0
  6. .prettierrc +6 -0
  7. .vscode/launch.json +75 -0
  8. Cargo.toml +182 -0
  9. LICENSE.txt +23 -0
  10. README.md +326 -0
  11. build.rs +11 -0
  12. build/windows/icon.ico +0 -0
  13. build/windows/icon.rc +1 -0
  14. docs/.gitignore +3 -0
  15. docs/Compilation Options.md +55 -0
  16. docs/plugins/bevy_device_lang.md +20 -0
  17. docs/plugins/bevy_ecs_ldtk.md +79 -0
  18. docs/plugins/bevy_flurx.md +103 -0
  19. docs/plugins/bevy_rapier2d.md +32 -0
  20. rust-toolchain.toml +2 -0
  21. scripts/Run-Debug.ps1 +28 -0
  22. scripts/Run-Release.ps1 +28 -0
  23. scripts/Update-Project.ps1 +2 -0
  24. src/components/ai/actions/drink.rs +53 -0
  25. src/components/ai/actions/mod.rs +6 -0
  26. src/components/ai/mod.rs +52 -0
  27. src/components/ai/scorers/mod.rs +11 -0
  28. src/components/ai/scorers/thirsty.rs +34 -0
  29. src/components/ai/thirst.rs +30 -0
  30. src/components/animals.rs +127 -0
  31. src/components/animation.rs +67 -0
  32. src/components/armor.rs +6 -0
  33. src/components/camera/fit_inside_current_level.rs +72 -0
  34. src/components/camera/mod.rs +5 -0
  35. src/components/childof.rs +18 -0
  36. src/components/climbing.rs +59 -0
  37. src/components/collision.rs +344 -0
  38. src/components/deathzone.rs +5 -0
  39. src/components/ground.rs +75 -0
  40. src/components/health.rs +32 -0
  41. src/components/hunger.rs +35 -0
  42. src/components/interactions.rs +152 -0
  43. src/components/items.rs +34 -0
  44. src/components/line_of_sight.rs +94 -0
  45. src/components/mod.rs +20 -0
  46. src/components/name.rs +4 -0
  47. src/components/predefinedpath.rs +107 -0
  48. src/components/sensorbundle.rs +37 -0
  49. src/components/settings.rs +84 -0
  50. src/components/swimming.rs +45 -0
.gitattributes CHANGED
@@ -1,35 +1 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ levels/biomes.ldtk filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/FUNDING.yml ADDED
@@ -0,0 +1 @@
 
 
1
+ github: ka-de
.github/workflows/rust.yml ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Rust
2
+
3
+ on:
4
+ push:
5
+ branches: ["master"]
6
+ pull_request:
7
+ branches: ["master"]
8
+
9
+ env:
10
+ CARGO_TERM_COLOR: always
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Cargo Cache
19
+ uses: actions/cache@v4
20
+ with:
21
+ path: |
22
+ ~/.cargo/registry
23
+ ~/.cargo/git
24
+ target
25
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
26
+ restore-keys: |
27
+ ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
28
+ ${{ runner.os }}-cargo
29
+ - name: Install dependencies
30
+ run: sudo apt-get install -y clang pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 libwayland-dev libxkbcommon-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev mesa-vulkan-drivers
31
+ - name: Cargo Update
32
+ run: cargo update --verbose
33
+ - name: Build Debug
34
+ run: cargo build --features "bevy/dynamic_linking" --verbose
35
+ - name: Run tests
36
+ run: cargo test --features "bevy/dynamic_linking" --all-targets --verbose
37
+ - name: Pedantic Linting
38
+ run: cargo clippy -- -W clippy::pedantic
39
+ # - name: Package Debug Build
40
+ # run: |
41
+ # DATE=$(date +%Y-%m-%d)
42
+ # COMMIT_HASH=$(git rev-parse --short "$GITHUB_SHA")
43
+ # zip -r "separated-debug-build-${DATE}-${COMMIT_HASH}.zip" target/debug/
44
+ # - name: Upload Debug Build
45
+ # uses: actions/upload-artifact@v2
46
+ # with:
47
+ # name: Debug Build
48
+ # path: "*.zip"
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ debug/
2
+ target/
3
+ rustc-ice-*.txt
4
+
5
+ Cargo.lock
6
+
7
+ # These are backup files generated by rustfmt
8
+ **/*.rs.bk
9
+
10
+ # MSVC Windows builds of rustc generate these, which store debugging information
11
+ *.pdb
.gitmodules ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [submodule "assets"]
2
+ path = assets
3
+ url = https://huggingface.co/REPLACE/assets
.prettierrc ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "useTabs": false,
3
+ "tabWidth": 4,
4
+ "printWidth": 100,
5
+ "endOfLine": "lf"
6
+ }
.vscode/launch.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "type": "lldb",
6
+ "request": "launch",
7
+ "sourceLanguages": ["rust"],
8
+ "name": "Debug SEPARATED",
9
+ "cargo": {
10
+ "args": ["build", "--bin=separated", "--package=separated"],
11
+ "filter": {
12
+ "name": "separated",
13
+ "kind": "bin"
14
+ }
15
+ },
16
+ "env": {
17
+ "CARGO_MANIFEST_DIR": "${workspaceFolder}",
18
+ "RUSTFLAGS": "-Clinker=rust-lld.exe -Zshare-generics=n -Zthreads=0",
19
+ "PATH": "${env:HOME}/.rustup/toolchains/nightly-x86_64-pc-windows-msvc/bin;${workspaceFolder}/target/debug/deps;${env:PATH}"
20
+ },
21
+ "args": [],
22
+ "cwd": "${workspaceFolder}"
23
+ }
24
+ ]
25
+ }
26
+
27
+ /*
28
+ * The `#![allow(clippy::match_same_arms)]` is a Rust attribute that allows a specific lint warning to be ignored.
29
+ *
30
+ * `#!` is a Rust attribute, which is a way to annotate a module or crate with additional information.
31
+ * `allow` is a specific attribute that allows a lint warning to be ignored.
32
+ * `clippy::match_same_arms` is the name of the lint warning being allowed.
33
+ *
34
+ * The `match_same_arms` lint warns when a match expression has multiple arms with the same pattern. For example:
35
+ *
36
+ * ```rust
37
+ match foo {
38
+ 1 => println!("one"),
39
+ 1 => println!("also one"), // warning: same arm
40
+ }
41
+ * ```
42
+ *
43
+ * In this case, `clippy` would normally warn about the duplicate arm, suggesting that it might be an error.
44
+ * By adding the `#![allow(clippy::match_same_arms)]` attribute, you're telling `clippy` to ignore this specific
45
+ * warning for the entire crate or module. This can be useful if you intentionally have a match expression with
46
+ * duplicate arms, or if you're working on a legacy codebase that hasn't been updated to avoid this pattern.
47
+ *
48
+ * Note that this attribute only affects the `clippy` linter, and not the standard Rust compiler warnings.
49
+ */
50
+
51
+ /*
52
+ * RUSTFLAGS="-Funsafe-code --cap-lints=warn" cargo check
53
+ *
54
+ * This command will cause the compilation to fail if unsafe code is detected.
55
+ * However, it’s important to note that this method might not be foolproof for all cases,
56
+ * as some dependencies might require unsafe code to function correctly, and this could
57
+ * lead to false positives or prevent your project from compiling.
58
+ *
59
+ * Remember, while unsafe code is often necessary for low-level system programming tasks,
60
+ * its usage should be minimized and carefully reviewed to maintain the safety guarantees
61
+ * that Rust provides.
62
+ */
63
+
64
+ /*
65
+ * The `--cap-lints=warn` flag in Rust is used to set the maximum lint level for the entire project to warn.
66
+ *
67
+ * This means that even if a lint is defined with a more severe levelsuch as deny or forbid,
68
+ * it will only issue a warning rather than causing the compilation to fail. Essentially, this flag ensures that
69
+ * no matter what lint levels are specified within the code or by dependencies, they will not exceed a warning level.
70
+ *
71
+ * This can be particularly useful when compiling a large number of crates, some of which you may not have control over,
72
+ * and you want to ensure that lint issues do not prevent the project from building. However, it’s important to note that
73
+ * while this flag can help with getting a project to compile, it does not address the underlying issues that the lints are warning about.
74
+ * It’s generally a good idea to review and address lint warnings to maintain code quality.
75
+ */
Cargo.toml ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cargo-features = ["profile-rustflags"]
2
+
3
+ [package]
4
+ name = "separated"
5
+ version = "1.0.0"
6
+ edition = "2021"
7
+ authors = ["Balazs Horvath", "Gaeros"]
8
+ description = "A 2D platformer game."
9
+ documentation = "https://cringe.live/docs/games/separated/"
10
+ readme = "README.md"
11
+ homepage = "https://cringe.live/separated/"
12
+ repository = "https://github.com/ka-de/separated"
13
+ license = "MIT"
14
+ keywords = ["gamedev"]
15
+ publish = false
16
+
17
+ [lints.rust]
18
+ unsafe_code = "warn"
19
+
20
+ [workspace]
21
+ resolver = "2"
22
+
23
+ [profile.awoo]
24
+ inherits = "dev"
25
+ opt-level = 0
26
+ debug = true
27
+ debug-assertions = true
28
+ overflow-checks = true
29
+ lto = false
30
+ panic = 'unwind'
31
+ incremental = true
32
+ codegen-units = 16
33
+
34
+ # Enable all compiler optimizations in debug builds.
35
+ [profile.dev]
36
+ opt-level = 3
37
+
38
+ # Enable all compiler optimizations for dependencies in debug builds.
39
+ # With some link time optimizations, it might produce better optimized
40
+ # code, using whole-program analysis, at the cost of longer linking time.
41
+ [profile.dev.package."*"]
42
+ opt-level = 3
43
+ codegen-units = 1
44
+
45
+ # Enable every possible compiler optimizations and stripping for release builds.
46
+ [profile.release]
47
+ opt-level = 3
48
+ lto = true
49
+ codegen-units = 1
50
+ strip = true
51
+
52
+ [features]
53
+ # Build release with `cargo build --release --no-default-features`
54
+ default = ["dev_features"]
55
+ dev_features = [
56
+ "bevy/trace",
57
+ "bevy/file_watcher",
58
+ "bevy/embedded_watcher",
59
+ "bevy_progress/debug",
60
+ "bevy_rapier2d/debug-render-2d",
61
+ "big-brain/trace",
62
+ #"sickle_ui/dev",
63
+ "bevy_progress/debug",
64
+ # "bevy_incandescent/debug",
65
+ "dep:bevy-inspector-egui",
66
+ # Allow egui to take priority over actions when processing inputs.
67
+ "input-manager/egui",
68
+ "dep:bevy_mod_debugdump",
69
+ "dep:graphviz-rust",
70
+ ]
71
+
72
+ [build-dependencies]
73
+ embed-resource = "2.4.2"
74
+
75
+ [dependencies]
76
+ winit = { version = "0.29.15", default-features = false, features = ["rwh_06"] }
77
+ image = { version = "0.25.1", default-features = false, features = ["png"] }
78
+ rand = "0.8.5"
79
+ unicode-segmentation = "1.11.0"
80
+ bevy_rapier2d = { version = "0.26.0", default-features = false, features = [
81
+ "dim2",
82
+ "simd-stable",
83
+ "parallel",
84
+ ] }
85
+ wgpu = { version = "0.19.3", default-features = false, features = [
86
+ "dx12",
87
+ "metal",
88
+ "naga",
89
+ "naga-ir",
90
+ ] }
91
+ bevy_rand = { git = "https://github.com/ka-de/bevy_rand", features = [
92
+ #"rand_chacha",
93
+ "wyrand",
94
+ "serialize",
95
+ ] }
96
+ input-manager = { git = "https://github.com/ka-de/input-manager", features = [
97
+ "timing",
98
+ "ui",
99
+ ] }
100
+ bevy_device_lang = { git = "https://github.com/ka-de/bevy_device_lang" }
101
+ aery = { git = "https://github.com/ka-de/aery" }
102
+ bevy_vector_shapes = { git = "https://github.com/ka-de/bevy_vector_shapes" }
103
+ # pathfinding = { git = "https://github.com/ka-de/pathfinding" }
104
+ big-brain = { git = "https://github.com/ka-de/big-brain" }
105
+ bevy_mod_debugdump = { git = "https://github.com/ka-de/bevy_mod_debugdump", optional = true }
106
+ graphviz-rust = { version = "0.9.0", optional = true }
107
+ seldom_state = { git = "https://github.com/ka-de/seldom_state" }
108
+ # bevy_text_input = { git = "https://github.com/ka-de/bevy_text_input" }
109
+ # bevy_flurx = { git = "https://github.com/ka-de/bevy_flurx" }
110
+ # bevy_magic_light_2d = { git = "https://github.com/ka-de/bevy_magic_light_2d" }
111
+ #bevy_incandescent = { git = "https://github.com/ka-de/bevy_incandescent", features = [
112
+ # "debug",
113
+ # "ray_marching",
114
+ #] }
115
+ bevy_mod_aseprite = { git = "https://github.com/ka-de/bevy_mod_aseprite" }
116
+
117
+ [dependencies.bevy]
118
+ version = "0.13.2"
119
+ default-features = false
120
+ features = [
121
+ "png",
122
+ "vorbis",
123
+ "bevy_audio",
124
+ "animation",
125
+ "bevy_gilrs", # Gamepad support
126
+ "bevy_sprite",
127
+ "bevy_animation",
128
+ "bevy_ui",
129
+ "bevy_core_pipeline", # This sounds important, but I've only seen camera stuff in it for 2D?
130
+ "bevy_text",
131
+ "subpixel_glyph_atlas",
132
+ "bevy_render",
133
+ "multi-threaded",
134
+ "bevy_winit",
135
+ "x11",
136
+ "wayland",
137
+ # Future Stuff
138
+ #"bevy_state", # Needs research
139
+ #"sysinfo_plugin", # OwO whats this?
140
+ ]
141
+
142
+ [dependencies.bevy_asset_loader]
143
+ git = "https://github.com/ka-de/bevy_asset_loader"
144
+ default-features = false
145
+ features = ["2d", "standard_dynamic_assets", "progress_tracking"]
146
+
147
+ [dependencies.bevy_ecs_ldtk]
148
+ git = "https://github.com/ka-de/bevy_ecs_ldtk"
149
+
150
+ [dependencies.bevy-inspector-egui]
151
+ git = "https://github.com/ka-de/bevy-inspector-egui"
152
+ optional = true
153
+ default-features = false
154
+ features = ["highlight_changes"]
155
+
156
+ #[dependencies.bevy_tweening]
157
+ #git = "https://github.com/ka-de/bevy_tweening"
158
+
159
+ [dependencies.bevy_progress]
160
+ git = "https://github.com/ka-de/bevy_progress"
161
+ features = ["assets"]
162
+ default-features = false
163
+
164
+ [dependencies.bevy-steamworks]
165
+ git = "https://github.com/ka-de/bevy_steamworks"
166
+ features = ["serde"]
167
+
168
+ [dependencies.sickle_ui]
169
+ git = "https://github.com/ka-de/sickle_ui"
170
+
171
+ [dependencies.bevy_yarnspinner]
172
+ git = "https://github.com/ka-de/YarnSpinner-Rust"
173
+
174
+ [dependencies.bevy_hanabi]
175
+ git = "https://github.com/ka-de/bevy_hanabi"
176
+ default-features = false
177
+ features = ["2d"]
178
+
179
+ [dependencies.rodio]
180
+ version = "0.17"
181
+ default-features = false
182
+ features = ["vorbis"]
LICENSE.txt ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Balazs Horvath and Contributors
4
+
5
+ All rights reserved.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SEPARATED
2
+
3
+ ---
4
+
5
+ ## Introduction
6
+
7
+ SEPARATED is a 2D platformer game where you can talk to NPCs. Most of the game is not yet implemented.
8
+
9
+ ## Table of Contents
10
+
11
+ - [SEPARATED](#separated)
12
+ - [Introduction](#introduction)
13
+ - [Table of Contents](#table-of-contents)
14
+ - [Player Inputs ∆](#player-inputs-)
15
+ - [Debugging Keyboard Shortcuts](#debugging-keyboard-shortcuts)
16
+ - [TODO](#todo)
17
+ - [`filesystem_watcher` and `asset_processor`](#filesystem_watcher-and-asset_processor)
18
+ - [Rust Things 🦀](#rust-things-)
19
+ - [Run in Wolf Mode (Debug)](#run-in-wolf-mode-debug)
20
+ - [Pedantic linting](#pedantic-linting)
21
+ - [Linting on all packages, treating warnings as errors](#linting-on-all-packages-treating-warnings-as-errors)
22
+ - [Format code](#format-code)
23
+ - [Test without default features](#test-without-default-features)
24
+ - [Test with only the `bevy_ui` features](#test-with-only-the-bevy_ui-features)
25
+ - [Test with all features enabled](#test-with-all-features-enabled)
26
+ - [Test with all features enabled on nightly](#test-with-all-features-enabled-on-nightly)
27
+ - [Generate documentation with all features enabled](#generate-documentation-with-all-features-enabled)
28
+ - [`seldom_state` + `input_manager` Example](#seldom_state--input_manager-example)
29
+
30
+ ## Player Inputs ∆
31
+
32
+ | Input | KeyCode | Gamepad Button/Axis |
33
+ | :----------- | :-----------------------: | :-------------------------: |
34
+ | **Run** | **Shift** | Xbox: **X** PS5: **Square** |
35
+ | **Interact** | **E** | Xbox: **B** PS5: **◯** |
36
+ | **Attack1** | **Q** | Xbox/PS5: **L1** |
37
+ | **Jump** | **Space** | Xbox: **A** PS5: **╳** |
38
+ | **Move** | **WASD** + **Arrow Keys** | **Any Axis + D-Pad** |
39
+
40
+ ## Debugging Keyboard Shortcuts
41
+
42
+ | Action | KeyCode |
43
+ | :----------------------------- | :-----: |
44
+ | Toggle Physics Wireframes | F9 |
45
+ | StateInspector (**GameState**) | F10 |
46
+ | WorldInspector | F11 |
47
+
48
+ ## TODO
49
+
50
+ ---
51
+
52
+ - **Use WyRand instead of `thread_rng()`**
53
+
54
+ ```rust
55
+ fn print_random_value(mut rng: ResMut<GlobalEntropy<WyRand>>) {
56
+ println!("Random value: {}", rng.next_u32());
57
+ }
58
+
59
+ use bevy_rand::WyRand;
60
+ use bevy_rand::prelude::{GlobalEntropy, ForkableRng};
61
+
62
+ #[derive(Component)]
63
+ struct Source;
64
+
65
+ fn setup_source(mut commands: Commands, mut global: ResMut<GlobalEntropy<WyRand>>) {
66
+ commands
67
+ .spawn((
68
+ Source,
69
+ global.fork_rng(),
70
+ ));
71
+ }
72
+ ```
73
+
74
+ ---
75
+
76
+ ```rust
77
+ if ( jumping || falling ) {
78
+
79
+ if velocity.y.abs() < jumpHangTimeThreshold {
80
+ // Increase acceleration for this duration also.
81
+
82
+ // Reduce gravity.
83
+ }
84
+ }
85
+
86
+ // If the player is moving downwards..
87
+ if velocity.y < 0 {
88
+ // Increase gravity while falling.
89
+ gravityScale *= fallGravityMultiplier;
90
+
91
+ // Cap maximum fall speed, so when falling over large distances,
92
+ // we don't accelerate to insanely high speeds.
93
+ }
94
+ ```
95
+
96
+ - **Localization**
97
+
98
+ - ⚠️ Started work by integrating `bevy_device_lang`. Requires a proper system that saves this value and allows the player to change it in the game menu, and also requires starting work on localization and saving and loading settings.
99
+
100
+ - **`bevy_asepritesheet` + `bevy_ecs_ldtk` integration.**
101
+
102
+ - **Patrol**
103
+
104
+ - Flip sprite when turning around!
105
+
106
+ - **Movement Improvements**
107
+ - Movement animations.
108
+ - Movement particle effects.
109
+ - Coyote (Grace) Time after falling off a ledge.
110
+ - Maybe needs a raycast in front of the player? Timer needs to start before falling off a ledge.
111
+ - **Jump Improvements**
112
+ - Jumping animations.
113
+ - Jumping particle effects.
114
+ - Wall Jumping
115
+ - ~~Prevent player movement for a short duration during the wall jump.~~ Reduce run force? Maybe a lerp between the wall jump speed and running speed?
116
+ - Air Time
117
+ - Jump Height
118
+ - Increase the player's jump height the longer the jump button is being held down.
119
+ - Clamp maximum falling speed.
120
+ - Coyote Time while jumping and pressing the jump button.
121
+ - There is already some check for being in the air we just need the input part I think.
122
+ - Bonus Air Time
123
+ - Peak Control
124
+ - Fast Fall
125
+ - Increase Player's falling speed after the peak of their jump by adjusting gravity.
126
+ - **Game Feel Improvements**
127
+
128
+ This is kinda broad but always iterate over every small mechanic towards more fun.
129
+
130
+ - **AI Stuff** ⚠️ Started work
131
+
132
+ - Pass player input(s) to ai-brain so it can use it for prediction.
133
+ - Basic Timer with Action Scheduling
134
+ - Thirst ✅
135
+ - Fatigue ⚠️
136
+
137
+ - **Pathfinding** ⚠️ Started work
138
+ - Use something to copy `dxil.dll` and `dxcompiler.dll` to Windows builds.
139
+ - **YarnSpinner**
140
+ - Begin YarnSpinner integration ✅
141
+ - YarnSpinner+LDTK integration ⚠️ Started work
142
+ - **UI**
143
+ - sickle_ui
144
+ - labels ✅
145
+ - keycap/gamepad button switching ⚠️
146
+
147
+ ## `filesystem_watcher` and `asset_processor`
148
+
149
+ ???
150
+
151
+ ## Rust Things 🦀
152
+
153
+ ---
154
+
155
+ ### Run in Wolf Mode (Debug)
156
+
157
+ ```pwsh
158
+ cargo run --profile awoo 2>&1 | Out-String -Stream | Where-Object { $_ -notmatch "ID3D12Device::CreateCommittedResource:" -and $_ -notmatch "Live Object at" -and $_ -notmatch "LineGizmo" -and $_ -notmatch "End of Frame" -and $_ -notmatch "prepare_windows" -and $_ -notmatch "cleanup" -and $_ -notmatch "SwapChain" -and $_ -notmatch "create_view" }
159
+ ```
160
+
161
+ ### Pedantic linting
162
+
163
+ ```bash
164
+ cargo clippy -- -W clippy::pedantic
165
+ ```
166
+
167
+ ### Linting on all packages, treating warnings as errors
168
+
169
+ ```bash
170
+ cargo clippy --workspace --all-targets --all-features -- -D warnings
171
+ ```
172
+
173
+ This command runs the `clippy` linter on all packages in the workspace, for all targets and features. The `-D warnings` option treats any warnings as errors.
174
+
175
+ ### Format code
176
+
177
+ ```bash
178
+ cargo fmt --all
179
+ ```
180
+
181
+ This command formats the code in every package using the default formatting rules provided by `rustfmt`.
182
+
183
+ ### Test without default features
184
+
185
+ ```bash
186
+ cargo test --no-default-features
187
+ ```
188
+
189
+ This command runs tests in the package, but disables the default features.
190
+
191
+ ### Test with only the `bevy_ui` features
192
+
193
+ ```bash
194
+ cargo test --no-default-features --features="bevy_ui"
195
+ ```
196
+
197
+ This command runs tests with only the `bevy_ui` feature enabled.
198
+
199
+ ### Test with all features enabled
200
+
201
+ ```bash
202
+ cargo test --all-features
203
+ ```
204
+
205
+ This command runs tests with all features enabled.
206
+
207
+ ### Test with all features enabled on nightly
208
+
209
+ ```bash
210
+ cargo +nightly build --all-features
211
+ ```
212
+
213
+ This command builds the package with all features enabled using the nightly version of the Rust compiler. This is typically used for generating documentation on docs.rs.
214
+
215
+ ### Generate documentation with all features enabled
216
+
217
+ ```bash
218
+ cargo +nightly doc --all-features --no-deps
219
+ ```
220
+
221
+ This command generates documentation for the package with all features enabled, without including dependencies, using the nightly version of the Rust compiler.
222
+
223
+ ## `seldom_state` + `input_manager` Example
224
+
225
+ ```rust
226
+ // In this game, you can move with the left and right arrow keys, and jump with space.
227
+ // `input-manager` handles the input.
228
+
229
+ use bevy::prelude::*;
230
+ use input_manager::{ axislike::VirtualAxis, prelude::* };
231
+ use seldom_state::prelude::*;
232
+
233
+ fn main() {
234
+ App::new()
235
+ .add_plugins((DefaultPlugins, InputManagerPlugin::<Action>::default(), StateMachinePlugin))
236
+ .add_systems(Startup, init)
237
+ .add_systems(Update, (walk, fall))
238
+ .run();
239
+ }
240
+
241
+ const JUMP_VELOCITY: f32 = 500.0;
242
+
243
+ fn init(mut commands: Commands, asset_server: Res<AssetServer>) {
244
+ commands.spawn(Camera2dBundle::default());
245
+
246
+ commands.spawn((
247
+ SpriteBundle {
248
+ transform: Transform::from_xyz(500.0, 0.0, 0.0),
249
+ texture: asset_server.load("player.png"),
250
+ ..default()
251
+ },
252
+ // From `input-manager`
253
+ InputManagerBundle {
254
+ input_map: InputMap::default()
255
+ .insert(Action::Move, VirtualAxis::horizontal_arrow_keys())
256
+ .insert(Action::Move, SingleAxis::symmetric(GamepadAxisType::LeftStickX, 0.0))
257
+ .insert(Action::Jump, KeyCode::Space)
258
+ .insert(Action::Jump, GamepadButtonType::South)
259
+ .build(),
260
+ ..default()
261
+ },
262
+ // This state machine achieves a very rigid movement system. Consider a state machine for
263
+ // whatever parts of your player controller that involve discrete states. Like the movement
264
+ // in Castlevania and Celeste, and the attacks in a fighting game.
265
+ StateMachine::default()
266
+ // Whenever the player presses jump, jump
267
+ .trans::<Grounded, _>(just_pressed(Action::Jump), Falling {
268
+ velocity: JUMP_VELOCITY,
269
+ })
270
+ // When the player hits the ground, idle
271
+ .trans::<Falling, _>(grounded, Grounded::Idle)
272
+ // When the player is grounded, set their movement direction
273
+ .trans_builder(value_unbounded(Action::Move), |_: &Grounded, value| {
274
+ Some(match value {
275
+ value if value > 0.5 => Grounded::Right,
276
+ value if value < -0.5 => Grounded::Left,
277
+ _ => Grounded::Idle,
278
+ })
279
+ }),
280
+ Grounded::Idle,
281
+ ));
282
+ }
283
+
284
+ #[derive(Actionlike, Clone, Eq, Hash, PartialEq, Reflect)]
285
+ enum Action {
286
+ Move,
287
+ Jump,
288
+ }
289
+
290
+ fn grounded(In(entity): In<Entity>, fallings: Query<(&Transform, &Falling)>) -> bool {
291
+ let (transform, falling) = fallings.get(entity).unwrap();
292
+ transform.translation.y <= 0.0 && falling.velocity <= 0.0
293
+ }
294
+
295
+ #[derive(Clone, Copy, Component, Reflect)]
296
+ #[component(storage = "SparseSet")]
297
+ enum Grounded {
298
+ Left = -1,
299
+ Idle = 0,
300
+ Right = 1,
301
+ }
302
+
303
+ #[derive(Clone, Component, Reflect)]
304
+ #[component(storage = "SparseSet")]
305
+ struct Falling {
306
+ velocity: f32,
307
+ }
308
+
309
+ const PLAYER_SPEED: f32 = 200.0;
310
+
311
+ fn walk(mut groundeds: Query<(&mut Transform, &Grounded)>, time: Res<Time>) {
312
+ for (mut transform, grounded) in &mut groundeds {
313
+ transform.translation.x += (*grounded as i32 as f32) * time.delta_seconds() * PLAYER_SPEED;
314
+ }
315
+ }
316
+
317
+ const GRAVITY: f32 = -1000.0;
318
+
319
+ fn fall(mut fallings: Query<(&mut Transform, &mut Falling)>, time: Res<Time>) {
320
+ for (mut transform, mut falling) in &mut fallings {
321
+ let dt = time.delta_seconds();
322
+ falling.velocity += dt * GRAVITY;
323
+ transform.translation.y += dt * falling.velocity;
324
+ }
325
+ }
326
+ ```
build.rs ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Used to embed resources like files or binaries into the executable.
2
+ extern crate embed_resource;
3
+ use std::env;
4
+
5
+ fn main() {
6
+ let target = env::var("TARGET").unwrap();
7
+ if target.contains("windows") {
8
+ // Sets the game icon in the executable for Windows.
9
+ embed_resource::compile("build/windows/icon.rc", embed_resource::NONE);
10
+ }
11
+ }
build/windows/icon.ico ADDED
build/windows/icon.rc ADDED
@@ -0,0 +1 @@
 
 
1
+ app_icon ICON "icon.ico"
docs/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Ignore everything that is not a `.dot` in the graphs directory.
2
+ /graphs/*.png
3
+ /graphs/*.svg
docs/Compilation Options.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Compilation Options
2
+
3
+ ---
4
+
5
+ ## Render Engine Graphs
6
+
7
+ ---
8
+
9
+ You can render graphs of the engine if you set the `RENDER_GRAPHS` environment variable `true`. By default it will output `.dot`, `.svg` and `.png` formats for each possible graphs in the `./docs/graph` folder, but it takes a long time so only do this if you like looking at game engines naked!
10
+
11
+ Bash:
12
+
13
+ ```bash
14
+ RENDER_GRAPHS=true cargo run
15
+ ```
16
+
17
+ PowerShell:
18
+
19
+ ```pwsh
20
+ $env:RENDER_GRAPHS = "true"
21
+ cargo run
22
+ ```
23
+
24
+ ## Tracing
25
+
26
+ ---
27
+
28
+ Bash:
29
+
30
+ ```bash
31
+ $env:RUST_LOG="trace" && cargo run --release --features "bevy/trace_tracy"
32
+ ```
33
+
34
+ PowerShell:
35
+
36
+ ```pwsh
37
+ $env:RUST_LOG="trace"
38
+ cargo run --release --features bevy/trace_tracy
39
+ ```
40
+
41
+ ## Filter out DX12 spam with PowerShell
42
+
43
+ ---
44
+
45
+ PowerShell:
46
+
47
+ ```pwsh
48
+ cargo run 2>&1 | Out-String -Stream | Where-Object { $_ -notmatch "ID3D12Device" -and $_ -notmatch "Live Object at" }
49
+ ```
50
+
51
+ With tracing:
52
+
53
+ ```pwsh
54
+ cargo run --features bevy/trace_tracy 2>&1 | Out-String -Stream | Where-Object { $_ -notmatch "ID3D12Device" -and $_ -notmatch "Live Object at" }
55
+ ```
docs/plugins/bevy_device_lang.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # `bevy_device_lang`
2
+
3
+ ---
4
+
5
+ Provides access device language cross-platform: iOS, Android, Web (Wasm), Windows & Linux. Useful to support app localization in the right language.
6
+
7
+ Example usages:
8
+
9
+ ```rust
10
+ fn bevy_system() {
11
+ let lang : Option<String> = bevy_device_lang::get_lang();
12
+ // ..
13
+ }
14
+
15
+ fn get_device_language() {
16
+ info!("Device language is {:?}", bevy_device_lang::get_lang());
17
+ }
18
+
19
+ add_systems(Startup, get_device_language)
20
+ ```
docs/plugins/bevy_ecs_ldtk.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # `bevy_ecs_ldtk`
2
+
3
+ ---
4
+
5
+ ## Customizing LDtk entities
6
+
7
+ ---
8
+
9
+ The spawning of entities can be customized by registering a bundle that will be used for spawning the entity. The bundle struct must implement the `LdtkEntity` trait:
10
+
11
+ ```rust
12
+ #[derive(Clone, Default, Bundle, LdtkEntity)]
13
+ pub struct PlayerBundle {
14
+ #[sprite_bundle("player.png")]
15
+ pub sprite_bundle: SpriteBundle,
16
+ #[from_entity_instance]
17
+ pub collider_bundle: ColliderBundle,
18
+ pub player: Player,
19
+ #[worldly]
20
+ pub worldly: Worldly,
21
+ }
22
+
23
+ fn my_plugin(app: &mut App) {
24
+ // Registers PlayerBundle to be spawned for a given Entity identifier
25
+ app.register_ldtk_entity::<PlayerBundle>("Player");
26
+ }
27
+ ```
28
+
29
+ The `LdtkEntity` trait can either be derived manually or using the derive macro as in the above example. Using derive, the fields can be components or bundles. Their instantiation can be customized using:
30
+
31
+ ## `Default` trait
32
+
33
+ ---
34
+
35
+ All fields must implement the `Default` trait. Implementing it manually lets you configure the initialization. This approach is relevant when:
36
+
37
+ - All usages of the component/bundle should have the same initial value,
38
+ - The initial value doesn't depend on the LDtk data of the instance.
39
+
40
+ ## `#[with(...)]` and `#[from_entity_instance]`
41
+
42
+ ---
43
+
44
+ These allow the field to be constructed from an [`&EntityInstance`](https://docs.rs/bevy_ecs_ldtk/latest/bevy_ecs_ldtk/ldtk/struct.EntityInstance.html), enabling access to the data of the instance (e.g., identifier, fields, pivot, etc.).
45
+ `#[with(...)]` allows specifying a `fn (entity: &EntityInstance) -> T` user function that will construct the field `T` from the `&EntityInstance`. `#[from_entity_instance]` is similar but uses the `From<&EntityInstance>` trait of the field, which should provide a `fn from(entity: &EntityInstance) -> T` function for constructing the field.
46
+
47
+ ## Special components
48
+
49
+ ---
50
+
51
+ Some sprite and LDtk components have predefined instantiation methods:
52
+
53
+ - `SpriteBundle` can be initialized using the [`#[sprite_bundle(...)]`](https://docs.rs/bevy_ecs_ldtk/latest/bevy_ecs_ldtk/app/trait.LdtkEntity.html#sprite_bundle) tag. See the player example.
54
+ - Similarly, `SpriteSheetBundle` can be initialized by specifying the [`#[sprite_sheet_bundle("path/to/asset.png", tile_width, tile_height, columns, rows, padding, offset, index)]`](https://docs.rs/bevy_ecs_ldtk/latest/bevy_ecs_ldtk/app/trait.LdtkEntity.html#sprite_sheet_bundle) tag.
55
+ - Both of the above tags will use data from the visual editor if no path or sheet coordinate are specified: `#[sprite_bundle]` and `#[sprite_sheet_bundle]`.
56
+ - `#[worldly] worldly: Worldly,` will attach a special component specifying that the entity should not be despawned when their level despawns.
57
+ - `#[grid_coords] grid_coords: GridCoords,` attach a component containing the initial grid position of the component.
58
+
59
+ ## Manually implementing `LdtkEntity`
60
+
61
+ ---
62
+
63
+ For advanced usage, the `LdtkEntity` trait can be manually implemented, defining the constructor function:
64
+
65
+ ```rust
66
+ impl LdtkEntity for MyComponentOrBundle {
67
+ fn bundle_entity(
68
+ entity_instance: &EntityInstance,
69
+ layer_instance: &LayerInstance,
70
+ tileset: Option<&Handle<Image>>,
71
+ tileset_definition: Option<&TilesetDefinition>,
72
+ asset_server: &AssetServer,
73
+ texture_atlases: &mut Assets<TextureAtlas>
74
+ ) -> Self
75
+ { Self { ... } }
76
+ }
77
+ ```
78
+
79
+ The arguments provide everything needed for inspecting the entity (`&EntityInstance`, tile sets), the level layer (`&LayerInstance`), and loading assets (`&AssetServer` and `&mut Assets<TextureAtlas>`).
docs/plugins/bevy_flurx.md ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # `bevy_flurx`
2
+
3
+ ---
4
+
5
+ ## Spawn and Respawn `flurx` Reactor
6
+
7
+ ---
8
+
9
+ ```rust
10
+ //! This example shows how to toggle [`Reactor`] processing.
11
+ //!
12
+ //! When you press [`KeyCode::Escape`], the box stops rotating.
13
+ //! When you press [`KeyCode::Enter`], the box starts rotating again.
14
+ //!
15
+ //! [`Reactor`]: bevy_flurx::prelude::Reactor
16
+
17
+ use bevy::DefaultPlugins;
18
+ use bevy::prelude::*;
19
+
20
+ use bevy_flurx::prelude::*;
21
+
22
+ #[derive(Component)]
23
+ struct RotateBox;
24
+
25
+ fn main() {
26
+ App::new()
27
+ .add_plugins((DefaultPlugins, FlurxPlugin))
28
+ .add_systems(Startup, (setup_camera_and_box, spawn_reactor))
29
+ .add_systems(Update, toggle)
30
+ .run();
31
+ }
32
+
33
+ fn setup_camera_and_box(
34
+ mut commands: Commands,
35
+ mut meshes: ResMut<Assets<Mesh>>,
36
+ mut materials: ResMut<Assets<StandardMaterial>>
37
+ ) {
38
+ commands.spawn((
39
+ PbrBundle {
40
+ mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
41
+ material: materials.add(StandardMaterial {
42
+ base_color: Color::BLUE,
43
+ ..default()
44
+ }),
45
+ ..default()
46
+ },
47
+ RotateBox,
48
+ ));
49
+ commands.spawn(PointLightBundle {
50
+ point_light: PointLight {
51
+ shadows_enabled: true,
52
+ intensity: 10_000_000.0,
53
+ range: 100.0,
54
+ ..default()
55
+ },
56
+ transform: Transform::from_xyz(8.0, 16.0, 8.0),
57
+ ..default()
58
+ });
59
+ commands.spawn(Camera3dBundle {
60
+ transform: Transform::from_xyz(0.0, 0.0, 6.0),
61
+ ..default()
62
+ });
63
+ }
64
+
65
+ fn spawn_reactor(mut commands: Commands) {
66
+ commands.spawn(
67
+ Reactor::schedule(|task| async move {
68
+ task.will(Update, wait::until(rotate_shape)).await;
69
+ })
70
+ );
71
+ }
72
+
73
+ fn rotate_shape(mut shape: Query<&mut Transform, With<RotateBox>>, time: Res<Time>) -> bool {
74
+ for mut t in shape.iter_mut() {
75
+ t.rotate_y(time.delta_seconds());
76
+ }
77
+ false
78
+ }
79
+
80
+ fn toggle(
81
+ mut commands: Commands,
82
+ reactor: Query<Entity, With<Reactor>>,
83
+ input: Res<ButtonInput<KeyCode>>
84
+ ) {
85
+ if input.just_pressed(KeyCode::Escape) {
86
+ if let Ok(entity) = reactor.get_single() {
87
+ info!("Despawning reactor.");
88
+ commands.entity(entity).remove::<Reactor>();
89
+ }
90
+ }
91
+
92
+ if input.just_pressed(KeyCode::Enter) {
93
+ if reactor.iter().next().is_none() {
94
+ info!("Spawning reactor.");
95
+ commands.spawn(
96
+ Reactor::schedule(|task| async move {
97
+ task.will(Update, wait::until(rotate_shape)).await;
98
+ })
99
+ );
100
+ }
101
+ }
102
+ }
103
+ ```
docs/plugins/bevy_rapier2d.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # `bevy_rapier2d`
2
+
3
+ ---
4
+
5
+ ## Default RapierConfiguration
6
+
7
+ ---
8
+
9
+ ```rust
10
+ impl RapierConfiguration {
11
+ /// Configures rapier with the specified length unit.
12
+ ///
13
+ /// See the documentation of [`IntegrationParameters::length_unit`] for additional details
14
+ /// on that argument.
15
+ ///
16
+ /// The default gravity is automatically scaled by that length unit.
17
+ pub fn new(length_unit: Real) -> Self {
18
+ Self {
19
+ gravity: Vect::Y * -9.81 * length_unit,
20
+ physics_pipeline_active: true,
21
+ query_pipeline_active: true,
22
+ timestep_mode: TimestepMode::Variable {
23
+ max_dt: 1.0 / 60.0,
24
+ time_scale: 1.0,
25
+ substeps: 1,
26
+ },
27
+ scaled_shape_subdivision: 10,
28
+ force_update_from_transform_changes: false,
29
+ }
30
+ }
31
+ }
32
+ ```
rust-toolchain.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [toolchain]
2
+ channel = "nightly"
scripts/Run-Debug.ps1 ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Define a function to colorize each line based on the log level
2
+ function Colorize-Output {
3
+ param([string]$Text)
4
+
5
+ if ($Text -match "DEBUG") {
6
+ Write-Host $Text -ForegroundColor Magenta
7
+ }
8
+ elseif ($Text -match "INFO") {
9
+ Write-Host $Text -ForegroundColor Blue
10
+ }
11
+ elseif ($Text -match "ERROR") {
12
+ Write-Host $Text -ForegroundColor Red
13
+ }
14
+ elseif ($Text -match "WARN") {
15
+ Write-Host $Text -ForegroundColor Yellow
16
+ }
17
+ elseif ($Text -match "TRACE") {
18
+ Write-Host $Text -ForegroundColor Cyan
19
+ }
20
+ else {
21
+ Write-Host $Text
22
+ }
23
+ }
24
+
25
+ # Run the command, filter the output, and pipe it to the colorizing function
26
+ cargo run 2>&1 | Out-String -Stream | Where-Object { $_ -notmatch "ID3D12Device::CreateCommittedResource:" -and $_ -notmatch "Live Object at" } | ForEach-Object {
27
+ Colorize-Output $_
28
+ }
scripts/Run-Release.ps1 ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Define a function to colorize each line based on the log level
2
+ function Colorize-Output {
3
+ param([string]$Text)
4
+
5
+ if ($Text -match "DEBUG") {
6
+ Write-Host $Text -ForegroundColor Magenta
7
+ }
8
+ elseif ($Text -match "INFO") {
9
+ Write-Host $Text -ForegroundColor Blue
10
+ }
11
+ elseif ($Text -match "ERROR") {
12
+ Write-Host $Text -ForegroundColor Red
13
+ }
14
+ elseif ($Text -match "WARN") {
15
+ Write-Host $Text -ForegroundColor Yellow
16
+ }
17
+ elseif ($Text -match "TRACE") {
18
+ Write-Host $Text -ForegroundColor Cyan
19
+ }
20
+ else {
21
+ Write-Host $Text
22
+ }
23
+ }
24
+
25
+ # Run the command, filter the output, and pipe it to the colorizing function
26
+ cargo run --release 2>&1 | Out-String -Stream | Where-Object { $_ -notmatch "ID3D12Device::CreateCommittedResource:" -and $_ -notmatch "Live Object at" } | ForEach-Object {
27
+ Colorize-Output $_
28
+ }
scripts/Update-Project.ps1 ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ git pull
2
+ cargo update
src/components/ai/actions/drink.rs ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::prelude::*;
2
+ use big_brain::prelude::*;
3
+
4
+ use crate::components::ai::thirst::Thirst;
5
+
6
+ #[derive(Reflect, Clone, Component, Debug, ActionBuilder)]
7
+ pub(crate) struct Drink {
8
+ pub until: f32,
9
+ pub per_second: f32,
10
+ }
11
+
12
+ // Action systems execute according to a state machine, where the states are
13
+ // labeled by ActionState.
14
+ pub(crate) fn drink_action_system(
15
+ time: Res<Time>,
16
+ mut thirsts: Query<&mut Thirst>,
17
+ // We execute actions by querying for their associated Action Component
18
+ // (Drink in this case). You'll always need both Actor and ActionState.
19
+ mut query: Query<(&Actor, &mut ActionState, &Drink, &ActionSpan)>
20
+ ) {
21
+ for (Actor(actor), mut state, drink, span) in &mut query {
22
+ // This sets up the tracing scope. Any `debug` calls here will be
23
+ // spanned together in the output.
24
+ let _guard = span.span().enter();
25
+
26
+ // Use the drink_action's actor to look up the corresponding Thirst Component.
27
+ if let Ok(mut thirst) = thirsts.get_mut(*actor) {
28
+ match *state {
29
+ ActionState::Requested => {
30
+ debug!("Time to drink some water!");
31
+ *state = ActionState::Executing;
32
+ }
33
+ ActionState::Executing => {
34
+ trace!("Drinking...");
35
+ thirst.thirst -=
36
+ drink.per_second * ((time.delta().as_micros() as f32) / 1_000_000.0);
37
+ if thirst.thirst <= drink.until {
38
+ // To "finish" an action, we set its state to Success or
39
+ // Failure.
40
+ debug!("Done drinking water");
41
+ *state = ActionState::Success;
42
+ }
43
+ }
44
+ // All Actions should make sure to handle cancellations!
45
+ ActionState::Cancelled => {
46
+ debug!("Action was cancelled. Considering this a failure.");
47
+ *state = ActionState::Failure;
48
+ }
49
+ _ => {}
50
+ }
51
+ }
52
+ }
53
+ }
src/components/ai/actions/mod.rs ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ // In most cases, the ActionBuilder just attaches the Action component to the
2
+ // actor entity. In this case, you can use the derive macro `ActionBuilder`
3
+ // to make your Action Component implement the ActionBuilder trait.
4
+ // You need your type to implement Clone and Debug (necessary for ActionBuilder)
5
+
6
+ pub(crate) mod drink;
src/components/ai/mod.rs ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 🧠 - AI
2
+
3
+ // Not to be confused by an actual LLM!
4
+
5
+ pub(crate) mod actions;
6
+ pub(crate) mod scorers;
7
+
8
+ // Components
9
+ pub(crate) mod thirst;
10
+
11
+ use bevy::prelude::*;
12
+
13
+ use big_brain::prelude::{ FirstToScore, Thinker };
14
+
15
+ use crate::components::ai::{ actions::drink::Drink, scorers::thirsty::Thirsty, thirst::Thirst };
16
+
17
+ // Now that we have all that defined, it's time to add a Thinker to an entity!
18
+ // The Thinker is the actual "brain" behind all the AI. Every entity you want
19
+ // to have AI behavior should have one *or more* Thinkers attached to it.
20
+ pub(crate) fn setup(mut cmd: Commands) {
21
+ // Create the entity and throw the Thirst component in there. Nothing special here.
22
+ // Neutral AI Brain
23
+ cmd.spawn((
24
+ Thirst::new(75.0, 1.6),
25
+ Thinker::build()
26
+ .label("NeutralAIBrain")
27
+ .picker(FirstToScore { threshold: 0.8 })
28
+ // Technically these are supposed to be ActionBuilders and
29
+ // ScorerBuilders, but our Clone impls simplify our code here.
30
+ .when(Thirsty, Drink {
31
+ until: 1.0,
32
+ per_second: 5.0,
33
+ }),
34
+ // ⚠️ TODO:
35
+ // When damaged by the player, become hostile.
36
+ ));
37
+
38
+ // AggressiveAIBrain
39
+ cmd.spawn((
40
+ Thirst::new(75.0, 1.6),
41
+ Thinker::build()
42
+ .label("AggressiveAIBrain")
43
+ .picker(FirstToScore { threshold: 0.8 })
44
+ .when(Thirsty, Drink {
45
+ until: 1.0,
46
+ per_second: 5.0,
47
+ }),
48
+ // ⚠️ TODO:
49
+ // When player is in line of sight and at the proper
50
+ // aggro radius, engage in combat.
51
+ ));
52
+ }
src/components/ai/scorers/mod.rs ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // These are special components that
2
+ // run in the background, calculating a "Score" value, which is what Big Brain
3
+ // will use to pick which Actions to execute.
4
+ //
5
+ // Just like with Actions, there is a distinction between Scorer components
6
+ // and the ScorerBuilder which will attach those components to the Actor entity.
7
+ //
8
+ // Again, in most cases, you can use the `ScorerBuilder` derive macro to make your
9
+ // Scorer Component act as a ScorerBuilder. You need it to implement Clone and Debug.
10
+
11
+ pub(crate) mod thirsty;
src/components/ai/scorers/thirsty.rs ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::prelude::*;
2
+ use big_brain::prelude::*;
3
+
4
+ use crate::components::ai::thirst::Thirst;
5
+
6
+ #[derive(Clone, Component, Debug, ScorerBuilder)]
7
+ pub(crate) struct Thirsty;
8
+
9
+ // Then, we have something called "Scorers".
10
+
11
+ // Looks familiar? It's a lot like Actions!
12
+ pub(crate) fn thirsty_scorer_system(
13
+ thirsts: Query<&Thirst>,
14
+ // Same dance with the Actor here, but now we use look up Score instead of ActionState.
15
+ mut query: Query<(&Actor, &mut Score, &ScorerSpan), With<Thirsty>>
16
+ ) {
17
+ for (Actor(actor), mut score, span) in &mut query {
18
+ if let Ok(thirst) = thirsts.get(*actor) {
19
+ // This is really what the job of a Scorer is. To calculate a
20
+ // generic "Utility" score that the Big Brain engine will compare
21
+ // against others, over time, and use to make decisions. This is
22
+ // generally "the higher the better", and "first across the finish
23
+ // line", but that's all configurable using Pickers!
24
+ //
25
+ // The score here must be between 0.0 and 1.0.
26
+ score.set(thirst.thirst / 100.0);
27
+ if thirst.thirst >= 80.0 {
28
+ span.span().in_scope(|| {
29
+ debug!("Thirst above threshold! Score: {}", thirst.thirst / 100.0)
30
+ });
31
+ }
32
+ }
33
+ }
34
+ }
src/components/ai/thirst.rs ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::prelude::*;
2
+
3
+ // First, we define a "Thirst" component and associated system. This is NOT
4
+ // THE AI. It's a plain old system that just makes an entity "thirstier" over
5
+ // time. This is what the AI will later interact with.
6
+ //
7
+ // There's nothing special here. It's a plain old Bevy component.
8
+ #[derive(Component, Debug, Reflect)]
9
+ pub(crate) struct Thirst {
10
+ pub per_second: f32,
11
+ pub thirst: f32,
12
+ }
13
+
14
+ impl Thirst {
15
+ pub fn new(thirst: f32, per_second: f32) -> Self {
16
+ Self { thirst, per_second }
17
+ }
18
+ }
19
+
20
+ pub(crate) fn thirst_system(time: Res<Time>, mut thirsts: Query<&mut Thirst>) {
21
+ for mut thirst in &mut thirsts {
22
+ thirst.thirst += thirst.per_second * ((time.delta().as_micros() as f32) / 1_000_000.0);
23
+ if thirst.thirst >= 100.0 {
24
+ debug!("Thirst >= {}", thirst.thirst);
25
+ thirst.thirst = 100.0;
26
+ }
27
+
28
+ trace!("Thirst: {}", thirst.thirst);
29
+ }
30
+ }
src/components/animals.rs ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::prelude::*;
2
+
3
+ // ⚠️ NOTE: Port to bevy_rand somehow.
4
+ use rand::seq::SliceRandom;
5
+ use rand::thread_rng;
6
+
7
+ // 🐾
8
+
9
+ // Animal types
10
+ #[derive(Component, PartialEq, Eq)]
11
+ pub enum AnimalType {
12
+ Cat,
13
+ Dog,
14
+ }
15
+
16
+ pub trait Animal: Component {
17
+ // A static method that returns a static string reference, representing the species of the animal
18
+ fn species() -> &'static str;
19
+
20
+ // A method that returns a reference to a String, representing the name of the animal
21
+ fn name(&self) -> &String;
22
+
23
+ // A method that returns an Option containing a static string reference, representing the gender of the animal
24
+ fn gender(&self) -> Option<&'static str> {
25
+ // Get the name of the animal
26
+ let name = self.name();
27
+
28
+ // Iterate over the ANIMAL_NAMES array
29
+ for &(animal_name, gender, _) in ANIMAL_NAMES {
30
+ // If the name of the animal matches the name in the array
31
+ if animal_name == name {
32
+ // Return the gender of the animal
33
+ return Some(gender);
34
+ }
35
+ }
36
+
37
+ // If no match is found, return None
38
+ None
39
+ }
40
+ }
41
+
42
+ // Function to generate an animal name based on the given animal type
43
+ fn generate_animal_name(animal_type: AnimalType) -> String {
44
+ // Create a random number generator
45
+ let mut rng = thread_rng();
46
+
47
+ // Choose a random animal name from the ANIMAL_NAMES array
48
+ // The chosen element is a tuple containing the name, gender, and type of the animal
49
+ match ANIMAL_NAMES.choose(&mut rng) {
50
+ Some((name, _gender, name_type)) => {
51
+ // If the type of the chosen animal matches the given animal type
52
+ if *name_type == animal_type {
53
+ // Return the name of the animal as a string
54
+ name.to_string()
55
+ } else {
56
+ // If the types don't match, recursively call the function until a matching animal name is found
57
+ generate_animal_name(animal_type)
58
+ }
59
+ }
60
+ None => {
61
+ // If the ANIMAL_NAMES array is empty, return a default name
62
+ "Default Animal Name".to_string()
63
+ }
64
+ }
65
+ }
66
+
67
+ #[rustfmt::skip]
68
+ pub const ANIMAL_NAMES: &[(&str, &str, AnimalType)] = &[
69
+ ("Malcolm", "male", AnimalType::Dog), ("Zoe", "female", AnimalType::Dog), ("Wash", "male", AnimalType::Dog),
70
+ ("Inara", "female", AnimalType::Dog), ("Jayne", "male", AnimalType::Dog), ("Kaylee", "female", AnimalType::Dog),
71
+ ("Simon", "male", AnimalType::Dog), ("River", "female", AnimalType::Dog), ("Book", "male", AnimalType::Dog),
72
+ ("Saffron", "female", AnimalType::Dog), ("Badger", "male", AnimalType::Dog), ("Nandi", "female", AnimalType::Dog),
73
+ ("Bester", "male", AnimalType::Dog), ("Dobson", "male", AnimalType::Dog), ("Atherton", "male", AnimalType::Dog),
74
+ ("Gabriel", "male", AnimalType::Dog), ("Regan", "female", AnimalType::Dog), ("Tracey", "male", AnimalType::Dog),
75
+ ("Amnon", "male", AnimalType::Dog), ("Fess", "male", AnimalType::Dog), ("Rance", "male", AnimalType::Dog),
76
+ ("Magistrate", "male", AnimalType::Dog), ("Lucy", "female", AnimalType::Dog), ("Ruth", "female", AnimalType::Dog),
77
+ ("Bree", "female", AnimalType::Dog), ("Jubal", "male", AnimalType::Dog), ("Fanty", "male", AnimalType::Dog),
78
+ ("Mingo", "male", AnimalType::Dog), ("Durin", "male", AnimalType::Dog), ("Bridget", "female", AnimalType::Dog),
79
+ ("Matty", "male", AnimalType::Dog), ("Ranse", "male", AnimalType::Dog), ("Heinrich", "male", AnimalType::Dog),
80
+ ("Lawrence", "male", AnimalType::Dog), ("Lund", "male", AnimalType::Dog), ("Monty", "male", AnimalType::Dog),
81
+ ("Corbin", "male", AnimalType::Dog), ("Petaline", "female", AnimalType::Dog), ("Helen", "female", AnimalType::Dog),
82
+ ("Fanti", "male", AnimalType::Dog), ("Kess", "female", AnimalType::Dog), ("Ransome", "male", AnimalType::Dog),
83
+ ("Sanda", "female", AnimalType::Dog), // End of 🐕
84
+ ("Picard", "male", AnimalType::Cat), ("Beverly", "female", AnimalType::Cat), ("Data", "male", AnimalType::Cat),
85
+ ("Troi", "female", AnimalType::Cat), ("Laforge", "male", AnimalType::Cat), ("Crusher", "male", AnimalType::Cat),
86
+ ("Yar", "female", AnimalType::Cat), ("Kirk", "male", AnimalType::Cat), ("Spock", "male", AnimalType::Cat),
87
+ ("Mccoy", "male", AnimalType::Cat), ("Scotty", "male", AnimalType::Cat), ("Uhura", "female", AnimalType::Cat),
88
+ ("Sulu", "male", AnimalType::Cat), ("Chekov", "male", AnimalType::Cat), ("Chakotay", "male", AnimalType::Cat),
89
+ ("Tuvok", "male", AnimalType::Cat), ("Sisko", "male", AnimalType::Cat), ("Kira", "female", AnimalType::Cat),
90
+ ("Dax", "female", AnimalType::Cat), ("Bashir", "male", AnimalType::Cat), ("Odo", "male", AnimalType::Cat),
91
+ ("Quark", "male", AnimalType::Cat), ("Archer", "male", AnimalType::Cat), ("Tucker", "male", AnimalType::Cat),
92
+ ("Tpol", "female", AnimalType::Cat), ("Reed", "male", AnimalType::Cat), ("Mayweather", "male", AnimalType::Cat),
93
+ ("Phlox", "male", AnimalType::Cat), ("Sato", "female", AnimalType::Cat), ("Sevenofnine", "female", AnimalType::Cat),
94
+ ("Doctor", "male", AnimalType::Cat), ("Paris", "male", AnimalType::Cat), ("Harrykim", "male", AnimalType::Cat),
95
+ ("Belanna", "female", AnimalType::Cat), ("Torres", "female", AnimalType::Cat), ("Jeanluc", "male", AnimalType::Cat),
96
+ ("Lorca", "male", AnimalType::Cat), ("Burnham", "female", AnimalType::Cat), ("Saru", "male", AnimalType::Cat),
97
+ ("Stamets", "male", AnimalType::Cat), ("Tilly", "female", AnimalType::Cat), ("Georgiou", "female", AnimalType::Cat),
98
+ ("Culber", "male", AnimalType::Cat), ("Cornwell", "female", AnimalType::Cat), ("Leland", "male", AnimalType::Cat),
99
+ ("Vance", "male", AnimalType::Cat), ("Reno", "female", AnimalType::Cat), ("Booker", "male", AnimalType::Cat),
100
+ ("Grudge", "female", AnimalType::Cat), ("Shaxs", "male", AnimalType::Cat), ("Detmer", "female", AnimalType::Cat),
101
+ ("Owosekun", "female", AnimalType::Cat), ("Rhys", "male", AnimalType::Cat), ("Pike", "male", AnimalType::Cat),
102
+ ("Number One", "male", AnimalType::Cat), ("Laan", "male", AnimalType::Cat), ("Chapel", "female", AnimalType::Cat),
103
+ ("Kyle", "male", AnimalType::Cat), ("Vina", "female", AnimalType::Cat), ("Mudd", "male", AnimalType::Cat),
104
+ ("Garak", "male", AnimalType::Cat), ("Leyton", "male", AnimalType::Cat), ("Ross", "male", AnimalType::Cat),
105
+ ("Nog", "male", AnimalType::Cat), ("Jake", "male", AnimalType::Cat), ("Seven", "female", AnimalType::Cat),
106
+ ("Janeway", "female", AnimalType::Cat), ("Tuvix", "male", AnimalType::Cat), ("Neelix", "male", AnimalType::Cat),
107
+ ("Kes", "female", AnimalType::Cat), ("Carey", "male", AnimalType::Cat), ("Vorik", "male", AnimalType::Cat),
108
+ ("Wildman", "female", AnimalType::Cat), ("Zahir", "male", AnimalType::Cat), ("Seska", "female", AnimalType::Cat),
109
+ ("Jonas", "male", AnimalType::Cat), ("Rio", "male", AnimalType::Cat), ("Maxwell", "male", AnimalType::Cat),
110
+ ("Tryla", "female", AnimalType::Cat), ("Lorian", "male", AnimalType::Cat), ("Icheb", "male", AnimalType::Cat),
111
+ ("Q", "male", AnimalType::Cat), ("Guinan", "female", AnimalType::Cat), ("Pulaski", "female", AnimalType::Cat),
112
+ ("Ro", "female", AnimalType::Cat), ("Hwomyn", "female", AnimalType::Cat), ("Riker", "male", AnimalType::Cat),
113
+ ("Shelby", "female", AnimalType::Cat), ("Obrien", "male", AnimalType::Cat), ("Keiko", "female", AnimalType::Cat),
114
+ ("Molly", "female", AnimalType::Cat), ("Kirayoshi", "male", AnimalType::Cat), ("Naomi", "female", AnimalType::Cat),
115
+ ("Ezri", "female", AnimalType::Cat), ("Kassidy", "female", AnimalType::Cat), ("Leeta", "female", AnimalType::Cat),
116
+ ("Nog", "male", AnimalType::Cat), ("Rom", "male", AnimalType::Cat), ("Brunt", "male", AnimalType::Cat),
117
+ ("Ishka", "female", AnimalType::Cat), ("Worf", "male", AnimalType::Cat), ("Martok", "male", AnimalType::Cat),
118
+ ("Grilka", "female", AnimalType::Cat), ("Sharan", "male", AnimalType::Cat), ("Alexander", "male", AnimalType::Cat),
119
+ ("Kehleyr", "female", AnimalType::Cat), ("Lwaxana", "female", AnimalType::Cat), ("Kamala", "female", AnimalType::Cat),
120
+ ("Vash", "female", AnimalType::Cat), ("Tasha", "female", AnimalType::Cat), ("Ogawa", "female", AnimalType::Cat),
121
+ ("Barclay", "male", AnimalType::Cat), ("Maddox", "male", AnimalType::Cat), ("Soong", "male", AnimalType::Cat),
122
+ ("Juliana", "female", AnimalType::Cat), ("Sela", "female", AnimalType::Cat), ("Toral", "male", AnimalType::Cat),
123
+ ("Ziyal", "female", AnimalType::Cat), ("Dukat", "male", AnimalType::Cat), ("Damar", "male", AnimalType::Cat),
124
+ ("Weyoun", "male", AnimalType::Cat), ("Eddington", "male", AnimalType::Cat), ("Michael", "male", AnimalType::Cat),
125
+ ("Sarina", "female", AnimalType::Cat), ("Hugh", "male", AnimalType::Cat), ("Lore", "male", AnimalType::Cat),
126
+ ("Elaurian", "male", AnimalType::Cat), // End of 🐈‍⬛
127
+ ];
src/components/animation.rs ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::prelude::*;
2
+
3
+ use super::health::Health;
4
+
5
+ // 🎞️
6
+ #[derive(Component, Clone)]
7
+ struct AnimationIndices {
8
+ first: usize, // The first index of the animation
9
+ last: usize, // The last index of the animation
10
+ current_index: usize, // The current index of the animation
11
+ }
12
+
13
+ #[derive(Component, Deref, DerefMut)]
14
+ struct AnimationTimer(Timer); // The timer for the animation
15
+
16
+ #[derive(Component)]
17
+ struct DeathAnimationPlayed(bool); // A boolean to track if the death animation has been played
18
+
19
+ // Function to play the death animation for entities with 0 health
20
+ fn play_death_animation(
21
+ mut query: Query<(&mut AnimationIndices, &Health, &mut DeathAnimationPlayed, &mut TextureAtlas)>
22
+ ) {
23
+ // Iterate over the entities
24
+ for (mut animation_indices, health, mut death_animation_played, mut atlas) in &mut query {
25
+ // If the health of the entity is 0 and the death animation has not been played
26
+ if health.current == 0 && !death_animation_played.0 {
27
+ // Set the indices for the death animation
28
+ animation_indices.first = 4;
29
+ animation_indices.last = 4;
30
+ animation_indices.current_index = 4;
31
+ // Update the TextureAtlas index
32
+ atlas.index = animation_indices.current_index;
33
+ // Mark the death animation as played
34
+ death_animation_played.0 = true;
35
+ }
36
+ }
37
+ }
38
+
39
+ // Function to animate the sprite of entities of type T
40
+ fn animate_sprite<T: Component>(
41
+ time: Res<Time>, // The current time
42
+ mut query: Query<(&mut AnimationIndices, &mut AnimationTimer, &mut TextureAtlas), With<T>>
43
+ ) {
44
+ // Iterate over the entities
45
+ for (mut indices, mut timer, mut atlas) in &mut query {
46
+ // Update the animation timer
47
+ timer.tick(time.delta());
48
+ // If the animation timer has just finished
49
+ if timer.just_finished() {
50
+ // Update the current index of the animation
51
+ indices.current_index = if indices.current_index == indices.last {
52
+ if indices.first == 4 {
53
+ // Loop back to the first frame of the death animation
54
+ 4
55
+ } else {
56
+ // Loop back to the first frame of the animation
57
+ indices.first
58
+ }
59
+ } else {
60
+ // Move to the next frame of the animation
61
+ indices.current_index + 1
62
+ };
63
+ // Update the TextureAtlas index
64
+ atlas.index = indices.current_index;
65
+ }
66
+ }
67
+ }
src/components/armor.rs ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ use bevy::prelude::*;
2
+
3
+ #[derive(Component, Clone, Default)]
4
+ pub struct Armor {
5
+ pub value: u32, // The armor value of the entity
6
+ }
src/components/camera/fit_inside_current_level.rs ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::prelude::*;
2
+ use bevy_ecs_ldtk::{
3
+ assets::{ LdtkProject, LevelIndices, LevelMetadataAccessor },
4
+ LevelIid,
5
+ LevelSelection,
6
+ };
7
+
8
+ use crate::entities::Player;
9
+
10
+ const ASPECT_RATIO: f32 = 16.0 / 9.0;
11
+
12
+ pub fn fit_inside_current_level(
13
+ mut camera_query: Query<
14
+ (&mut bevy::render::camera::OrthographicProjection, &mut Transform),
15
+ Without<Player>
16
+ >,
17
+ player_query: Query<&Transform, With<Player>>,
18
+ level_query: Query<(&Transform, &LevelIid), (Without<OrthographicProjection>, Without<Player>)>,
19
+ ldtk_projects: Query<&Handle<LdtkProject>>,
20
+ level_selection: Res<LevelSelection>,
21
+ ldtk_project_assets: Res<Assets<LdtkProject>>
22
+ ) {
23
+ if let Ok(Transform { translation: player_translation, .. }) = player_query.get_single() {
24
+ let player_translation = *player_translation;
25
+
26
+ let (mut orthographic_projection, mut camera_transform) = camera_query.single_mut();
27
+
28
+ for (level_transform, level_iid) in &level_query {
29
+ let ldtk_project = ldtk_project_assets
30
+ .get(ldtk_projects.single())
31
+ .expect("Project should be loaded if level has spawned");
32
+
33
+ let level = ldtk_project
34
+ .get_raw_level_by_iid(&level_iid.to_string())
35
+ .expect("Spawned level should exist in LDtk project");
36
+
37
+ if level_selection.is_match(&LevelIndices::default(), level) {
38
+ let level_ratio = (level.px_wid as f32) / (level.px_hei as f32);
39
+ orthographic_projection.viewport_origin = Vec2::ZERO;
40
+
41
+ if level_ratio > ASPECT_RATIO {
42
+ // level is wider than the screen
43
+ let height = ((level.px_hei as f32) / 9.0).round() * 9.0;
44
+ let width = height * ASPECT_RATIO;
45
+ orthographic_projection.scaling_mode =
46
+ bevy::render::camera::ScalingMode::Fixed { width, height };
47
+ camera_transform.translation.x = (
48
+ player_translation.x -
49
+ level_transform.translation.x -
50
+ width / 2.0
51
+ ).clamp(0.0, (level.px_wid as f32) - width);
52
+ camera_transform.translation.y = 0.0;
53
+ } else {
54
+ // level is taller than the screen
55
+ let width = ((level.px_wid as f32) / 16.0).round() * 16.0;
56
+ let height = width / ASPECT_RATIO;
57
+ orthographic_projection.scaling_mode =
58
+ bevy::render::camera::ScalingMode::Fixed { width, height };
59
+ camera_transform.translation.y = (
60
+ player_translation.y -
61
+ level_transform.translation.y -
62
+ height / 2.0
63
+ ).clamp(0.0, (level.px_hei as f32) - height);
64
+ camera_transform.translation.x = 0.0;
65
+ }
66
+
67
+ camera_transform.translation.x += level_transform.translation.x;
68
+ camera_transform.translation.y += level_transform.translation.y;
69
+ }
70
+ }
71
+ }
72
+ }
src/components/camera/mod.rs ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ //! 🎥
2
+ //!
3
+ //! > NOTE: Remember to keep camera updates to PostUpdate!
4
+
5
+ pub(crate) mod fit_inside_current_level;
src/components/childof.rs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::prelude::*;
2
+ use aery::prelude::*;
3
+
4
+ use super::name::Name;
5
+
6
+ #[derive(Relation)]
7
+ pub struct ChildOf;
8
+
9
+ pub fn debug_children(
10
+ tree: Query<(&Name, Relations<ChildOf>)>,
11
+ roots: Query<Entity, Root<ChildOf>>
12
+ ) {
13
+ tree.traverse::<ChildOf>(roots.iter())
14
+ .track_self()
15
+ .for_each(|Name(parent), _, Name(child), _| {
16
+ debug!("{} is the parent of {}", parent, child);
17
+ });
18
+ }
src/components/climbing.rs ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::collections::HashSet;
2
+ use bevy::prelude::*;
3
+ use bevy_rapier2d::{ dynamics::GravityScale, pipeline::CollisionEvent };
4
+
5
+ use crate::plugins::rapier_utils::reciprocal_collisions;
6
+
7
+ // Attach this to any component to allow the player (or any climber entity) to climb up and
8
+ // down on it.
9
+ #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)]
10
+ pub struct Climbable;
11
+
12
+ // Attach this component to any entity to allow them to climb up ladders.
13
+ #[derive(Clone, Eq, PartialEq, Debug, Default, Component)]
14
+ pub struct Climber {
15
+ pub climbing: bool,
16
+ pub intersecting_climbables: HashSet<Entity>,
17
+ }
18
+
19
+ // Checks for collision events between climbers and climbable entities.
20
+ // If a collision starts, the climbable entity is added to the climber’s set of intersecting climbables.
21
+ // If a collision stops, the climbable entity is removed from the set.
22
+ pub fn detect_climb_range(
23
+ mut climbers: Query<&mut Climber>,
24
+ climbables: Query<Entity, With<Climbable>>,
25
+ mut collisions: EventReader<CollisionEvent>
26
+ ) {
27
+ reciprocal_collisions(&mut collisions, move |collider_a, collider_b, _, start| {
28
+ if
29
+ let (Ok(mut climber), Ok(climbable)) = (
30
+ climbers.get_mut(*collider_a),
31
+ climbables.get(*collider_b),
32
+ )
33
+ {
34
+ if start {
35
+ climber.intersecting_climbables.insert(climbable);
36
+ } else {
37
+ climber.intersecting_climbables.remove(&climbable);
38
+ }
39
+ true
40
+ } else {
41
+ false
42
+ }
43
+ });
44
+ }
45
+
46
+ // Checks if a climber entity is climbing.
47
+ // If it is, the gravity scale is set to 0.0, effectively ignoring gravity.
48
+ // If the climber is not climbing, the gravity scale is set back to 1.0.
49
+ pub fn ignore_gravity_if_climbing(
50
+ mut query: Query<(&Climber, &mut GravityScale), Changed<Climber>>
51
+ ) {
52
+ for (climber, mut gravity_scale) in &mut query {
53
+ if climber.climbing {
54
+ gravity_scale.0 = 0.0;
55
+ } else {
56
+ gravity_scale.0 = 1.0;
57
+ }
58
+ }
59
+ }
src/components/collision.rs ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::{ prelude::*, utils::HashMap, utils::HashSet, ecs::system::EntityCommands };
2
+ use bevy_ecs_ldtk::{
3
+ assets::LdtkProject,
4
+ ldtk::{ loaded_level::LoadedLevel, LayerInstance },
5
+ EntityInstance,
6
+ GridCoords,
7
+ LdtkIntCell,
8
+ LevelIid,
9
+ };
10
+ use bevy_rapier2d::{
11
+ dynamics::{ CoefficientCombineRule, GravityScale, LockedAxes, RigidBody, Velocity },
12
+ geometry::{ Collider, ColliderMassProperties, CollisionGroups, Friction, Group },
13
+ };
14
+
15
+ use crate::entities::intcells::Wall;
16
+
17
+ /// Spawns heron collisions for the walls that have just been spawned
18
+ ///
19
+ /// Lookup the levels corresponding to the walls that have been spawned, and
20
+ /// associate to them the GridCoords of the walls.
21
+ ///
22
+ /// See [`spawn_wall_collision_for_level`] for the actual collider generation
23
+ /// algorithm.
24
+ pub fn spawn_wall_collision(
25
+ mut commands: Commands,
26
+ wall_query: Query<(&GridCoords, &Parent), Added<Wall>>,
27
+ parent_query: Query<&Parent, Without<Wall>>,
28
+ level_query: Query<(Entity, &LevelIid)>,
29
+ ldtk_projects: Query<&Handle<LdtkProject>>,
30
+ ldtk_project_assets: Res<Assets<LdtkProject>>
31
+ ) {
32
+ if wall_query.is_empty() {
33
+ return;
34
+ }
35
+
36
+ let ldtk_project = ldtk_project_assets
37
+ .get(ldtk_projects.single())
38
+ .expect("Project should be loaded if level has spawned");
39
+
40
+ // Consider where the walls are
41
+ // storing them as GridCoords in a HashSet for quick, easy lookup
42
+ //
43
+ // The key of this map will be the entity of the level the wall belongs to.
44
+ // This has two consequences in the resulting collision entities:
45
+ // 1. it forces the walls to be split along level boundaries
46
+ // 2. it lets us easily add the collision entities as children of the appropriate level entity
47
+ let mut level_to_wall_locations: HashMap<Entity, HashSet<GridCoords>> = HashMap::new();
48
+
49
+ wall_query.iter().for_each(|(&grid_coords, parent)| {
50
+ // An intgrid tile's direct parent will be a layer entity, not the level entity
51
+ // To get the level entity, you need the tile's grandparent.
52
+ // This is where parent_query comes in.
53
+ if let Ok(grandparent) = parent_query.get(parent.get()) {
54
+ level_to_wall_locations.entry(grandparent.get()).or_default().insert(grid_coords);
55
+ }
56
+ });
57
+
58
+ level_query.iter().for_each(|(level_entity, level_iid)| {
59
+ if let Some(level_walls) = level_to_wall_locations.get(&level_entity) {
60
+ let level = ldtk_project
61
+ .as_standalone()
62
+ .get_loaded_level_by_iid(&level_iid.to_string())
63
+ .expect("Spawned level should exist in LDtk project");
64
+
65
+ spawn_wall_collision_for_level(level, level_walls, commands.entity(level_entity));
66
+ }
67
+ });
68
+ }
69
+
70
+ /// Spawns heron collisions for the walls of a level
71
+ ///
72
+ /// You could just insert a ColliderBundle in to the WallBundle,
73
+ /// but this spawns a different collider for EVERY wall tile.
74
+ /// This approach leads to bad performance.
75
+ ///
76
+ /// Instead, by flagging the wall tiles and spawning the collisions later,
77
+ /// we can minimize the amount of colliding entities.
78
+ ///
79
+ /// The algorithm used here is a nice compromise between simplicity, speed,
80
+ /// and a small number of rectangle colliders.
81
+ /// In basic terms, it will:
82
+ /// 1. consider where the walls are
83
+ /// 2. combine wall tiles into flat "plates" in each individual row
84
+ /// 3. combine the plates into rectangles across multiple rows wherever possible
85
+ /// 4. spawn colliders for each rectangle
86
+ fn spawn_wall_collision_for_level(
87
+ level: LoadedLevel,
88
+ level_walls: &bevy::utils::hashbrown::HashSet<GridCoords>,
89
+ mut entity_commands: EntityCommands
90
+ ) {
91
+ /// Represents a wide wall that is 1 tile tall
92
+ /// Used to spawn wall collisions
93
+ #[derive(Clone, Eq, PartialEq, Debug, Default, Hash)]
94
+ struct Plate {
95
+ left: i32,
96
+ right: i32,
97
+ }
98
+
99
+ /// A simple rectangle type representing a wall of any size
100
+ struct Rect {
101
+ left: i32,
102
+ right: i32,
103
+ top: i32,
104
+ bottom: i32,
105
+ }
106
+
107
+ let LayerInstance {
108
+ c_wid: width,
109
+ c_hei: height,
110
+ grid_size,
111
+ ..
112
+ } = *level
113
+ .layer_instances()
114
+ .iter()
115
+ .filter(|level| level.identifier == "Collisions")
116
+ .next()
117
+ .expect("could not find the Collisions layer");
118
+
119
+ // combine wall tiles into flat "plates" in each individual row
120
+ let mut plate_stack: Vec<Vec<Plate>> = Vec::new();
121
+
122
+ for y in 0..height {
123
+ let mut row_plates: Vec<Plate> = Vec::new();
124
+ let mut plate_start = None;
125
+
126
+ // + 1 to the width so the algorithm "terminates" plates that touch the right edge
127
+ for x in 0..width + 1 {
128
+ match (plate_start, level_walls.contains(&(GridCoords { x, y }))) {
129
+ (Some(s), false) => {
130
+ row_plates.push(Plate {
131
+ left: s,
132
+ right: x - 1,
133
+ });
134
+ plate_start = None;
135
+ }
136
+ (None, true) => {
137
+ plate_start = Some(x);
138
+ }
139
+ _ => (),
140
+ }
141
+ }
142
+
143
+ plate_stack.push(row_plates);
144
+ }
145
+
146
+ // combine "plates" into rectangles across multiple rows
147
+ let mut rect_builder: HashMap<Plate, Rect> = HashMap::new();
148
+ let mut prev_row: Vec<Plate> = Vec::new();
149
+ let mut wall_rects: Vec<Rect> = Vec::new();
150
+
151
+ // an extra empty row so the algorithm "finishes" the rects that touch the top edge
152
+ plate_stack.push(Vec::new());
153
+
154
+ for (y, current_row) in plate_stack.into_iter().enumerate() {
155
+ for prev_plate in &prev_row {
156
+ if !current_row.contains(prev_plate) {
157
+ // remove the finished rect so that the same plate in the future starts a new rect
158
+ if let Some(rect) = rect_builder.remove(prev_plate) {
159
+ wall_rects.push(rect);
160
+ }
161
+ }
162
+ }
163
+ for plate in &current_row {
164
+ rect_builder
165
+ .entry(plate.clone())
166
+ .and_modify(|e| {
167
+ e.top += 1;
168
+ })
169
+ .or_insert(Rect {
170
+ bottom: y as i32,
171
+ top: y as i32,
172
+ left: plate.left,
173
+ right: plate.right,
174
+ });
175
+ }
176
+ prev_row = current_row;
177
+ }
178
+
179
+ entity_commands.with_children(|level| {
180
+ // Spawn colliders for every rectangle..
181
+ // Making the collider a child of the level serves two purposes:
182
+ // 1. Adjusts the transforms to be relative to the level for free
183
+ // 2. the colliders will be despawned automatically when levels unload
184
+ for wall_rect in wall_rects {
185
+ level
186
+ .spawn_empty()
187
+ .insert(Name::new("wall_collision"))
188
+ .insert(
189
+ Collider::cuboid(
190
+ (((wall_rect.right as f32) - (wall_rect.left as f32) + 1.0) *
191
+ (grid_size as f32)) /
192
+ 2.0,
193
+ (((wall_rect.top as f32) - (wall_rect.bottom as f32) + 1.0) *
194
+ (grid_size as f32)) /
195
+ 2.0
196
+ )
197
+ )
198
+ .insert(RigidBody::Fixed)
199
+ .insert(Friction::new(1.0))
200
+ .insert(
201
+ Transform::from_xyz(
202
+ (((wall_rect.left + wall_rect.right + 1) as f32) * (grid_size as f32)) /
203
+ 2.0,
204
+ (((wall_rect.bottom + wall_rect.top + 1) as f32) * (grid_size as f32)) /
205
+ 2.0,
206
+ 0.0
207
+ )
208
+ )
209
+ .insert(GlobalTransform::default());
210
+ }
211
+ });
212
+ }
213
+
214
+ #[derive(Clone, Default, Bundle, LdtkIntCell)]
215
+ pub struct ColliderBundle {
216
+ pub collider: Collider,
217
+ pub rigid_body: RigidBody,
218
+ pub velocity: Velocity,
219
+ pub rotation_constraints: LockedAxes,
220
+ pub gravity_scale: GravityScale,
221
+ pub friction: Friction,
222
+ pub density: ColliderMassProperties,
223
+ pub collision_group: CollisionGroups,
224
+ }
225
+
226
+ const PLAYER_GROUP: Group = Group::GROUP_1;
227
+ const NPC_GROUP: Group = Group::GROUP_2;
228
+
229
+ impl From<&EntityInstance> for ColliderBundle {
230
+ fn from(entity_instance: &EntityInstance) -> ColliderBundle {
231
+ let rotation_constraints = LockedAxes::ROTATION_LOCKED;
232
+
233
+ match entity_instance.identifier.as_ref() {
234
+ "Player" =>
235
+ ColliderBundle {
236
+ collider: Collider::cuboid(8.0, 12.0),
237
+ friction: Friction {
238
+ coefficient: 0.0,
239
+ combine_rule: CoefficientCombineRule::Min,
240
+ },
241
+ rotation_constraints,
242
+ collision_group: CollisionGroups::new(PLAYER_GROUP, Group::ALL - NPC_GROUP),
243
+ ..Default::default()
244
+ },
245
+ "Enemy" =>
246
+ ColliderBundle {
247
+ collider: Collider::cuboid(12.0, 12.0),
248
+ rigid_body: RigidBody::KinematicVelocityBased,
249
+ rotation_constraints,
250
+ ..Default::default()
251
+ },
252
+ "Npc" =>
253
+ ColliderBundle {
254
+ collider: Collider::cuboid(12.0, 12.0),
255
+ rigid_body: RigidBody::KinematicVelocityBased,
256
+ rotation_constraints,
257
+ density: ColliderMassProperties::Density(50.0),
258
+ collision_group: CollisionGroups::new(NPC_GROUP, Group::ALL - NPC_GROUP),
259
+ ..Default::default()
260
+ },
261
+ "NpcPatrol" =>
262
+ ColliderBundle {
263
+ collider: Collider::cuboid(12.0, 12.0),
264
+ rigid_body: RigidBody::KinematicVelocityBased,
265
+ rotation_constraints,
266
+ density: ColliderMassProperties::Density(50.0),
267
+ collision_group: CollisionGroups::new(NPC_GROUP, Group::ALL),
268
+ ..Default::default()
269
+ },
270
+ "Cauldron" =>
271
+ ColliderBundle {
272
+ collider: Collider::cuboid(12.0, 12.0),
273
+ rigid_body: RigidBody::KinematicVelocityBased,
274
+ rotation_constraints,
275
+ density: ColliderMassProperties::Density(50.0),
276
+ collision_group: CollisionGroups::new(NPC_GROUP, Group::ALL),
277
+ ..Default::default()
278
+ },
279
+ "Kade" =>
280
+ ColliderBundle {
281
+ collider: Collider::cuboid(12.0, 12.0),
282
+ rigid_body: RigidBody::KinematicVelocityBased,
283
+ rotation_constraints,
284
+ density: ColliderMassProperties::Density(50.0),
285
+ collision_group: CollisionGroups::new(NPC_GROUP, Group::ALL),
286
+ ..Default::default()
287
+ },
288
+ "Dog" =>
289
+ ColliderBundle {
290
+ collider: Collider::cuboid(12.0, 12.0),
291
+ rigid_body: RigidBody::KinematicVelocityBased,
292
+ rotation_constraints,
293
+ density: ColliderMassProperties::Density(50.0),
294
+ collision_group: CollisionGroups::new(NPC_GROUP, Group::ALL),
295
+ ..Default::default()
296
+ },
297
+ "DogPatrol" =>
298
+ ColliderBundle {
299
+ collider: Collider::cuboid(12.0, 12.0),
300
+ rigid_body: RigidBody::KinematicVelocityBased,
301
+ rotation_constraints,
302
+ density: ColliderMassProperties::Density(50.0),
303
+ collision_group: CollisionGroups::new(NPC_GROUP, Group::ALL),
304
+ ..Default::default()
305
+ },
306
+ "Cat" =>
307
+ ColliderBundle {
308
+ collider: Collider::cuboid(12.0, 12.0),
309
+ rigid_body: RigidBody::KinematicVelocityBased,
310
+ rotation_constraints,
311
+ density: ColliderMassProperties::Density(50.0),
312
+ collision_group: CollisionGroups::new(NPC_GROUP, Group::ALL),
313
+ ..Default::default()
314
+ },
315
+ "CatPatrol" =>
316
+ ColliderBundle {
317
+ collider: Collider::cuboid(12.0, 12.0),
318
+ rigid_body: RigidBody::KinematicVelocityBased,
319
+ rotation_constraints,
320
+ density: ColliderMassProperties::Density(50.0),
321
+ collision_group: CollisionGroups::new(NPC_GROUP, Group::ALL),
322
+ ..Default::default()
323
+ },
324
+ "Chest" =>
325
+ ColliderBundle {
326
+ collider: Collider::cuboid(8.0, 8.0),
327
+ rotation_constraints,
328
+ density: ColliderMassProperties::Density(50.0),
329
+ ..default()
330
+ },
331
+ _ => {
332
+ debug!("default ColliderBundle used for: {}", entity_instance.identifier);
333
+ ColliderBundle {
334
+ collider: Collider::cuboid(
335
+ (entity_instance.width as f32) / 2.0,
336
+ (entity_instance.height as f32) / 2.0
337
+ ),
338
+ rotation_constraints,
339
+ ..default()
340
+ }
341
+ }
342
+ }
343
+ }
344
+ }
src/components/deathzone.rs ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ use bevy::prelude::*;
2
+
3
+ // 💀 Zone
4
+ #[derive(Component)]
5
+ pub struct DeathZone;
src/components/ground.rs ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::{ prelude::*, utils::HashSet };
2
+ use bevy_rapier2d::{ geometry::{ ActiveEvents, Collider, Sensor }, pipeline::CollisionEvent };
3
+
4
+ use crate::plugins::rapier_utils::reciprocal_collisions;
5
+
6
+ #[derive(Clone, Default, Component)]
7
+ pub struct GroundDetection {
8
+ pub on_ground: bool,
9
+ }
10
+
11
+ #[derive(Component)]
12
+ pub struct GroundSensor {
13
+ pub ground_detection_entity: Entity,
14
+ pub intersecting_ground_entities: HashSet<Entity>,
15
+ }
16
+
17
+ pub fn spawn_ground_sensor(
18
+ mut commands: Commands,
19
+ detect_ground_for: Query<(Entity, &Collider), Added<GroundDetection>>
20
+ ) {
21
+ for (entity, shape) in &detect_ground_for {
22
+ if let Some(cuboid) = shape.as_cuboid() {
23
+ let Vec2 { x: half_extents_x, y: half_extents_y } = cuboid.half_extents();
24
+
25
+ let detector_shape = Collider::cuboid(half_extents_x / 2.0, 2.0);
26
+
27
+ let sensor_translation = Vec3::new(0.0, -half_extents_y, 0.0);
28
+
29
+ commands.entity(entity).with_children(|builder| {
30
+ builder
31
+ .spawn_empty()
32
+ .insert(Name::new("ground_sensor"))
33
+ .insert(ActiveEvents::COLLISION_EVENTS)
34
+ .insert(detector_shape)
35
+ .insert(Sensor)
36
+ .insert(Transform::from_translation(sensor_translation))
37
+ .insert(GlobalTransform::default())
38
+ .insert(GroundSensor {
39
+ ground_detection_entity: entity,
40
+ intersecting_ground_entities: HashSet::new(),
41
+ });
42
+ });
43
+ }
44
+ }
45
+ }
46
+
47
+ pub fn ground_detection(
48
+ mut ground_sensors: Query<&mut GroundSensor>,
49
+ mut collisions: EventReader<CollisionEvent>,
50
+ collidables: Query<Entity, (With<Collider>, Without<Sensor>)>
51
+ ) {
52
+ reciprocal_collisions(&mut collisions, move |e1, e2, _, start| {
53
+ if let (true, Ok(mut sensor)) = (collidables.contains(*e1), ground_sensors.get_mut(*e2)) {
54
+ if start {
55
+ sensor.intersecting_ground_entities.insert(*e1);
56
+ } else {
57
+ sensor.intersecting_ground_entities.remove(e1);
58
+ }
59
+ true
60
+ } else {
61
+ false
62
+ }
63
+ });
64
+ }
65
+
66
+ pub fn update_on_ground(
67
+ mut ground_detectors: Query<&mut GroundDetection>,
68
+ ground_sensors: Query<&GroundSensor, Changed<GroundSensor>>
69
+ ) {
70
+ for sensor in &ground_sensors {
71
+ if let Ok(mut ground_detection) = ground_detectors.get_mut(sensor.ground_detection_entity) {
72
+ ground_detection.on_ground = !sensor.intersecting_ground_entities.is_empty();
73
+ }
74
+ }
75
+ }
src/components/health.rs ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::{ ecs::component::Component, reflect::Reflect };
2
+
3
+ use super::armor::Armor;
4
+
5
+ // 🩸
6
+ #[derive(Component, Clone, Reflect)]
7
+ pub struct Health {
8
+ pub current: u32,
9
+ pub max: u32,
10
+ pub hunger: u32,
11
+ }
12
+
13
+ impl Default for Health {
14
+ fn default() -> Self {
15
+ Self {
16
+ current: 100,
17
+ max: 100,
18
+ hunger: 0,
19
+ }
20
+ }
21
+ }
22
+
23
+ impl Health {
24
+ pub fn take_damage(&mut self, mut damage: u32, armor: Option<&Armor>) {
25
+ if let Some(armor) = armor {
26
+ let reduction = ((armor.value as f32) / 100.0) * (damage as f32);
27
+ damage -= reduction.ceil() as u32;
28
+ }
29
+
30
+ self.current = self.current.saturating_sub(damage);
31
+ }
32
+ }
src/components/hunger.rs ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::time::Duration;
2
+
3
+ use bevy::{ ecs::system::{ Query, Res, ResMut, Resource }, time::{ Time, Timer } };
4
+
5
+ use super::health::Health;
6
+
7
+ // 🍗
8
+ #[derive(Resource, Default)]
9
+ pub struct HungerTimer(Timer); // A timer to track the hunger of entities
10
+
11
+ // Function to decrease the hunger of entities over time
12
+ pub fn decrease_hunger(
13
+ time: Res<Time>, // The current time
14
+ mut hunger_timer: ResMut<HungerTimer>, // The hunger timer
15
+ mut health_query: Query<&mut Health> // The health of the entities
16
+ ) {
17
+ // Update the hunger timer
18
+ hunger_timer.0.tick(time.delta());
19
+ // If the hunger timer has just finished
20
+ if hunger_timer.0.just_finished() {
21
+ // Iterate over the entities
22
+ for mut health in &mut health_query {
23
+ // Decrease the hunger of the entity by 1
24
+ health.hunger = health.hunger.saturating_sub(1);
25
+ // If the hunger of the entity reaches 0, decrease its health by 1
26
+ if health.hunger == 0 {
27
+ health.current = health.current.saturating_sub(1);
28
+ }
29
+ }
30
+ // Set the duration of the hunger timer to 20 seconds
31
+ hunger_timer.0.set_duration(Duration::from_secs(20));
32
+ // Reset the hunger timer to count down again
33
+ hunger_timer.0.reset();
34
+ }
35
+ }
src/components/interactions.rs ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::collections::HashSet;
2
+
3
+ use bevy::{
4
+ core::Name,
5
+ render::color::Color,
6
+ ecs::{
7
+ component::Component,
8
+ entity::Entity,
9
+ event::EventReader,
10
+ query::{ Added, With },
11
+ system::{ Commands, Query },
12
+ },
13
+ hierarchy::{ Parent, PushChild },
14
+ log,
15
+ math::Vec2,
16
+ transform::components::GlobalTransform,
17
+ };
18
+ use bevy_ecs_ldtk::{ ldtk::ldtk_fields::LdtkFields, EntityInstance };
19
+ use bevy_rapier2d::{ geometry::{ ActiveEvents, Collider, Sensor }, pipeline::CollisionEvent };
20
+
21
+ use crate::entities::Player;
22
+ use crate::plugins::rapier_utils::reciprocal_collisions;
23
+
24
+ #[derive(Component, Default)]
25
+ pub struct InteractionSensor {
26
+ pub intersecting_entities: HashSet<Entity>,
27
+ pub closest_entity: Option<Entity>,
28
+ }
29
+
30
+ /// Spawn a sensing region around the Player.
31
+ ///
32
+ /// The sensor is spawn as a children to the Player. It contains an
33
+ /// [`InteractionSensor`] component that tracks interactive entities in range
34
+ /// and the closest one.
35
+ pub(crate) fn spawn_interaction_sensor(
36
+ mut commands: Commands,
37
+ mut detect_interaction_for: Query<(Entity, &Collider), Added<Player>>
38
+ ) {
39
+ for (parent, shape) in &mut detect_interaction_for {
40
+ if let Some(cuboid) = shape.as_cuboid() {
41
+ let Vec2 { x: half_extents_x, y: half_extents_y } = cuboid.half_extents();
42
+ let mut sensor_cmds = commands.spawn((
43
+ ActiveEvents::COLLISION_EVENTS,
44
+ Collider::cuboid(half_extents_x * 4.0, half_extents_y * 1.5),
45
+ Sensor,
46
+ InteractionSensor::default(),
47
+ ));
48
+ #[cfg(feature = "dev_features")]
49
+ sensor_cmds.insert((
50
+ Name::new("interaction_sensor"),
51
+ bevy_rapier2d::render::ColliderDebugColor(Color::rgb(0.0, 1.0, 0.0)),
52
+ ));
53
+ let child = sensor_cmds.id();
54
+ commands.add(PushChild { parent, child });
55
+ }
56
+ }
57
+ }
58
+
59
+ #[derive(Component)]
60
+ pub struct Interactive {
61
+ pub name: String,
62
+ }
63
+
64
+ /// Adds the [`Interactive`] component to LDtk entities that have a name and the
65
+ /// `hasDialogue` field set to true
66
+ pub(crate) fn setup_interactive_entity(
67
+ mut commands: Commands,
68
+ query: Query<(Entity, &EntityInstance), Added<EntityInstance>>
69
+ ) {
70
+ for (entity, ldtk_entity) in query.iter() {
71
+ if
72
+ let (Ok(name), Ok(true)) = (
73
+ ldtk_entity.get_string_field("name"),
74
+ ldtk_entity.get_bool_field("hasDialogue"),
75
+ )
76
+ {
77
+ log::debug!("New interactive {}: {}", ldtk_entity.identifier, name);
78
+ commands.entity(entity).insert(Interactive { name: name.into() });
79
+ }
80
+ }
81
+ }
82
+
83
+ /// System collecting collision events of the interaction sensor with
84
+ /// interactive entities
85
+ pub(crate) fn interaction_detection(
86
+ mut interaction_sensors: Query<&mut InteractionSensor>,
87
+ interactive_entities: Query<Entity, With<Interactive>>,
88
+ mut collisions: EventReader<CollisionEvent>
89
+ ) {
90
+ reciprocal_collisions(&mut collisions, move |interactor_entity, interactive_entity, _, start| {
91
+ if
92
+ let (Ok(mut interactor), true) = (
93
+ interaction_sensors.get_mut(*interactor_entity),
94
+ interactive_entities.contains(*interactive_entity),
95
+ )
96
+ {
97
+ let set = &mut interactor.intersecting_entities;
98
+ if start {
99
+ set.insert(*interactive_entity);
100
+ } else {
101
+ set.remove(interactive_entity);
102
+ }
103
+ true
104
+ } else {
105
+ false
106
+ }
107
+ });
108
+ }
109
+
110
+ /// System that tracks distances between interactive entities and the sensor, in
111
+ /// order to elect the closest interactive entity.
112
+ pub(crate) fn update_interactions(
113
+ mut interaction_sensors: Query<(&mut InteractionSensor, &Parent)>,
114
+ player_query: Query<&GlobalTransform, With<Player>>,
115
+ interactive: Query<(&GlobalTransform, &Collider), With<Interactive>>
116
+ ) {
117
+ for (mut sensor, parent) in &mut interaction_sensors {
118
+ // Bypass the player transform query if the is no interactive entities
119
+ // in range
120
+ if sensor.intersecting_entities.is_empty() {
121
+ if sensor.closest_entity != None {
122
+ sensor.closest_entity = None;
123
+ }
124
+ continue;
125
+ }
126
+ let player_transform = player_query.get(**parent).unwrap();
127
+ // Find the closest entity.
128
+ let mut closest_dist = f32::INFINITY;
129
+ let mut closest_entity = None;
130
+ for interactive_entity in &sensor.intersecting_entities {
131
+ let Ok((interactive_transform, interactive_collider)) = interactive.get(
132
+ *interactive_entity
133
+ ) else {
134
+ continue;
135
+ };
136
+
137
+ let distance = interactive_collider.distance_to_local_point(
138
+ player_transform.reparented_to(interactive_transform).translation.truncate(),
139
+ false
140
+ );
141
+ if distance < closest_dist {
142
+ closest_dist = distance;
143
+ closest_entity = Some(*interactive_entity);
144
+ }
145
+ }
146
+ // Gate the mutation such that only real change are detected when using
147
+ // `Changed<InteractionSensor>` (See `Mut<>` mutation detection)
148
+ if sensor.closest_entity != closest_entity {
149
+ sensor.closest_entity = closest_entity;
150
+ }
151
+ }
152
+ }
src/components/items.rs ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::{
2
+ input::{ ButtonInput, keyboard::KeyCode },
3
+ ecs::{ component::Component, system::Res, system::Query, query::With },
4
+ };
5
+ use bevy_ecs_ldtk::{ ldtk::ldtk_fields::LdtkFields, EntityInstance };
6
+
7
+ use crate::entities::Player;
8
+
9
+ #[derive(Clone, Component, Debug, Eq, Default, PartialEq)]
10
+ pub struct Items(Vec<String>);
11
+
12
+ impl From<&EntityInstance> for Items {
13
+ fn from(entity_instance: &EntityInstance) -> Self {
14
+ Items(
15
+ entity_instance
16
+ .iter_enums_field("items")
17
+ .expect("items field should be correctly typed")
18
+ .cloned()
19
+ .collect()
20
+ )
21
+ }
22
+ }
23
+
24
+ pub fn dbg_player_items(
25
+ input: Res<ButtonInput<KeyCode>>,
26
+ mut query: Query<(&Items, &EntityInstance), With<Player>>
27
+ ) {
28
+ for (items, entity_instance) in &mut query {
29
+ if input.just_pressed(KeyCode::KeyP) {
30
+ dbg!(&items);
31
+ dbg!(&entity_instance);
32
+ }
33
+ }
34
+ }
src/components/line_of_sight.rs ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::marker::PhantomData;
2
+
3
+ use bevy::{
4
+ core::Name,
5
+ ecs::{ component::Component, entity::Entity, query::With, system::{ Query, Res } },
6
+ log,
7
+ math::Vec2,
8
+ transform::components::GlobalTransform,
9
+ };
10
+ use bevy_rapier2d::{ pipeline::QueryFilter, plugin::RapierContext };
11
+
12
+ /// Component applied to entities that should detect line of sight to the target
13
+ #[derive(Component, Clone)]
14
+ pub(crate) struct LineOfSight<Target> {
15
+ max_distance: f32,
16
+ last_sighted: Option<Vec2>,
17
+ in_sight: bool,
18
+ marker: PhantomData<Target>,
19
+ }
20
+
21
+ /// Component applied to entities that seek the target
22
+ impl<Target> Default for LineOfSight<Target> {
23
+ fn default() -> Self {
24
+ Self { max_distance: 250.0, last_sighted: None, in_sight: false, marker: PhantomData }
25
+ }
26
+ }
27
+
28
+ pub(crate) fn line_of_sight<Target: Component>(
29
+ targets: Query<(Entity, &GlobalTransform), With<Target>>,
30
+ mut observers: Query<(&mut LineOfSight<Target>, &GlobalTransform, Entity, Option<&Name>)>,
31
+ rapier_context: Res<RapierContext>
32
+ ) {
33
+ for (target_entity, target_transform) in &targets {
34
+ let target_pos = target_transform.translation().truncate();
35
+
36
+ // Iterate over entities having a LineOfSight component
37
+ for (
38
+ mut line_of_sight,
39
+ observer_transform,
40
+ observer_entity,
41
+ observer_name,
42
+ ) in &mut observers {
43
+ let observer_pos = observer_transform.translation().truncate();
44
+ let vector = target_pos - observer_pos;
45
+ let max_distance = line_of_sight.max_distance;
46
+ let distance = vector.length();
47
+ if distance > max_distance {
48
+ continue;
49
+ }
50
+ let ray_dir = vector.normalize();
51
+
52
+ // Cast ray from the observer toward the target
53
+ let stalked = if
54
+ let Some((collided_entity, toi)) = rapier_context.cast_ray(
55
+ observer_pos,
56
+ ray_dir,
57
+ max_distance,
58
+ false,
59
+ // FIXME: make sight obstacles (wall etc) into a group and use .groups() to only hit target and obstacles
60
+ QueryFilter::new().exclude_sensors().exclude_collider(observer_entity)
61
+ )
62
+ {
63
+ // TODO: remove debug
64
+ if collided_entity == target_entity && !line_of_sight.in_sight {
65
+ log::debug!(
66
+ "{:?}({:?}) sees the target! toi={}",
67
+ observer_name,
68
+ observer_entity,
69
+ toi
70
+ );
71
+ }
72
+ collided_entity == target_entity
73
+ } else {
74
+ false
75
+ };
76
+
77
+ // Minimal updates
78
+ if stalked {
79
+ let last_sighted = Some(target_pos);
80
+ if !line_of_sight.in_sight || line_of_sight.last_sighted != last_sighted {
81
+ let los_mut = line_of_sight.as_mut();
82
+ los_mut.last_sighted = last_sighted;
83
+ los_mut.in_sight = true;
84
+ }
85
+ } else {
86
+ if line_of_sight.in_sight {
87
+ line_of_sight.in_sight = false;
88
+ // TODO: remove debug
89
+ log::debug!("{:?}({:?}) lost the target.", observer_name, observer_entity);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
src/components/mod.rs ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pub(crate) mod animals;
2
+ pub(crate) mod health;
3
+ pub(crate) mod collision;
4
+ pub(crate) mod ground;
5
+ pub(crate) mod items;
6
+ pub(crate) mod sensorbundle;
7
+ pub(crate) mod camera;
8
+ pub(crate) mod hunger;
9
+ pub(crate) mod armor;
10
+ pub(crate) mod predefinedpath;
11
+ pub(crate) mod animation;
12
+ pub(crate) mod settings;
13
+ pub(crate) mod interactions;
14
+ pub(crate) mod deathzone;
15
+ pub(super) mod swimming;
16
+ pub(super) mod climbing;
17
+ pub(super) mod line_of_sight;
18
+ pub(super) mod ai;
19
+ pub(super) mod childof;
20
+ pub(super) mod name;
src/components/name.rs ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ use bevy::prelude::*;
2
+
3
+ #[derive(Component)]
4
+ pub struct Name(pub &'static str);
src/components/predefinedpath.rs ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::{
2
+ asset::{ AssetServer, Assets, Handle },
3
+ ecs::{ component::Component, system::Query },
4
+ math::{ IVec2, Vec2 },
5
+ render::texture::Image,
6
+ sprite::TextureAtlasLayout,
7
+ transform::components::Transform,
8
+ };
9
+ use bevy_ecs_ldtk::{
10
+ ldtk::{ LayerInstance, TilesetDefinition },
11
+ utils::ldtk_pixel_coords_to_translation_pivoted,
12
+ EntityInstance,
13
+ prelude::LdtkEntity,
14
+ prelude::LdtkFields,
15
+ };
16
+ use bevy_rapier2d::dynamics::Velocity;
17
+
18
+ #[derive(Clone, PartialEq, Debug, Default, Component)]
19
+ pub struct PredefinedPath {
20
+ pub points: Vec<Vec2>,
21
+ pub index: usize,
22
+ pub forward: bool,
23
+ }
24
+
25
+ impl LdtkEntity for PredefinedPath {
26
+ fn bundle_entity(
27
+ entity_instance: &EntityInstance,
28
+ layer_instance: &LayerInstance,
29
+ _: Option<&Handle<Image>>,
30
+ _: Option<&TilesetDefinition>,
31
+ _: &AssetServer,
32
+ _: &mut Assets<TextureAtlasLayout>
33
+ ) -> PredefinedPath {
34
+ let mut points = Vec::new();
35
+ points.push(
36
+ ldtk_pixel_coords_to_translation_pivoted(
37
+ entity_instance.px,
38
+ layer_instance.c_hei * layer_instance.grid_size,
39
+ IVec2::new(entity_instance.width, entity_instance.height),
40
+ entity_instance.pivot
41
+ )
42
+ );
43
+
44
+ let ldtk_path_points = entity_instance
45
+ .iter_points_field("path")
46
+ .expect("path field should be correctly typed");
47
+
48
+ for ldtk_point in ldtk_path_points {
49
+ // The +1 is necessary here due to the pivot of the entities in the sample
50
+ // file.
51
+ // The paths set up in the file look flat and grounded,
52
+ // but technically they're not if you consider the pivot,
53
+ // which is at the bottom-center for the skulls.
54
+ let pixel_coords =
55
+ (ldtk_point.as_vec2() + Vec2::new(0.5, 1.0)) *
56
+ Vec2::splat(layer_instance.grid_size as f32);
57
+
58
+ points.push(
59
+ ldtk_pixel_coords_to_translation_pivoted(
60
+ pixel_coords.as_ivec2(),
61
+ layer_instance.c_hei * layer_instance.grid_size,
62
+ IVec2::new(entity_instance.width, entity_instance.height),
63
+ entity_instance.pivot
64
+ )
65
+ );
66
+ }
67
+
68
+ PredefinedPath {
69
+ points,
70
+ index: 1,
71
+ forward: true,
72
+ }
73
+ }
74
+ }
75
+
76
+ pub fn move_on_path(mut query: Query<(&mut Transform, &mut Velocity, &mut PredefinedPath)>) {
77
+ for (mut transform, mut velocity, mut path) in &mut query {
78
+ if path.points.len() <= 1 {
79
+ continue;
80
+ }
81
+
82
+ let mut new_velocity =
83
+ (path.points[path.index] - transform.translation.truncate()).normalize() * 20.0;
84
+
85
+ if new_velocity.dot(velocity.linvel) < 0.0 {
86
+ if path.index == 0 {
87
+ path.forward = true;
88
+ } else if path.index == path.points.len() - 1 {
89
+ path.forward = false;
90
+ }
91
+
92
+ transform.translation.x = path.points[path.index].x;
93
+ transform.translation.y = path.points[path.index].y;
94
+
95
+ if path.forward {
96
+ path.index += 1;
97
+ } else {
98
+ path.index -= 1;
99
+ }
100
+
101
+ new_velocity =
102
+ (path.points[path.index] - transform.translation.truncate()).normalize() * 20.0;
103
+ }
104
+
105
+ velocity.linvel = new_velocity;
106
+ }
107
+ }
src/components/sensorbundle.rs ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::ecs::bundle::Bundle;
2
+ use bevy_ecs_ldtk::{ IntGridCell, LdtkIntCell };
3
+ use bevy_rapier2d::{ dynamics::LockedAxes, geometry::{ ActiveEvents, Collider, Sensor } };
4
+
5
+ #[derive(Clone, Default, Bundle, LdtkIntCell)]
6
+ pub struct SensorBundle {
7
+ pub collider: Collider,
8
+ pub sensor: Sensor,
9
+ pub active_events: ActiveEvents,
10
+ pub rotation_constraints: LockedAxes,
11
+ }
12
+
13
+ impl From<IntGridCell> for SensorBundle {
14
+ fn from(int_grid_cell: IntGridCell) -> SensorBundle {
15
+ let rotation_constraints = LockedAxes::ROTATION_LOCKED;
16
+
17
+ // Ladder
18
+ if int_grid_cell.value == 2 {
19
+ SensorBundle {
20
+ collider: Collider::cuboid(8.0, 8.0),
21
+ sensor: Sensor,
22
+ rotation_constraints,
23
+ active_events: ActiveEvents::COLLISION_EVENTS,
24
+ }
25
+ // Water
26
+ } else if int_grid_cell.value == 4 {
27
+ SensorBundle {
28
+ collider: Collider::cuboid(8.0, 8.0),
29
+ sensor: Sensor,
30
+ rotation_constraints,
31
+ active_events: ActiveEvents::COLLISION_EVENTS,
32
+ }
33
+ } else {
34
+ SensorBundle::default()
35
+ }
36
+ }
37
+ }
src/components/settings.rs ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use bevy::ecs::change_detection::DetectChanges;
2
+ use bevy::window::{ WindowLevel, PresentMode, PrimaryWindow, WindowMode };
3
+ use bevy::prelude::{ Resource, Res, Query, With, Window };
4
+
5
+ // GameWindowLevel
6
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
7
+ pub enum GameWindowLevel {
8
+ Normal,
9
+ AlwaysOnTop,
10
+ }
11
+
12
+ impl From<GameWindowLevel> for WindowLevel {
13
+ fn from(level: GameWindowLevel) -> Self {
14
+ match level {
15
+ GameWindowLevel::Normal => WindowLevel::Normal,
16
+ GameWindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop,
17
+ }
18
+ }
19
+ }
20
+
21
+ // GameVsyncMode
22
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
23
+ pub enum GameVsyncMode {
24
+ AutoNoVsync,
25
+ AutoVsync,
26
+ Immediate,
27
+ }
28
+
29
+ impl From<GameVsyncMode> for PresentMode {
30
+ fn from(mode: GameVsyncMode) -> Self {
31
+ match mode {
32
+ GameVsyncMode::AutoNoVsync => PresentMode::AutoNoVsync,
33
+ GameVsyncMode::AutoVsync => PresentMode::AutoVsync,
34
+ GameVsyncMode::Immediate => PresentMode::Immediate,
35
+ }
36
+ }
37
+ }
38
+
39
+ // GameWindowMode
40
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
41
+ pub enum GameWindowMode {
42
+ Windowed,
43
+ Fullscreen,
44
+ }
45
+
46
+ impl From<GameWindowMode> for WindowMode {
47
+ fn from(mode: GameWindowMode) -> Self {
48
+ match mode {
49
+ GameWindowMode::Windowed => WindowMode::Windowed,
50
+ GameWindowMode::Fullscreen => WindowMode::Fullscreen,
51
+ }
52
+ }
53
+ }
54
+
55
+ // GameSettings
56
+ #[derive(Resource)]
57
+ pub struct GameSettings {
58
+ pub window_level: GameWindowLevel,
59
+ pub vsync_mode: GameVsyncMode,
60
+ pub window_mode: GameWindowMode,
61
+ }
62
+
63
+ impl Default for GameSettings {
64
+ fn default() -> Self {
65
+ Self {
66
+ window_level: GameWindowLevel::Normal,
67
+ vsync_mode: GameVsyncMode::AutoVsync,
68
+ window_mode: GameWindowMode::Fullscreen,
69
+ }
70
+ }
71
+ }
72
+
73
+ pub fn update_window_settings(
74
+ settings: Res<GameSettings>,
75
+ mut primary_window: Query<&mut Window, With<PrimaryWindow>>
76
+ ) {
77
+ if settings.is_changed() {
78
+ if let Ok(mut window) = primary_window.get_single_mut() {
79
+ window.window_level = settings.window_level.into();
80
+ window.present_mode = settings.vsync_mode.into();
81
+ window.mode = settings.window_mode.into();
82
+ }
83
+ }
84
+ }
src/components/swimming.rs ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::collections::HashSet;
2
+ use bevy::ecs::{
3
+ component::Component,
4
+ entity::Entity,
5
+ event::EventReader,
6
+ query::With,
7
+ system::Query,
8
+ };
9
+ use bevy_rapier2d::pipeline::CollisionEvent;
10
+
11
+ use crate::plugins::rapier_utils::reciprocal_collisions;
12
+
13
+ #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Component)]
14
+ pub struct Swimmable;
15
+
16
+ // Attach this component to any entity to allow them to swim
17
+ #[derive(Clone, Eq, PartialEq, Debug, Default, Component)]
18
+ pub struct Swimmer {
19
+ pub swimming: bool,
20
+ pub intersecting_swimmables: HashSet<Entity>,
21
+ }
22
+
23
+ pub fn detect_swim_range(
24
+ mut swimmers: Query<&mut Swimmer>,
25
+ swimmables: Query<Entity, With<Swimmable>>,
26
+ mut collisions: EventReader<CollisionEvent>
27
+ ) {
28
+ reciprocal_collisions(&mut collisions, move |collider_a, collider_b, _, start| {
29
+ if
30
+ let (Ok(mut swimmer), Ok(swimmable)) = (
31
+ swimmers.get_mut(*collider_a),
32
+ swimmables.get(*collider_b),
33
+ )
34
+ {
35
+ if start {
36
+ swimmer.intersecting_swimmables.insert(swimmable);
37
+ } else {
38
+ swimmer.intersecting_swimmables.remove(&swimmable);
39
+ }
40
+ true
41
+ } else {
42
+ false
43
+ }
44
+ });
45
+ }