Merge pull request #23 from kristoferssolo/feat/music

This commit is contained in:
Kristofers Solo 2025-01-05 21:03:50 +02:00 committed by GitHub
commit cbf3f7c835
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 257 additions and 648 deletions

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 B

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

View File

@ -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.

View File

@ -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.

View File

@ -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`:
![A screenshot showing a manually triggered workflow on GitHub Actions](./img/workflow-dispatch-release.png)
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>
![A screenshot showing where to add secrets in the GitHub Actions settings](./img/workflow-secrets.png)
</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".
![A screenshot showing a web build selected in the itch.io uploads](img/workflow-itch-release.png)

View File

@ -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

View File

@ -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>>,

View File

@ -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;
} }

View File

@ -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,
} }

View File

@ -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(

View File

@ -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 {

View File

@ -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;

View File

@ -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)

View File

@ -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)>,

View File

@ -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;

View File

@ -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>>,

View File

@ -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"),

View File

@ -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),
],
}
}
}

View File

@ -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>()

View File

@ -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)),
); );
} }

View File

@ -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)

View 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,
));
}
}
}

View File

@ -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);

View File

@ -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);
} }

View File

@ -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()
} }

View File

@ -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);