Merge pull request #23 from kristoferssolo/feat/music
6
.github/workflows/ci.yaml
vendored
@ -21,13 +21,17 @@ jobs:
|
|||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
|
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
|
||||||
|
- name: Install cargo-nextest
|
||||||
|
run: cargo install cargo-nextest
|
||||||
- name: Populate target directory from cache
|
- name: Populate target directory from cache
|
||||||
uses: Leafwing-Studios/cargo-cache@v2
|
uses: Leafwing-Studios/cargo-cache@v2
|
||||||
with:
|
with:
|
||||||
sweep-cache: true
|
sweep-cache: true
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
cargo test --locked --workspace --no-default-features
|
cargo nextest run --locked --workspace --no-default-features --all-targets
|
||||||
|
# Run doctests separately since nextest doesn't support them
|
||||||
|
cargo test --doc --locked --workspace --no-default-features
|
||||||
# Run clippy lints.
|
# Run clippy lints.
|
||||||
clippy:
|
clippy:
|
||||||
name: Clippy
|
name: Clippy
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 956 B |
353
docs/design.md
@ -1,353 +0,0 @@
|
|||||||
# Design philosophy
|
|
||||||
|
|
||||||
The high-level goal of this template is to feel like the official template that is currently missing from Bevy.
|
|
||||||
The exists an [official CI template](https://github.com/bevyengine/bevy_github_ci_template), but, in our opinion,
|
|
||||||
that one is currently more of an extension to the [Bevy examples](https://bevyengine.org/examples/) than an actual template.
|
|
||||||
We say this because it is extremely bare-bones and as such does not provide things that in practice are necessary for game development.
|
|
||||||
|
|
||||||
## Principles
|
|
||||||
|
|
||||||
So, how would an official template that is built for real-world game development look like?
|
|
||||||
The Bevy Jam working group has agreed on the following guiding design principles:
|
|
||||||
|
|
||||||
- Show how to do things in pure Bevy. This means using no 3rd-party dependencies.
|
|
||||||
- Have some basic game code written out already.
|
|
||||||
- Have everything outside of code already set up.
|
|
||||||
- Nice IDE support.
|
|
||||||
- `cargo-generate` support.
|
|
||||||
- Workflows that provide CI and CD with an auto-publish to itch.io.
|
|
||||||
- Builds configured for performance by default.
|
|
||||||
- Answer questions that will quickly come up when creating an actual game.
|
|
||||||
- How do I structure my code?
|
|
||||||
- How do I preload assets?
|
|
||||||
- What are best practices for creating UI?
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
The last point means that in order to make this template useful for real-life projects,
|
|
||||||
we have to make some decisions that are necessarily opinionated.
|
|
||||||
|
|
||||||
These opinions are based on the experience of the Bevy Jam working group and
|
|
||||||
what we have found to be useful in our own projects.
|
|
||||||
If you disagree with any of these, it should be easy to change them.
|
|
||||||
|
|
||||||
Bevy is still young, and many design patterns are still being discovered and refined.
|
|
||||||
Most do not even have an agreed name yet. For some prior work in this area that inspired us,
|
|
||||||
see [the Unofficial Bevy Cheatbook](https://bevy-cheatbook.github.io/) and [bevy_best_practices](https://github.com/tbillington/bevy_best_practices).
|
|
||||||
|
|
||||||
## Pattern Table of Contents
|
|
||||||
|
|
||||||
- [Plugin Organization](#plugin-organization)
|
|
||||||
- [Widgets](#widgets)
|
|
||||||
- [Asset Preloading](#asset-preloading)
|
|
||||||
- [Spawn Commands](#spawn-commands)
|
|
||||||
- [Interaction Callbacks](#interaction-callbacks)
|
|
||||||
- [Dev Tools](#dev-tools)
|
|
||||||
- [Screen States](#screen-states)
|
|
||||||
|
|
||||||
When talking about these, use their name followed by "pattern",
|
|
||||||
e.g. "the widgets pattern", or "the plugin organization pattern".
|
|
||||||
|
|
||||||
## Plugin Organization
|
|
||||||
|
|
||||||
### Pattern
|
|
||||||
|
|
||||||
Structure your code into plugins like so:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// game.rs
|
|
||||||
mod player;
|
|
||||||
mod enemy;
|
|
||||||
mod powerup;
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
|
||||||
app.add_plugins((player::plugin, enemy::plugin, powerup::plugin));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// player.rs / enemy.rs / powerup.rs
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
|
||||||
app.add_systems(Update, (your, systems, here));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reasoning
|
|
||||||
|
|
||||||
Bevy is great at organizing code into plugins. The most lightweight way to do this is by using simple functions as plugins.
|
|
||||||
By splitting your code like this, you can easily keep all your systems and resources locally grouped. Everything that belongs to the `player` is only in `player.rs`, and so on.
|
|
||||||
|
|
||||||
A good rule of thumb is to have one plugin per file,
|
|
||||||
but feel free to leave out a plugin if your file does not need to do anything with the `App`.
|
|
||||||
|
|
||||||
## Widgets
|
|
||||||
|
|
||||||
### Pattern
|
|
||||||
|
|
||||||
Spawn your UI elements by extending the [`Widgets` trait](../src/theme/widgets.rs):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub trait Widgets {
|
|
||||||
fn button(&mut self, text: impl Into<String>) -> EntityCommands;
|
|
||||||
fn header(&mut self, text: impl Into<String>) -> EntityCommands;
|
|
||||||
fn label(&mut self, text: impl Into<String>) -> EntityCommands;
|
|
||||||
fn text_input(&mut self, text: impl Into<String>) -> EntityCommands;
|
|
||||||
fn image(&mut self, texture: Handle<Texture>) -> EntityCommands;
|
|
||||||
fn progress_bar(&mut self, progress: f32) -> EntityCommands;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reasoning
|
|
||||||
|
|
||||||
This pattern is inspired by [sickle_ui](https://github.com/UmbraLuminosa/sickle_ui).
|
|
||||||
`Widgets` is implemented for `Commands` and similar, so you can easily spawn UI elements in your systems.
|
|
||||||
By encapsulating a widget inside a function, you save on a lot of boilerplate code and can easily change the appearance of all widgets of a certain type.
|
|
||||||
By returning `EntityCommands`, you can easily chain multiple widgets together and insert children into a parent widget.
|
|
||||||
|
|
||||||
## Asset Preloading
|
|
||||||
|
|
||||||
### Pattern
|
|
||||||
|
|
||||||
Define your assets with a resource that maps asset paths to `Handle`s.
|
|
||||||
If you're defining the assets in code, add their paths as constants.
|
|
||||||
Otherwise, load them dynamically from e.g. a file.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Resource, Debug, Deref, DerefMut, Reflect)]
|
|
||||||
#[reflect(Resource)]
|
|
||||||
pub struct ImageHandles(HashMap<String, Handle<Image>>);
|
|
||||||
|
|
||||||
impl ImageHandles {
|
|
||||||
pub const PATH_PLAYER: &'static str = "images/player.png";
|
|
||||||
pub const PATH_ENEMY: &'static str = "images/enemy.png";
|
|
||||||
pub const PATH_POWERUP: &'static str = "images/powerup.png";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromWorld for ImageHandles {
|
|
||||||
fn from_world(world: &mut World) -> Self {
|
|
||||||
let asset_server = world.resource::<AssetServer>();
|
|
||||||
|
|
||||||
let paths = [
|
|
||||||
ImageHandles::PATH_PLAYER,
|
|
||||||
ImageHandles::PATH_ENEMY,
|
|
||||||
ImageHandles::PATH_POWERUP,
|
|
||||||
];
|
|
||||||
let map = paths
|
|
||||||
.into_iter()
|
|
||||||
.map(|path| (path.to_string(), asset_server.load(path)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self(map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then start preloading in the `assets::plugin`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
|
||||||
app.register_type::<ImageHandles>();
|
|
||||||
app.init_resource::<ImageHandles>();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And finally add a loading check to the `screens::loading::plugin`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn all_assets_loaded(
|
|
||||||
image_handles: Res<ImageHandles>,
|
|
||||||
) -> bool {
|
|
||||||
image_handles.all_loaded(&asset_server)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reasoning
|
|
||||||
|
|
||||||
This pattern is inspired by [bevy_asset_loader](https://github.com/NiklasEi/bevy_asset_loader).
|
|
||||||
By preloading your assets, you can avoid hitches during gameplay.
|
|
||||||
We start loading as soon as the app starts and wait for all assets to be loaded in the loading screen.
|
|
||||||
|
|
||||||
By using strings as keys, you can dynamically load assets based on input data such as a level file.
|
|
||||||
If you prefer a purely static approach, you can also use an `enum YourAssetHandleKey` and `impl AsRef<str> for YourAssetHandleKey`.
|
|
||||||
You can also mix the dynamic and static approach according to your needs.
|
|
||||||
|
|
||||||
## Spawn Commands
|
|
||||||
|
|
||||||
### Pattern
|
|
||||||
|
|
||||||
Spawn a game object by using a custom command. Inside the command,
|
|
||||||
run the spawning code with `world.run_system_once` or `world.run_system_once_with`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// monster.rs
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SpawnMonster {
|
|
||||||
pub health: u32,
|
|
||||||
pub transform: Transform,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command for SpawnMonster {
|
|
||||||
fn apply(self, world: &mut World) {
|
|
||||||
world.run_system_once_with(self, spawn_monster);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_monster(
|
|
||||||
spawn_monster: In<SpawnMonster>,
|
|
||||||
mut commands: Commands,
|
|
||||||
) {
|
|
||||||
commands.spawn((
|
|
||||||
Name::new("Monster"),
|
|
||||||
Health::new(spawn_monster.health),
|
|
||||||
SpatialBundle::from_transform(spawn_monster.transform),
|
|
||||||
// other components
|
|
||||||
));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And then to use a spawn command, add it to `Commands`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// dangerous_forest.rs
|
|
||||||
|
|
||||||
fn spawn_forest_goblin(mut commands: Commands) {
|
|
||||||
commands.add(SpawnMonster {
|
|
||||||
health: 100,
|
|
||||||
transform: Transform::from_xyz(10.0, 0.0, 0.0),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reasoning
|
|
||||||
|
|
||||||
By encapsulating the spawning of a game object in a custom command,
|
|
||||||
you save on boilerplate code and can easily change the behavior of spawning.
|
|
||||||
We use `world.run_system_once_with` to run the spawning code with the same syntax as a regular system.
|
|
||||||
That way you can easily add system parameters to access things like assets and resources while spawning the entity.
|
|
||||||
|
|
||||||
A limitation of this approach is that calling code cannot extend the spawn call with additional components or children,
|
|
||||||
as custom commands don't return `Entity` or `EntityCommands`. This kind of usage will be possible in future Bevy versions.
|
|
||||||
|
|
||||||
## Interaction Callbacks
|
|
||||||
|
|
||||||
### Pattern
|
|
||||||
|
|
||||||
When spawning an entity that can be interacted with, such as a button that can be pressed,
|
|
||||||
use an observer to handle the interaction:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn spawn_button(mut commands: Commands) {
|
|
||||||
// See the Widgets pattern for information on the `button` method
|
|
||||||
commands.button("Pay up!").observe(pay_money);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pay_money(_trigger: Trigger<OnPress>, mut money: ResMut<Money>) {
|
|
||||||
money.0 -= 10.0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The event `OnPress`, which is [defined in this template](../src/theme/interaction.rs),
|
|
||||||
is triggered when the button is [`Interaction::Pressed`](https://docs.rs/bevy/latest/bevy/prelude/enum.Interaction.html#variant.Pressed).
|
|
||||||
|
|
||||||
If you have many interactions that only change a state, consider using the following helper function:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn spawn_button(mut commands: Commands) {
|
|
||||||
commands.button("Play the game").observe(enter_state(Screen::Gameplay));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn enter_state<S: FreelyMutableState>(
|
|
||||||
new_state: S,
|
|
||||||
) -> impl Fn(Trigger<OnPress>, ResMut<NextState<S>>) {
|
|
||||||
move |_trigger, mut next_state| next_state.set(new_state.clone())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reasoning
|
|
||||||
|
|
||||||
This pattern is inspired by [bevy_mod_picking](https://github.com/aevyrie/bevy_mod_picking).
|
|
||||||
By pairing the system handling the interaction with the entity as an observer,
|
|
||||||
the code running on interactions can be scoped to the exact context of the interaction.
|
|
||||||
|
|
||||||
For example, the code for what happens when you press a *specific* button is directly attached to that exact button.
|
|
||||||
|
|
||||||
This also keeps the interaction logic close to the entity that is interacted with,
|
|
||||||
allowing for better code organization.
|
|
||||||
|
|
||||||
## Dev Tools
|
|
||||||
|
|
||||||
### Pattern
|
|
||||||
|
|
||||||
Add all systems that are only relevant while developing the game to the [`dev_tools` plugin](../src/dev_tools.rs):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// dev_tools.rs
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
|
||||||
app.add_systems(Update, (draw_debug_lines, show_debug_console, show_fps_counter));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reasoning
|
|
||||||
|
|
||||||
The `dev_tools` plugin is only included in dev builds.
|
|
||||||
By adding your dev tools here, you automatically guarantee that they are not included in release builds.
|
|
||||||
|
|
||||||
## Screen States
|
|
||||||
|
|
||||||
### Pattern
|
|
||||||
|
|
||||||
Use the [`Screen`](../src/screen/mod.rs) enum to represent your game's screens as states:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(States, Debug, Hash, PartialEq, Eq, Clone, Default)]
|
|
||||||
pub enum Screen {
|
|
||||||
#[default]
|
|
||||||
Splash,
|
|
||||||
Loading,
|
|
||||||
Title,
|
|
||||||
Gameplay,
|
|
||||||
Victory,
|
|
||||||
Leaderboard,
|
|
||||||
MultiplayerLobby,
|
|
||||||
SecretMinigame,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Constrain entities that should only be present in a certain screen to that screen by adding a
|
|
||||||
[`StateScoped`](https://docs.rs/bevy/latest/bevy/prelude/struct.StateScoped.html) component to them.
|
|
||||||
Transition between screens by setting the [`NextState<Screen>`](https://docs.rs/bevy/latest/bevy/prelude/enum.NextState.html) resource.
|
|
||||||
|
|
||||||
For each screen, create a plugin that handles the setup and teardown of the screen with `OnEnter` and `OnExit`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// game_over.rs
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
|
||||||
app.add_systems(OnEnter(Screen::Victory), show_victory_screen);
|
|
||||||
app.add_systems(OnExit(Screen::Victory), reset_highscore);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_victory_screen(mut commands: Commands) {
|
|
||||||
commands.
|
|
||||||
.ui_root()
|
|
||||||
.insert((Name::new("Victory screen"), StateScoped(Screen::Victory)))
|
|
||||||
.with_children(|parent| {
|
|
||||||
// Spawn UI elements.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset_highscore(mut highscore: ResMut<Highscore>) {
|
|
||||||
*highscore = default();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reasoning
|
|
||||||
|
|
||||||
"Screen" is not meant as a physical screen, but as "what kind of screen is the game showing right now", e.g. the title screen, the loading screen, the credits screen, the victory screen, etc.
|
|
||||||
These screens usually correspond to different logical states of your game that have different systems running.
|
|
||||||
|
|
||||||
By using dedicated `State`s for each screen, you can easily manage systems and entities that are only relevant for a certain screen.
|
|
||||||
This allows you to flexibly transition between screens whenever your game logic requires it.
|
|
||||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 73 KiB |
@ -1,27 +0,0 @@
|
|||||||
# Known Issues
|
|
||||||
|
|
||||||
## My audio is stuttering on web
|
|
||||||
|
|
||||||
There are a number of issues with audio on web, so this is not an exhaustive list. The short version is that you can try the following:
|
|
||||||
|
|
||||||
- If you use materials, make sure to force render pipelines to [load at the start of the game](https://github.com/rparrett/bevy_pipelines_ready/blob/main/src/lib.rs).
|
|
||||||
- Keep the FPS high.
|
|
||||||
- Advise your users to play on Chromium-based browsers.
|
|
||||||
- Apply the suggestions from the blog post [Workaround for the Choppy Music in Bevy Web Builds](https://necrashter.github.io/bevy-choppy-music-workaround).
|
|
||||||
|
|
||||||
## My game window is flashing white for a split second when I start the game on native
|
|
||||||
|
|
||||||
The game window is created before the GPU is ready to render everything.
|
|
||||||
This means that it will start with a white screen for a little bit.
|
|
||||||
The workaround is to [spawn the Window hidden](https://github.com/bevyengine/bevy/blob/release-0.14.0/examples/window/window_settings.rs#L29-L32)
|
|
||||||
and then [make it visible after a few frames](https://github.com/bevyengine/bevy/blob/release-0.14.0/examples/window/window_settings.rs#L56-L64).
|
|
||||||
|
|
||||||
## My character or camera is not moving smoothly
|
|
||||||
|
|
||||||
Choppy movement is often caused by movement updates being tied to the frame rate.
|
|
||||||
See the [physics_in_fixed_timestep](https://github.com/bevyengine/bevy/blob/main/examples/movement/physics_in_fixed_timestep.rs) example
|
|
||||||
for how to fix this.
|
|
||||||
|
|
||||||
A camera not moving smoothly is pretty much always caused by the camera position being tied too tightly to the character's position.
|
|
||||||
To give the camera some inertia, use the [`smooth_nudge`](https://github.com/bevyengine/bevy/blob/main/examples/movement/smooth_follow.rs#L127-L142)
|
|
||||||
to interpolate the camera position towards its target position.
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
# Recommended 3rd-party tools
|
|
||||||
|
|
||||||
Check out the [Bevy Assets](https://bevyengine.org/assets/) page for more great options.
|
|
||||||
|
|
||||||
## Libraries
|
|
||||||
|
|
||||||
A few libraries that the authors of this template have vetted and think you might find useful:
|
|
||||||
|
|
||||||
| Name | Category | Description |
|
|
||||||
| -------------------------------------------------------------------------------------- | -------------- | ------------------------------------- |
|
|
||||||
| [`leafwing-input-manager`](https://github.com/Leafwing-Studios/leafwing-input-manager) | Input | Input -> Action mapping |
|
|
||||||
| [`bevy_mod_picking`](https://github.com/aevyrie/bevy_mod_picking) | Input | Advanced mouse interaction |
|
|
||||||
| [`bevy-inspector-egui`](https://github.com/jakobhellermann/bevy-inspector-egui) | Debugging | Live entity inspector |
|
|
||||||
| [`bevy_mod_debugdump`](https://github.com/jakobhellermann/bevy_mod_debugdump) | Debugging | Schedule inspector |
|
|
||||||
| [`avian`](https://github.com/Jondolf/avian) | Physics | Physics engine |
|
|
||||||
| [`bevy_rapier`](https://github.com/dimforge/bevy_rapier) | Physics | Physics engine (not ECS-driven) |
|
|
||||||
| [`bevy_common_assets`](https://github.com/NiklasEi/bevy_common_assets) | Asset loading | Asset loaders for common file formats |
|
|
||||||
| [`bevy_asset_loader`](https://github.com/NiklasEi/bevy_asset_loader) | Asset loading | Asset management tools |
|
|
||||||
| [`iyes_progress`](https://github.com/IyesGames/iyes_progress) | Asset loading | Progress tracking |
|
|
||||||
| [`bevy_kira_audio`](https://github.com/NiklasEi/bevy_kira_audio) | Audio | Advanced audio |
|
|
||||||
| [`sickle_ui`](https://github.com/UmbraLuminosa/sickle_ui) | UI | UI widgets |
|
|
||||||
| [`bevy_egui`](https://github.com/mvlabat/bevy_egui) | UI / Debugging | UI framework (great for debug UI) |
|
|
||||||
| [`tiny_bail`](https://github.com/benfrankel/tiny_bail) | Error handling | Error handling macros |
|
|
||||||
|
|
||||||
In particular:
|
|
||||||
|
|
||||||
- `leafwing-input-manager` and `bevy_mod_picking` are very likely to be upstreamed into Bevy in the near future.
|
|
||||||
- `bevy-inspector-egui` and `bevy_mod_debugdump` help fill the gap until Bevy has its own editor.
|
|
||||||
- `avian` or `bevy_rapier` helps fill the gap until Bevy has its own physics engine. `avian` is easier to use, while `bevy_rapier` is more performant.
|
|
||||||
- `sickle_ui` is well-aligned with `bevy_ui` and helps fill the gap until Bevy has a full collection of UI widgets.
|
|
||||||
|
|
||||||
None of these are necessary, but they can save you a lot of time and effort.
|
|
||||||
|
|
||||||
## VS Code extensions
|
|
||||||
|
|
||||||
If you're using [VS Code](https://code.visualstudio.com/), the following extensions are highly recommended:
|
|
||||||
|
|
||||||
| Name | Description |
|
|
||||||
|-----------------------------------------------------------------------------------------------------------|-----------------------------------|
|
|
||||||
| [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) | Rust support |
|
|
||||||
| [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) | TOML support |
|
|
||||||
| [vscode-ron](https://marketplace.visualstudio.com/items?itemName=a5huynh.vscode-ron) | RON support |
|
|
||||||
| [Dependi](https://marketplace.visualstudio.com/items?itemName=fill-labs.dependi) | `crates.io` dependency resolution |
|
|
||||||
| [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) | `.editorconfig` support |
|
|
||||||
|
|
||||||
> [!Note]
|
|
||||||
> <details>
|
|
||||||
> <summary>About the included rust-analyzer settings</summary>
|
|
||||||
>
|
|
||||||
> This template sets [`rust-analyzer.cargo.targetDir`](https://rust-analyzer.github.io/generated_config.html#rust-analyzer.cargo.targetDir)
|
|
||||||
> to `true` in [`.vscode/settings.json`](../.vscode/settings.json).
|
|
||||||
>
|
|
||||||
> This makes `rust-analyzer` use a different `target` directory than `cargo`,
|
|
||||||
> which means that you can run commands like `cargo run` even while `rust-analyzer` is still indexing.
|
|
||||||
> As a trade-off, this will use more disk space.
|
|
||||||
>
|
|
||||||
> If that is an issue for you, you can set it to `false` or remove the setting entirely.
|
|
||||||
> </details>
|
|
||||||
|
|
||||||
## Other templates
|
|
||||||
|
|
||||||
There are many other Bevy templates out there.
|
|
||||||
Check out the [templates category](https://bevyengine.org/assets/#templates) on Bevy Assets for more options.
|
|
||||||
Even if you don't end up using them, they are a great way to learn how to implement certain features you might be interested in.
|
|
||||||
@ -1,125 +0,0 @@
|
|||||||
# Workflows
|
|
||||||
|
|
||||||
This template uses [GitHub workflows](https://docs.github.com/en/actions/using-workflows) for [CI / CD](https://www.redhat.com/en/topics/devops/what-is-ci-cd), defined in [`.github/workflows/`](../.github/workflows).
|
|
||||||
|
|
||||||
## CI (testing)
|
|
||||||
|
|
||||||
The [CI workflow](.github/workflows/ci.yaml) will trigger on every commit or PR to `main`, and do the following:
|
|
||||||
|
|
||||||
- Run tests.
|
|
||||||
- Run Clippy lints.
|
|
||||||
- Check formatting.
|
|
||||||
- Check documentation.
|
|
||||||
|
|
||||||
> [!Tip]
|
|
||||||
> <details>
|
|
||||||
> <summary>You may want to set up a <a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets">GitHub ruleset</a> to require that all commits to <code>main</code> pass CI.</summary>
|
|
||||||
>
|
|
||||||
> <img src="img/workflow-ruleset.png" alt="A screenshot showing a GitHub ruleset with status checks enabled" width="100%">
|
|
||||||
> </details>
|
|
||||||
|
|
||||||
## CD (releasing)
|
|
||||||
|
|
||||||
The [CD workflow](../.github/workflows/release.yaml) will trigger on every pushed tag in the format `v1.2.3`, and do the following:
|
|
||||||
|
|
||||||
- Create a release build for Windows, macOS, Linux, and web.
|
|
||||||
- (Optional) Upload to [GitHub releases](https://docs.github.com/en/repositories/releasing-projects-on-github).
|
|
||||||
- (Optional) Upload to [itch.io](https://itch.io).
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>This workflow can also be triggered manually.</summary>
|
|
||||||
|
|
||||||
In your GitHub repository, navigate to `Actions > Release > Run workflow`:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Enter a version number in the format `v1.2.3`, then hit the green `Run workflow` button.
|
|
||||||
</details>
|
|
||||||
|
|
||||||
> [!Important]
|
|
||||||
> Using this workflow requires some setup. We will go through this now.
|
|
||||||
|
|
||||||
### Configure environment variables
|
|
||||||
|
|
||||||
The release workflow can be configured by tweaking the environment variables in [`.github/workflows/release.yaml`](../.github/workflows/release.yaml).
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Click here for a list of variables and how they're used.</summary>
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# The base filename of the binary produced by `cargo build`.
|
|
||||||
cargo_build_binary_name: bevy_quickstart
|
|
||||||
|
|
||||||
# The path to the assets directory.
|
|
||||||
assets_path: assets
|
|
||||||
|
|
||||||
# Whether to upload the packages produced by this workflow to a GitHub release.
|
|
||||||
upload_to_github: true
|
|
||||||
|
|
||||||
# The itch.io project to upload to in the format `user-name/project-name`.
|
|
||||||
# There will be no upload to itch.io if this is commented out.
|
|
||||||
upload_to_itch: the-bevy-flock/bevy-quickstart
|
|
||||||
|
|
||||||
############
|
|
||||||
# ADVANCED #
|
|
||||||
############
|
|
||||||
|
|
||||||
# The ID of the app produced by this workflow.
|
|
||||||
# Applies to macOS releases.
|
|
||||||
# Must contain only A-Z, a-z, 0-9, hyphens, and periods: https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier
|
|
||||||
app_id: the-bevy-flock.bevy-quickstart
|
|
||||||
|
|
||||||
# The base filename of the binary in the package produced by this workflow.
|
|
||||||
# Applies to Windows, macOS, and Linux releases.
|
|
||||||
# Defaults to `cargo_build_binary_name` if commented out.
|
|
||||||
app_binary_name: bevy_quickstart
|
|
||||||
|
|
||||||
# The name of the `.zip` or `.dmg` file produced by this workflow.
|
|
||||||
# Defaults to `app_binary_name` if commented out.
|
|
||||||
app_package_name: bevy_quickstart
|
|
||||||
|
|
||||||
# The display name of the app produced by this workflow.
|
|
||||||
# Applies to macOS releases.
|
|
||||||
# Defaults to `app_package_name` if commented out.
|
|
||||||
app_display_name: Bevy Quickstart
|
|
||||||
|
|
||||||
# The short display name of the app produced by this workflow.
|
|
||||||
# Applies to macOS releases.
|
|
||||||
# Must be 15 or fewer characters: https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundlename
|
|
||||||
# Defaults to `app_display_name` if commented out.
|
|
||||||
app_short_name: Bevy Quickstart
|
|
||||||
|
|
||||||
# Before enabling LFS, please take a look at GitHub's documentation for costs and quota limits:
|
|
||||||
# https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-storage-and-bandwidth-usage
|
|
||||||
git_lfs: false
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
The values are set automatically by `cargo generate`, or you can edit them yourself and push a commit.
|
|
||||||
|
|
||||||
### Set up itch.io upload
|
|
||||||
|
|
||||||
#### Add butler credentials
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>In your GitHub repository, navigate to <code>Settings > Secrets and variables > Actions</code>.</summary>
|
|
||||||
|
|
||||||

|
|
||||||
</details>
|
|
||||||
|
|
||||||
Hit `New repository secret` and enter the following values, then hit `Add secret`:
|
|
||||||
|
|
||||||
- **Name:** `BUTLER_CREDENTIALS`
|
|
||||||
- **Secret:** Your [itch.io API key](https://itch.io/user/settings/api-keys) (create a new one if necessary)
|
|
||||||
|
|
||||||
#### Create itch.io project
|
|
||||||
|
|
||||||
Create a new itch.io project with the same user and project name as in the `upload_to_itch` variable in [`.github/workflows/release.yaml`](../.github/workflows/release.yaml).
|
|
||||||
Hit `Save & view page` at the bottom of the page.
|
|
||||||
|
|
||||||
[Trigger the release workflow](#cd-releasing) for the first time. Once it's done, go back to itch.io and hit `Edit game` in the top left.
|
|
||||||
|
|
||||||
Set `Kind of project` to `HTML`, then find the newly uploaded `web` build and tick the box that says "This file will be played in the browser".
|
|
||||||
|
|
||||||

|
|
||||||
3
justfile
@ -20,6 +20,7 @@ web-release:
|
|||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
test:
|
test:
|
||||||
|
RUSTC_WRAPPER=sccache RUST_BACKTRACE=full cargo test --doc --locked --workspace --no-default-features
|
||||||
RUSTC_WRAPPER=sccache RUST_BACKTRACE=full cargo nextest run --no-default-features --all-targets
|
RUSTC_WRAPPER=sccache RUST_BACKTRACE=full cargo nextest run --no-default-features --all-targets
|
||||||
|
|
||||||
# Run CI localy
|
# Run CI localy
|
||||||
@ -29,4 +30,4 @@ ci:
|
|||||||
cargo fmt --all -- --check
|
cargo fmt --all -- --check
|
||||||
cargo clippy --workspace --all-targets --all-features -- --deny warnings
|
cargo clippy --workspace --all-targets --all-features -- --deny warnings
|
||||||
cargo doc --workspace --all-features --document-private-items --no-deps
|
cargo doc --workspace --all-features --document-private-items --no-deps
|
||||||
cargo test --workspace --no-default-features
|
just test
|
||||||
|
|||||||
@ -10,6 +10,12 @@ use crate::{
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
/// Move floor entities to their target Y positions based on movement speed
|
||||||
|
///
|
||||||
|
/// # Behavior
|
||||||
|
/// - Calculates movement distance based on player speed and delta time
|
||||||
|
/// - Moves floors towards their target Y position
|
||||||
|
/// - Removes FloorYTarget component when floor reaches destination
|
||||||
pub fn move_floors(
|
pub fn move_floors(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut maze_query: Query<(Entity, &mut Transform, &FloorYTarget), With<FloorYTarget>>,
|
mut maze_query: Query<(Entity, &mut Transform, &FloorYTarget), With<FloorYTarget>>,
|
||||||
@ -30,6 +36,13 @@ pub fn move_floors(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle floor transition events by setting up floor movement targets
|
||||||
|
///
|
||||||
|
/// # Behavior
|
||||||
|
/// - Checks if any floors are currently moving
|
||||||
|
/// - Processes floor transition events
|
||||||
|
/// - Sets target Y positions for all maze entities
|
||||||
|
/// - Updates current and next floor designations
|
||||||
pub fn handle_floor_transition_events(
|
pub fn handle_floor_transition_events(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut maze_query: Query<(Entity, &Transform, &Floor, Option<&FloorYTarget>), With<HexMaze>>,
|
mut maze_query: Query<(Entity, &Transform, &Floor, Option<&FloorYTarget>), With<HexMaze>>,
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
floor::{
|
floor::{
|
||||||
components::{CurrentFloor, Floor, FloorYTarget},
|
components::{CurrentFloor, Floor, FloorYTarget},
|
||||||
@ -6,7 +8,6 @@ use crate::{
|
|||||||
},
|
},
|
||||||
maze::{components::MazeConfig, events::SpawnMaze},
|
maze::{components::MazeConfig, events::SpawnMaze},
|
||||||
};
|
};
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
pub(super) fn spawn_floor(
|
pub(super) fn spawn_floor(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
@ -19,7 +20,7 @@ pub(super) fn spawn_floor(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for event in event_reader.read() {
|
for event in event_reader.read() {
|
||||||
if current_floor.0 == 0 && *event == TransitionFloor::Descend {
|
if current_floor.0 == 1 && *event == TransitionFloor::Descend {
|
||||||
warn!("Cannot descend below floor 1");
|
warn!("Cannot descend below floor 1");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ impl Plugin for AppPlugin {
|
|||||||
})
|
})
|
||||||
.set(AudioPlugin {
|
.set(AudioPlugin {
|
||||||
global_volume: GlobalVolume {
|
global_volume: GlobalVolume {
|
||||||
volume: Volume::new(0.),
|
volume: Volume::new(0.2),
|
||||||
},
|
},
|
||||||
..default()
|
..default()
|
||||||
}),
|
}),
|
||||||
@ -84,7 +84,7 @@ enum AppSet {
|
|||||||
TickTimers,
|
TickTimers,
|
||||||
/// Record player input.
|
/// Record player input.
|
||||||
RecordInput,
|
RecordInput,
|
||||||
/// Do everything else (consider splitting this into further variants).
|
/// Do everything else.
|
||||||
Update,
|
Update,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
|
//! Maze asset management and generation.
|
||||||
|
//!
|
||||||
|
//! Module handles the creation and management of meshes and materials
|
||||||
|
//! used in the maze visualization, including hexagonal tiles and walls.
|
||||||
|
|
||||||
use super::resources::GlobalMazeConfig;
|
use super::resources::GlobalMazeConfig;
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::WALL_OVERLAP_MODIFIER,
|
constants::WALL_OVERLAP_MODIFIER,
|
||||||
theme::{palette::rose_pine::RosePineDawn, prelude::ColorScheme},
|
theme::{palette::rose_pine::RosePineDawn, prelude::ColorScheme},
|
||||||
};
|
};
|
||||||
|
|
||||||
use bevy::{prelude::*, utils::HashMap};
|
use bevy::{prelude::*, utils::HashMap};
|
||||||
use std::f32::consts::FRAC_PI_2;
|
use std::f32::consts::FRAC_PI_2;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
@ -10,15 +16,26 @@ use strum::IntoEnumIterator;
|
|||||||
const HEX_SIDES: u32 = 6;
|
const HEX_SIDES: u32 = 6;
|
||||||
const WHITE_EMISSION_INTENSITY: f32 = 10.;
|
const WHITE_EMISSION_INTENSITY: f32 = 10.;
|
||||||
|
|
||||||
|
/// Collection of mesh and material assets used in maze rendering.
|
||||||
|
///
|
||||||
|
/// This struct contains all the necessary assets for rendering maze components,
|
||||||
|
/// including hexagonal tiles, walls, and custom colored materials.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct MazeAssets {
|
pub struct MazeAssets {
|
||||||
|
/// Mesh for hexagonal floor tiles
|
||||||
pub hex_mesh: Handle<Mesh>,
|
pub hex_mesh: Handle<Mesh>,
|
||||||
|
/// Mesh for wall segments
|
||||||
pub wall_mesh: Handle<Mesh>,
|
pub wall_mesh: Handle<Mesh>,
|
||||||
|
/// Default material for hexagonal tiles
|
||||||
pub hex_material: Handle<StandardMaterial>,
|
pub hex_material: Handle<StandardMaterial>,
|
||||||
|
/// Default material for walls
|
||||||
pub wall_material: Handle<StandardMaterial>,
|
pub wall_material: Handle<StandardMaterial>,
|
||||||
|
/// Custom materials mapped to specific colors from the RosePineDawn palette
|
||||||
pub custom_materials: HashMap<RosePineDawn, Handle<StandardMaterial>>,
|
pub custom_materials: HashMap<RosePineDawn, Handle<StandardMaterial>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MazeAssets {
|
impl MazeAssets {
|
||||||
|
/// Creates a new instance of MazeAssets with all necessary meshes and materials.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
meshes: &mut ResMut<Assets<Mesh>>,
|
meshes: &mut ResMut<Assets<Mesh>>,
|
||||||
materials: &mut ResMut<Assets<StandardMaterial>>,
|
materials: &mut ResMut<Assets<StandardMaterial>>,
|
||||||
@ -43,6 +60,7 @@ impl MazeAssets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a hexagonal mesh for floor tiles.
|
||||||
fn generate_hex_mesh(radius: f32, depth: f32) -> Mesh {
|
fn generate_hex_mesh(radius: f32, depth: f32) -> Mesh {
|
||||||
let hexagon = RegularPolygon {
|
let hexagon = RegularPolygon {
|
||||||
sides: HEX_SIDES,
|
sides: HEX_SIDES,
|
||||||
@ -54,6 +72,7 @@ fn generate_hex_mesh(radius: f32, depth: f32) -> Mesh {
|
|||||||
Mesh::from(prism_shape).rotated_by(rotation)
|
Mesh::from(prism_shape).rotated_by(rotation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a square mesh for wall segments.
|
||||||
fn generate_square_mesh(depth: f32, wall_size: f32) -> Mesh {
|
fn generate_square_mesh(depth: f32, wall_size: f32) -> Mesh {
|
||||||
let square = Rectangle::new(wall_size, wall_size);
|
let square = Rectangle::new(wall_size, wall_size);
|
||||||
let rectangular_prism = Extrusion::new(square, depth);
|
let rectangular_prism = Extrusion::new(square, depth);
|
||||||
@ -62,6 +81,7 @@ fn generate_square_mesh(depth: f32, wall_size: f32) -> Mesh {
|
|||||||
Mesh::from(rectangular_prism).rotated_by(rotation)
|
Mesh::from(rectangular_prism).rotated_by(rotation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a glowing white material for default tile appearance.
|
||||||
pub fn white_material() -> StandardMaterial {
|
pub fn white_material() -> StandardMaterial {
|
||||||
StandardMaterial {
|
StandardMaterial {
|
||||||
emissive: LinearRgba::new(
|
emissive: LinearRgba::new(
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
|
//! Maze components and configuration.
|
||||||
|
//!
|
||||||
|
//! Module defines the core components and configuration structures used
|
||||||
|
//! for maze generation and rendering, including hexagonal maze layouts,
|
||||||
|
//! tiles, walls, and maze configuration.
|
||||||
|
use super::GlobalMazeConfig;
|
||||||
use crate::floor::components::Floor;
|
use crate::floor::components::Floor;
|
||||||
|
|
||||||
use super::GlobalMazeConfig;
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use hexlab::Maze;
|
use hexlab::Maze;
|
||||||
use hexx::{Hex, HexLayout, HexOrientation};
|
use hexx::{Hex, HexLayout, HexOrientation};
|
||||||
@ -19,17 +24,27 @@ pub struct Tile;
|
|||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct Wall;
|
pub struct Wall;
|
||||||
|
|
||||||
|
/// Configuration for a single maze instance.
|
||||||
|
///
|
||||||
|
/// Contains all necessary parameters to generate and position a maze,
|
||||||
|
/// including its size, start/end positions, random seed, and layout.
|
||||||
#[derive(Debug, Reflect, Component, Clone)]
|
#[derive(Debug, Reflect, Component, Clone)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct MazeConfig {
|
pub struct MazeConfig {
|
||||||
|
/// Radius of the hexagonal maze
|
||||||
pub radius: u16,
|
pub radius: u16,
|
||||||
|
/// Starting position in hex coordinates
|
||||||
pub start_pos: Hex,
|
pub start_pos: Hex,
|
||||||
|
/// Ending position in hex coordinates
|
||||||
pub end_pos: Hex,
|
pub end_pos: Hex,
|
||||||
|
/// Random seed for maze generation
|
||||||
pub seed: u64,
|
pub seed: u64,
|
||||||
|
/// Layout configuration for hex-to-world space conversion
|
||||||
pub layout: HexLayout,
|
pub layout: HexLayout,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MazeConfig {
|
impl MazeConfig {
|
||||||
|
/// Creates a new maze configuration with the specified parameters.
|
||||||
fn new(
|
fn new(
|
||||||
radius: u16,
|
radius: u16,
|
||||||
orientation: HexOrientation,
|
orientation: HexOrientation,
|
||||||
@ -71,6 +86,7 @@ impl MazeConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the maze configuration with new global settings.
|
||||||
pub fn update(&mut self, global_conig: &GlobalMazeConfig) {
|
pub fn update(&mut self, global_conig: &GlobalMazeConfig) {
|
||||||
self.layout.hex_size = Vec2::splat(global_conig.hex_size);
|
self.layout.hex_size = Vec2::splat(global_conig.hex_size);
|
||||||
}
|
}
|
||||||
@ -88,6 +104,10 @@ impl Default for MazeConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a random position within a hexagonal radius.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A valid Hex coordinate within the specified radius
|
||||||
fn generate_pos<R: Rng>(radius: u16, rng: &mut R) -> Hex {
|
fn generate_pos<R: Rng>(radius: u16, rng: &mut R) -> Hex {
|
||||||
let radius = radius as i32;
|
let radius = radius as i32;
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ pub mod errors;
|
|||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod resources;
|
pub mod resources;
|
||||||
mod systems;
|
mod systems;
|
||||||
pub mod triggers;
|
mod triggers;
|
||||||
|
|
||||||
use bevy::{ecs::system::RunSystemOnce, prelude::*};
|
use bevy::{ecs::system::RunSystemOnce, prelude::*};
|
||||||
use components::HexMaze;
|
use components::HexMaze;
|
||||||
|
|||||||
@ -1,10 +1,23 @@
|
|||||||
use crate::maze::{
|
//! Common maze generation utilities.
|
||||||
components::MazeConfig,
|
use crate::maze::{components::MazeConfig, errors::MazeError};
|
||||||
errors::{MazeError, MazeResult},
|
|
||||||
};
|
|
||||||
use hexlab::prelude::*;
|
use hexlab::prelude::*;
|
||||||
|
|
||||||
pub fn generate_maze(config: &MazeConfig) -> MazeResult<Maze> {
|
/// Generates a new maze based on the provided configuration.
|
||||||
|
///
|
||||||
|
/// This function uses a recursive backtracking algorithm to generate
|
||||||
|
/// a hexagonal maze with the specified parameters.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// - `config` - Configuration parameters for maze generation including radius and seed.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `Result<Maze, MazeError>` - The generated maze or an error if generation fails.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `MazeError::GenerationFailed` if:
|
||||||
|
/// - The maze builder fails to create a valid maze
|
||||||
|
/// - The provided radius or seed results in an invalid configuration
|
||||||
|
pub fn generate_maze(config: &MazeConfig) -> Result<Maze, MazeError> {
|
||||||
MazeBuilder::new()
|
MazeBuilder::new()
|
||||||
.with_radius(config.radius)
|
.with_radius(config.radius)
|
||||||
.with_seed(config.seed)
|
.with_seed(config.seed)
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
|
//! Maze despawning functionality.
|
||||||
|
//!
|
||||||
|
//! Module handles the cleanup of maze entities when they need to be removed,
|
||||||
|
//! ensuring proper cleanup of both the maze and all its child entities.
|
||||||
use crate::{floor::components::Floor, maze::events::DespawnMaze};
|
use crate::{floor::components::Floor, maze::events::DespawnMaze};
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
pub(super) fn despawn_maze(
|
/// Despawns a maze and all its associated entities for a given floor.
|
||||||
|
pub fn despawn_maze(
|
||||||
trigger: Trigger<DespawnMaze>,
|
trigger: Trigger<DespawnMaze>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
query: Query<(Entity, &Floor)>,
|
query: Query<(Entity, &Floor)>,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
pub mod common;
|
pub mod common;
|
||||||
mod despawn;
|
mod despawn;
|
||||||
mod respawn;
|
mod respawn;
|
||||||
pub mod spawn;
|
mod spawn;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use despawn::despawn_maze;
|
use despawn::despawn_maze;
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
//! Maze respawning functionality.
|
||||||
|
//!
|
||||||
|
//! Module provides the ability to regenerate mazes for existing floors,
|
||||||
|
//! maintaining the same floor entity but replacing its internal maze structure.
|
||||||
|
|
||||||
use super::{common::generate_maze, spawn::spawn_maze_tiles};
|
use super::{common::generate_maze, spawn::spawn_maze_tiles};
|
||||||
use crate::{
|
use crate::{
|
||||||
floor::components::Floor,
|
floor::components::Floor,
|
||||||
@ -6,7 +11,15 @@ use crate::{
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use hexlab::Maze;
|
use hexlab::Maze;
|
||||||
|
|
||||||
pub(super) fn respawn_maze(
|
/// Respawns a maze for an existing floor with a new configuration.
|
||||||
|
///
|
||||||
|
/// # Behavior:
|
||||||
|
/// - Finds the target floor
|
||||||
|
/// - Generates a new maze configuration
|
||||||
|
/// - Cleans up existing maze tiles
|
||||||
|
/// - Spawns new maze tiles
|
||||||
|
/// - Updates the floor's configuration
|
||||||
|
pub fn respawn_maze(
|
||||||
trigger: Trigger<RespawnMaze>,
|
trigger: Trigger<RespawnMaze>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
|
//! Maze spawning and rendering functionality.
|
||||||
|
//!
|
||||||
|
//! Module handles the creation and visualization of hexagonal mazes.
|
||||||
|
|
||||||
use super::common::generate_maze;
|
use super::common::generate_maze;
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::FLOOR_Y_OFFSET,
|
constants::FLOOR_Y_OFFSET,
|
||||||
floor::components::{CurrentFloor, Floor},
|
floor::{
|
||||||
|
components::{CurrentFloor, Floor},
|
||||||
|
events::TransitionFloor,
|
||||||
|
},
|
||||||
maze::{
|
maze::{
|
||||||
assets::MazeAssets,
|
assets::MazeAssets,
|
||||||
components::{HexMaze, MazeConfig, Tile, Wall},
|
components::{HexMaze, MazeConfig, Tile, Wall},
|
||||||
@ -17,13 +24,15 @@ use hexlab::prelude::{Tile as HexTile, *};
|
|||||||
use hexx::HexOrientation;
|
use hexx::HexOrientation;
|
||||||
use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_6};
|
use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_6};
|
||||||
|
|
||||||
pub(crate) fn spawn_maze(
|
/// Spawns a new maze for the specified floor on [`SpawnMaze`] event.
|
||||||
|
pub fn spawn_maze(
|
||||||
trigger: Trigger<SpawnMaze>,
|
trigger: Trigger<SpawnMaze>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
maze_query: Query<(Entity, &Floor, &Maze)>,
|
maze_query: Query<(Entity, &Floor, &Maze)>,
|
||||||
global_config: Res<GlobalMazeConfig>,
|
global_config: Res<GlobalMazeConfig>,
|
||||||
|
mut event_writer: EventWriter<TransitionFloor>,
|
||||||
) {
|
) {
|
||||||
let SpawnMaze { floor, config } = trigger.event();
|
let SpawnMaze { floor, config } = trigger.event();
|
||||||
|
|
||||||
@ -40,9 +49,10 @@ pub(crate) fn spawn_maze(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate vertical offset based on floor number
|
||||||
let y_offset = match *floor {
|
let y_offset = match *floor {
|
||||||
1 => 0,
|
1 => 0, // Ground/Initial floor (floor 1) is at y=0
|
||||||
_ => FLOOR_Y_OFFSET,
|
_ => FLOOR_Y_OFFSET, // Other floors are offset vertically
|
||||||
} as f32;
|
} as f32;
|
||||||
|
|
||||||
let entity = commands
|
let entity = commands
|
||||||
@ -56,7 +66,7 @@ pub(crate) fn spawn_maze(
|
|||||||
Visibility::Visible,
|
Visibility::Visible,
|
||||||
StateScoped(Screen::Gameplay),
|
StateScoped(Screen::Gameplay),
|
||||||
))
|
))
|
||||||
.insert_if(CurrentFloor, || *floor == 1)
|
.insert_if(CurrentFloor, || *floor == 1) // Only floor 1 gets CurrentFloor
|
||||||
.id();
|
.id();
|
||||||
|
|
||||||
let assets = MazeAssets::new(&mut meshes, &mut materials, &global_config);
|
let assets = MazeAssets::new(&mut meshes, &mut materials, &global_config);
|
||||||
@ -68,8 +78,14 @@ pub(crate) fn spawn_maze(
|
|||||||
config,
|
config,
|
||||||
&global_config,
|
&global_config,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: find a better way to handle double event indirection
|
||||||
|
if *floor != 1 {
|
||||||
|
event_writer.send(TransitionFloor::Ascend);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawns all tiles for a maze as children of the parent maze entity
|
||||||
pub fn spawn_maze_tiles(
|
pub fn spawn_maze_tiles(
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
parent_entity: Entity,
|
parent_entity: Entity,
|
||||||
@ -85,6 +101,7 @@ pub fn spawn_maze_tiles(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawns a single hexagonal tile with appropriate transforms and materials
|
||||||
pub(super) fn spawn_single_hex_tile(
|
pub(super) fn spawn_single_hex_tile(
|
||||||
parent: &mut ChildBuilder,
|
parent: &mut ChildBuilder,
|
||||||
assets: &MazeAssets,
|
assets: &MazeAssets,
|
||||||
@ -98,6 +115,7 @@ pub(super) fn spawn_single_hex_tile(
|
|||||||
HexOrientation::Flat => Quat::from_rotation_y(FRAC_PI_6), // 30 degrees rotation
|
HexOrientation::Flat => Quat::from_rotation_y(FRAC_PI_6), // 30 degrees rotation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Select material based on tile position: start, end, or default
|
||||||
let material = match tile.pos() {
|
let material = match tile.pos() {
|
||||||
pos if pos == maze_config.start_pos => assets
|
pos if pos == maze_config.start_pos => assets
|
||||||
.custom_materials
|
.custom_materials
|
||||||
@ -123,12 +141,14 @@ pub(super) fn spawn_single_hex_tile(
|
|||||||
.with_children(|parent| spawn_walls(parent, assets, tile.walls(), global_config));
|
.with_children(|parent| spawn_walls(parent, assets, tile.walls(), global_config));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawns walls around a hexagonal tile based on the walls configuration
|
||||||
fn spawn_walls(
|
fn spawn_walls(
|
||||||
parent: &mut ChildBuilder,
|
parent: &mut ChildBuilder,
|
||||||
assets: &MazeAssets,
|
assets: &MazeAssets,
|
||||||
walls: &Walls,
|
walls: &Walls,
|
||||||
global_config: &GlobalMazeConfig,
|
global_config: &GlobalMazeConfig,
|
||||||
) {
|
) {
|
||||||
|
// Base rotation for wall alignment (90 degrees counter-clockwise)
|
||||||
let z_rotation = Quat::from_rotation_z(-FRAC_PI_2);
|
let z_rotation = Quat::from_rotation_z(-FRAC_PI_2);
|
||||||
let y_offset = global_config.height / 2.;
|
let y_offset = global_config.height / 2.;
|
||||||
|
|
||||||
@ -137,12 +157,25 @@ fn spawn_walls(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate the angle for this wall
|
||||||
|
// FRAC_PI_3 = 60 deg
|
||||||
|
// Negative because going clockwise
|
||||||
|
// i * 60 produces: 0, 60, 120, 180, 240, 300
|
||||||
let wall_angle = -FRAC_PI_3 * i as f32;
|
let wall_angle = -FRAC_PI_3 * i as f32;
|
||||||
|
|
||||||
|
// cos(angle) gives x coordinate on unit circle
|
||||||
|
// sin(angle) gives z coordinate on unit circle
|
||||||
|
// Multiply by wall_offset to get actual distance from center
|
||||||
let x_offset = global_config.wall_offset() * f32::cos(wall_angle);
|
let x_offset = global_config.wall_offset() * f32::cos(wall_angle);
|
||||||
let z_offset = global_config.wall_offset() * f32::sin(wall_angle);
|
let z_offset = global_config.wall_offset() * f32::sin(wall_angle);
|
||||||
|
|
||||||
|
// x: distance along x-axis from center
|
||||||
|
// y: vertical offset from center
|
||||||
|
// z: distance along z-axis from center
|
||||||
let pos = Vec3::new(x_offset, y_offset, z_offset);
|
let pos = Vec3::new(x_offset, y_offset, z_offset);
|
||||||
|
|
||||||
|
// 1. Rotate around x-axis to align wall with angle
|
||||||
|
// 2. Add FRAC_PI_2 (90) to make wall perpendicular to angle
|
||||||
let x_rotation = Quat::from_rotation_x(wall_angle + FRAC_PI_2);
|
let x_rotation = Quat::from_rotation_x(wall_angle + FRAC_PI_2);
|
||||||
let final_rotation = z_rotation * x_rotation;
|
let final_rotation = z_rotation * x_rotation;
|
||||||
|
|
||||||
@ -150,6 +183,7 @@ fn spawn_walls(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawns a single wall segment with the specified rotation and position
|
||||||
fn spawn_single_wall(parent: &mut ChildBuilder, assets: &MazeAssets, rotation: Quat, offset: Vec3) {
|
fn spawn_single_wall(parent: &mut ChildBuilder, assets: &MazeAssets, rotation: Quat, offset: Vec3) {
|
||||||
parent.spawn((
|
parent.spawn((
|
||||||
Name::new("Wall"),
|
Name::new("Wall"),
|
||||||
|
|||||||
@ -17,3 +17,32 @@ pub(super) fn blue_material() -> StandardMaterial {
|
|||||||
..default()
|
..default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Asset, Reflect, Clone)]
|
||||||
|
pub struct PlayerAssets {
|
||||||
|
// This #[dependency] attribute marks the field as a dependency of the Asset.
|
||||||
|
// This means that it will not finish loading until the labeled asset is also loaded.
|
||||||
|
#[dependency]
|
||||||
|
pub steps: Vec<Handle<AudioSource>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerAssets {
|
||||||
|
pub const PATH_STEP_1: &str = "audio/sound_effects/step1.ogg";
|
||||||
|
pub const PATH_STEP_2: &str = "audio/sound_effects/step2.ogg";
|
||||||
|
pub const PATH_STEP_3: &str = "audio/sound_effects/step3.ogg";
|
||||||
|
pub const PATH_STEP_4: &str = "audio/sound_effects/step4.ogg";
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromWorld for PlayerAssets {
|
||||||
|
fn from_world(world: &mut World) -> Self {
|
||||||
|
let assets = world.resource::<AssetServer>();
|
||||||
|
Self {
|
||||||
|
steps: vec![
|
||||||
|
assets.load(Self::PATH_STEP_1),
|
||||||
|
assets.load(Self::PATH_STEP_2),
|
||||||
|
assets.load(Self::PATH_STEP_3),
|
||||||
|
assets.load(Self::PATH_STEP_4),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,12 +4,16 @@ pub mod events;
|
|||||||
mod systems;
|
mod systems;
|
||||||
mod triggers;
|
mod triggers;
|
||||||
|
|
||||||
|
use assets::PlayerAssets;
|
||||||
use bevy::{ecs::system::RunSystemOnce, prelude::*};
|
use bevy::{ecs::system::RunSystemOnce, prelude::*};
|
||||||
use components::Player;
|
use components::Player;
|
||||||
use events::{DespawnPlayer, RespawnPlayer, SpawnPlayer};
|
use events::{DespawnPlayer, RespawnPlayer, SpawnPlayer};
|
||||||
|
|
||||||
|
use crate::asset_tracking::LoadResource;
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
pub(super) fn plugin(app: &mut App) {
|
||||||
app.register_type::<Player>()
|
app.register_type::<Player>()
|
||||||
|
.load_resource::<PlayerAssets>()
|
||||||
.add_event::<SpawnPlayer>()
|
.add_event::<SpawnPlayer>()
|
||||||
.add_event::<RespawnPlayer>()
|
.add_event::<RespawnPlayer>()
|
||||||
.add_event::<DespawnPlayer>()
|
.add_event::<DespawnPlayer>()
|
||||||
|
|||||||
@ -1,22 +1,31 @@
|
|||||||
mod input;
|
mod input;
|
||||||
mod movement;
|
mod movement;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
mod sound_effect;
|
||||||
mod vertical_transition;
|
mod vertical_transition;
|
||||||
|
|
||||||
use crate::screens::Screen;
|
use crate::{screens::Screen, AppSet};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use input::player_input;
|
use input::player_input;
|
||||||
use movement::player_movement;
|
use movement::player_movement;
|
||||||
|
use sound_effect::play_movement_sound;
|
||||||
use vertical_transition::handle_floor_transition;
|
use vertical_transition::handle_floor_transition;
|
||||||
|
|
||||||
|
use super::assets::PlayerAssets;
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
pub(super) fn plugin(app: &mut App) {
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
player_input,
|
player_input.in_set(AppSet::RecordInput),
|
||||||
player_movement.after(player_input),
|
player_movement,
|
||||||
handle_floor_transition,
|
handle_floor_transition.in_set(AppSet::RecordInput),
|
||||||
|
(play_movement_sound)
|
||||||
|
.chain()
|
||||||
|
.run_if(resource_exists::<PlayerAssets>)
|
||||||
|
.in_set(AppSet::Update),
|
||||||
)
|
)
|
||||||
|
.chain()
|
||||||
.run_if(in_state(Screen::Gameplay)),
|
.run_if(in_state(Screen::Gameplay)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,22 @@
|
|||||||
|
//! Player movement system and related utilities.
|
||||||
|
//!
|
||||||
|
//! This module handles smooth player movement between hexagonal tiles,
|
||||||
|
//! including position interpolation and movement completion detection.
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::MOVEMENT_THRESHOLD,
|
constants::MOVEMENT_THRESHOLD,
|
||||||
floor::components::CurrentFloor,
|
floor::components::CurrentFloor,
|
||||||
maze::components::MazeConfig,
|
maze::components::MazeConfig,
|
||||||
player::components::{CurrentPosition, MovementSpeed, MovementTarget, Player},
|
player::components::{CurrentPosition, MovementSpeed, MovementTarget, Player},
|
||||||
};
|
};
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use hexx::Hex;
|
use hexx::Hex;
|
||||||
|
|
||||||
pub(super) fn player_movement(
|
/// System handles player movement between hexagonal tiles.
|
||||||
|
///
|
||||||
|
/// Smoothly interpolates player position between hexagonal tiles,
|
||||||
|
/// handling movement target acquisition, position updates, and movement completion.
|
||||||
|
pub fn player_movement(
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
mut query: Query<
|
mut query: Query<
|
||||||
(
|
(
|
||||||
@ -48,10 +57,12 @@ pub(super) fn player_movement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determines if the movement should be completed based on proximity to target.
|
||||||
fn should_complete_movement(current_pos: Vec3, target_pos: Vec3) -> bool {
|
fn should_complete_movement(current_pos: Vec3, target_pos: Vec3) -> bool {
|
||||||
(target_pos - current_pos).length() < MOVEMENT_THRESHOLD
|
(target_pos - current_pos).length() < MOVEMENT_THRESHOLD
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the player's position based on movement parameters.
|
||||||
fn update_position(
|
fn update_position(
|
||||||
transform: &mut Transform,
|
transform: &mut Transform,
|
||||||
current_pos: Vec3,
|
current_pos: Vec3,
|
||||||
@ -69,6 +80,7 @@ fn update_position(
|
|||||||
transform.translation += movement;
|
transform.translation += movement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculates the world position for a given hex coordinate.
|
||||||
fn calculate_target_position(maze_config: &MazeConfig, target_hex: Hex, y: f32) -> Vec3 {
|
fn calculate_target_position(maze_config: &MazeConfig, target_hex: Hex, y: f32) -> Vec3 {
|
||||||
let world_pos = maze_config.layout.hex_to_world_pos(target_hex);
|
let world_pos = maze_config.layout.hex_to_world_pos(target_hex);
|
||||||
Vec3::new(world_pos.x, y, world_pos.y)
|
Vec3::new(world_pos.x, y, world_pos.y)
|
||||||
|
|||||||
31
src/player/systems/sound_effect.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use crate::{
|
||||||
|
audio::SoundEffect,
|
||||||
|
player::{
|
||||||
|
assets::PlayerAssets,
|
||||||
|
components::{MovementTarget, Player},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
|
||||||
|
pub fn play_movement_sound(
|
||||||
|
mut commands: Commands,
|
||||||
|
player_assets: Res<PlayerAssets>,
|
||||||
|
moving_players: Query<&MovementTarget, (Changed<MovementTarget>, With<Player>)>,
|
||||||
|
) {
|
||||||
|
for movement_target in moving_players.iter() {
|
||||||
|
if movement_target.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rng = &mut rand::thread_rng();
|
||||||
|
if let Some(random_step) = player_assets.steps.choose(rng) {
|
||||||
|
commands.spawn((
|
||||||
|
AudioPlayer(random_step.clone()),
|
||||||
|
PlaybackSettings::DESPAWN,
|
||||||
|
SoundEffect,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,8 @@
|
|||||||
use bevy::prelude::*;
|
//! Floor transition handling system.
|
||||||
|
//!
|
||||||
|
//! This module manages player transitions between different maze floors,
|
||||||
|
//! handling both ascending and descending movements based on player position
|
||||||
|
//! and input.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
floor::{
|
floor::{
|
||||||
@ -9,6 +13,12 @@ use crate::{
|
|||||||
player::components::{CurrentPosition, Player},
|
player::components::{CurrentPosition, Player},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
/// Handles floor transitions when a player reaches start/end positions.
|
||||||
|
///
|
||||||
|
/// System checks if the player is at a valid transition point (start or end position)
|
||||||
|
/// and triggers floor transitions when the appropriate input is received.
|
||||||
pub fn handle_floor_transition(
|
pub fn handle_floor_transition(
|
||||||
mut player_query: Query<&CurrentPosition, With<Player>>,
|
mut player_query: Query<&CurrentPosition, With<Player>>,
|
||||||
maze_query: Query<(&MazeConfig, &Floor), With<CurrentFloor>>,
|
maze_query: Query<(&MazeConfig, &Floor), With<CurrentFloor>>,
|
||||||
@ -25,13 +35,13 @@ pub fn handle_floor_transition(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for current_hex in player_query.iter_mut() {
|
for current_hex in player_query.iter_mut() {
|
||||||
// Check for ascending
|
// Check for ascending (at end position)
|
||||||
if current_hex.0 == config.end_pos {
|
if current_hex.0 == config.end_pos {
|
||||||
info!("Ascending");
|
info!("Ascending");
|
||||||
event_writer.send(TransitionFloor::Ascend);
|
event_writer.send(TransitionFloor::Ascend);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for descending
|
// Check for descending (at start position, not on first floor)
|
||||||
if current_hex.0 == config.start_pos && floor.0 != 1 {
|
if current_hex.0 == config.start_pos && floor.0 != 1 {
|
||||||
info!("Descending");
|
info!("Descending");
|
||||||
event_writer.send(TransitionFloor::Descend);
|
event_writer.send(TransitionFloor::Descend);
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
//! The screen state for the main gameplay.
|
//! The screen state for the main gameplay.
|
||||||
|
|
||||||
use bevy::{input::common_conditions::input_just_pressed, prelude::*};
|
|
||||||
|
|
||||||
use crate::maze::spawn_level_command;
|
use crate::maze::spawn_level_command;
|
||||||
use crate::player::spawn_player_command;
|
use crate::player::spawn_player_command;
|
||||||
use crate::{asset_tracking::LoadResource, audio::Music, screens::Screen};
|
use crate::screens::Screen;
|
||||||
|
|
||||||
|
use bevy::{input::common_conditions::input_just_pressed, prelude::*};
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
pub(super) fn plugin(app: &mut App) {
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
@ -12,10 +12,6 @@ pub(super) fn plugin(app: &mut App) {
|
|||||||
(spawn_level_command, spawn_player_command).chain(),
|
(spawn_level_command, spawn_player_command).chain(),
|
||||||
);
|
);
|
||||||
|
|
||||||
app.load_resource::<GameplayMusic>();
|
|
||||||
app.add_systems(OnEnter(Screen::Gameplay), play_gameplay_music);
|
|
||||||
app.add_systems(OnExit(Screen::Gameplay), stop_music);
|
|
||||||
|
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
return_to_title_screen
|
return_to_title_screen
|
||||||
@ -23,41 +19,6 @@ pub(super) fn plugin(app: &mut App) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Asset, Reflect, Clone)]
|
|
||||||
pub struct GameplayMusic {
|
|
||||||
#[dependency]
|
|
||||||
handle: Handle<AudioSource>,
|
|
||||||
entity: Option<Entity>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromWorld for GameplayMusic {
|
|
||||||
fn from_world(world: &mut World) -> Self {
|
|
||||||
let assets = world.resource::<AssetServer>();
|
|
||||||
Self {
|
|
||||||
handle: assets.load("audio/music/Fluffing A Duck.ogg"),
|
|
||||||
entity: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn play_gameplay_music(mut commands: Commands, mut music: ResMut<GameplayMusic>) {
|
|
||||||
music.entity = Some(
|
|
||||||
commands
|
|
||||||
.spawn((
|
|
||||||
AudioPlayer::<AudioSource>(music.handle.clone()),
|
|
||||||
PlaybackSettings::LOOP,
|
|
||||||
Music,
|
|
||||||
))
|
|
||||||
.id(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stop_music(mut commands: Commands, mut music: ResMut<GameplayMusic>) {
|
|
||||||
if let Some(entity) = music.entity.take() {
|
|
||||||
commands.entity(entity).despawn_recursive();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn return_to_title_screen(mut next_screen: ResMut<NextState<Screen>>) {
|
fn return_to_title_screen(mut next_screen: ResMut<NextState<Screen>>) {
|
||||||
next_screen.set(Screen::Title);
|
next_screen.set(Screen::Title);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,11 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
screens::{gameplay::GameplayMusic, Screen},
|
screens::Screen,
|
||||||
theme::{interaction::InteractionAssets, prelude::*},
|
theme::{interaction::InteractionAssets, prelude::*},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
app.add_systems(OnEnter(Screen::Loading), spawn_loading_screen);
|
app.add_systems(OnEnter(Screen::Loading), spawn_loading_screen);
|
||||||
|
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
@ -33,9 +33,6 @@ fn continue_to_title_screen(mut next_screen: ResMut<NextState<Screen>>) {
|
|||||||
next_screen.set(Screen::Title);
|
next_screen.set(Screen::Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn all_assets_loaded(
|
const fn all_assets_loaded(interaction_assets: Option<Res<InteractionAssets>>) -> bool {
|
||||||
interaction_assets: Option<Res<InteractionAssets>>,
|
interaction_assets.is_some()
|
||||||
gameplay_music: Option<Res<GameplayMusic>>,
|
|
||||||
) -> bool {
|
|
||||||
interaction_assets.is_some() && gameplay_music.is_some()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use bevy::{
|
|||||||
|
|
||||||
use crate::{screens::Screen, theme::prelude::*, AppSet};
|
use crate::{screens::Screen, theme::prelude::*, AppSet};
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
// Spawn splash screen.
|
// Spawn splash screen.
|
||||||
app.insert_resource(ClearColor(SPLASH_BACKGROUND_COLOR));
|
app.insert_resource(ClearColor(SPLASH_BACKGROUND_COLOR));
|
||||||
app.add_systems(OnEnter(Screen::Splash), spawn_splash_screen);
|
app.add_systems(OnEnter(Screen::Splash), spawn_splash_screen);
|
||||||
|
|||||||