awoo
Browse filesSigned-off-by: Balazs Horvath <[email protected]>
This view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -35
- .github/FUNDING.yml +1 -0
- .github/workflows/rust.yml +48 -0
- .gitignore +11 -0
- .gitmodules +3 -0
- .prettierrc +6 -0
- .vscode/launch.json +75 -0
- Cargo.toml +182 -0
- LICENSE.txt +23 -0
- README.md +326 -0
- build.rs +11 -0
- build/windows/icon.ico +0 -0
- build/windows/icon.rc +1 -0
- docs/.gitignore +3 -0
- docs/Compilation Options.md +55 -0
- docs/plugins/bevy_device_lang.md +20 -0
- docs/plugins/bevy_ecs_ldtk.md +79 -0
- docs/plugins/bevy_flurx.md +103 -0
- docs/plugins/bevy_rapier2d.md +32 -0
- rust-toolchain.toml +2 -0
- scripts/Run-Debug.ps1 +28 -0
- scripts/Run-Release.ps1 +28 -0
- scripts/Update-Project.ps1 +2 -0
- src/components/ai/actions/drink.rs +53 -0
- src/components/ai/actions/mod.rs +6 -0
- src/components/ai/mod.rs +52 -0
- src/components/ai/scorers/mod.rs +11 -0
- src/components/ai/scorers/thirsty.rs +34 -0
- src/components/ai/thirst.rs +30 -0
- src/components/animals.rs +127 -0
- src/components/animation.rs +67 -0
- src/components/armor.rs +6 -0
- src/components/camera/fit_inside_current_level.rs +72 -0
- src/components/camera/mod.rs +5 -0
- src/components/childof.rs +18 -0
- src/components/climbing.rs +59 -0
- src/components/collision.rs +344 -0
- src/components/deathzone.rs +5 -0
- src/components/ground.rs +75 -0
- src/components/health.rs +32 -0
- src/components/hunger.rs +35 -0
- src/components/interactions.rs +152 -0
- src/components/items.rs +34 -0
- src/components/line_of_sight.rs +94 -0
- src/components/mod.rs +20 -0
- src/components/name.rs +4 -0
- src/components/predefinedpath.rs +107 -0
- src/components/sensorbundle.rs +37 -0
- src/components/settings.rs +84 -0
- src/components/swimming.rs +45 -0
.gitattributes
CHANGED
@@ -1,35 +1 @@
|
|
1 |
-
|
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 ¤t_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 |
+
}
|