diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 86db61e..5851f19 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,13 +21,17 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: Install dependencies 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 uses: Leafwing-Studios/cargo-cache@v2 with: sweep-cache: true - name: Run tests 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. clippy: name: Clippy diff --git a/assets/audio/music/Fluffing A Duck.ogg b/assets/audio/music/Fluffing A Duck.ogg deleted file mode 100644 index 2c88097..0000000 Binary files a/assets/audio/music/Fluffing A Duck.ogg and /dev/null differ diff --git a/assets/audio/music/Monkeys Spinning Monkeys.ogg b/assets/audio/music/Monkeys Spinning Monkeys.ogg deleted file mode 100644 index ddd190c..0000000 Binary files a/assets/audio/music/Monkeys Spinning Monkeys.ogg and /dev/null differ diff --git a/assets/images/ducky.png b/assets/images/ducky.png deleted file mode 100644 index 1cddde8..0000000 Binary files a/assets/images/ducky.png and /dev/null differ diff --git a/docs/design.md b/docs/design.md deleted file mode 100644 index 84008b7..0000000 --- a/docs/design.md +++ /dev/null @@ -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) -> EntityCommands; - fn header(&mut self, text: impl Into) -> EntityCommands; - fn label(&mut self, text: impl Into) -> EntityCommands; - fn text_input(&mut self, text: impl Into) -> EntityCommands; - fn image(&mut self, texture: Handle) -> 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>); - -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::(); - - 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::(); - app.init_resource::(); -} -``` - -And finally add a loading check to the `screens::loading::plugin`: - -```rust -fn all_assets_loaded( - image_handles: Res, -) -> 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 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, - 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, mut money: ResMut) { - 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( - new_state: S, -) -> impl Fn(Trigger, ResMut>) { - 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`](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 = 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. diff --git a/docs/img/readme-manual-setup.png b/docs/img/readme-manual-setup.png deleted file mode 100644 index e4a6099..0000000 Binary files a/docs/img/readme-manual-setup.png and /dev/null differ diff --git a/docs/img/thumbnail.png b/docs/img/thumbnail.png deleted file mode 100644 index abeaa5c..0000000 Binary files a/docs/img/thumbnail.png and /dev/null differ diff --git a/docs/img/workflow-dispatch-release.png b/docs/img/workflow-dispatch-release.png deleted file mode 100644 index f7a01f1..0000000 Binary files a/docs/img/workflow-dispatch-release.png and /dev/null differ diff --git a/docs/img/workflow-itch-release.png b/docs/img/workflow-itch-release.png deleted file mode 100644 index ab96972..0000000 Binary files a/docs/img/workflow-itch-release.png and /dev/null differ diff --git a/docs/img/workflow-ruleset.png b/docs/img/workflow-ruleset.png deleted file mode 100644 index 1469dcd..0000000 Binary files a/docs/img/workflow-ruleset.png and /dev/null differ diff --git a/docs/img/workflow-secrets.png b/docs/img/workflow-secrets.png deleted file mode 100644 index 3ecfa8f..0000000 Binary files a/docs/img/workflow-secrets.png and /dev/null differ diff --git a/docs/known-issues.md b/docs/known-issues.md deleted file mode 100644 index 68bcce8..0000000 --- a/docs/known-issues.md +++ /dev/null @@ -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. diff --git a/docs/tooling.md b/docs/tooling.md deleted file mode 100644 index 6ed70e2..0000000 --- a/docs/tooling.md +++ /dev/null @@ -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] ->
-> About the included rust-analyzer settings -> -> 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. ->
- -## 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. diff --git a/docs/workflows.md b/docs/workflows.md deleted file mode 100644 index 9d59766..0000000 --- a/docs/workflows.md +++ /dev/null @@ -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] ->
-> You may want to set up a GitHub ruleset to require that all commits to main pass CI. -> -> A screenshot showing a GitHub ruleset with status checks enabled ->
- -## 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). - -
- This workflow can also be triggered manually. - - 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. -
- -> [!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). - -
- Click here for a list of variables and how they're used. - - ```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 - ``` - -
- -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 - -
- In your GitHub repository, navigate to Settings > Secrets and variables > Actions. - - ![A screenshot showing where to add secrets in the GitHub Actions settings](./img/workflow-secrets.png) -
- -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) diff --git a/justfile b/justfile index d6e631b..eb95310 100644 --- a/justfile +++ b/justfile @@ -20,6 +20,7 @@ web-release: # Run tests 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 # Run CI localy @@ -29,4 +30,4 @@ ci: cargo fmt --all -- --check cargo clippy --workspace --all-targets --all-features -- --deny warnings cargo doc --workspace --all-features --document-private-items --no-deps - cargo test --workspace --no-default-features + just test diff --git a/src/floor/systems/movement.rs b/src/floor/systems/movement.rs index bb98dd7..526d2f8 100644 --- a/src/floor/systems/movement.rs +++ b/src/floor/systems/movement.rs @@ -10,6 +10,12 @@ use crate::{ 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( mut commands: Commands, mut maze_query: Query<(Entity, &mut Transform, &FloorYTarget), With>, @@ -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( mut commands: Commands, mut maze_query: Query<(Entity, &Transform, &Floor, Option<&FloorYTarget>), With>, diff --git a/src/floor/systems/spawn.rs b/src/floor/systems/spawn.rs index e59cfbb..9cd6c42 100644 --- a/src/floor/systems/spawn.rs +++ b/src/floor/systems/spawn.rs @@ -1,3 +1,5 @@ +use bevy::prelude::*; + use crate::{ floor::{ components::{CurrentFloor, Floor, FloorYTarget}, @@ -6,7 +8,6 @@ use crate::{ }, maze::{components::MazeConfig, events::SpawnMaze}, }; -use bevy::prelude::*; pub(super) fn spawn_floor( mut commands: Commands, @@ -19,7 +20,7 @@ pub(super) fn spawn_floor( }; 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"); return; } diff --git a/src/lib.rs b/src/lib.rs index 1a03a18..9ad5598 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,7 +53,7 @@ impl Plugin for AppPlugin { }) .set(AudioPlugin { global_volume: GlobalVolume { - volume: Volume::new(0.), + volume: Volume::new(0.2), }, ..default() }), @@ -84,7 +84,7 @@ enum AppSet { TickTimers, /// Record player input. RecordInput, - /// Do everything else (consider splitting this into further variants). + /// Do everything else. Update, } diff --git a/src/maze/assets.rs b/src/maze/assets.rs index 7b9f108..15350eb 100644 --- a/src/maze/assets.rs +++ b/src/maze/assets.rs @@ -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 crate::{ constants::WALL_OVERLAP_MODIFIER, theme::{palette::rose_pine::RosePineDawn, prelude::ColorScheme}, }; + use bevy::{prelude::*, utils::HashMap}; use std::f32::consts::FRAC_PI_2; use strum::IntoEnumIterator; @@ -10,15 +16,26 @@ use strum::IntoEnumIterator; const HEX_SIDES: u32 = 6; 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 { + /// Mesh for hexagonal floor tiles pub hex_mesh: Handle, + /// Mesh for wall segments pub wall_mesh: Handle, + /// Default material for hexagonal tiles pub hex_material: Handle, + /// Default material for walls pub wall_material: Handle, + /// Custom materials mapped to specific colors from the RosePineDawn palette pub custom_materials: HashMap>, } impl MazeAssets { + /// Creates a new instance of MazeAssets with all necessary meshes and materials. pub fn new( meshes: &mut ResMut>, materials: &mut ResMut>, @@ -43,6 +60,7 @@ impl MazeAssets { } } +/// Generates a hexagonal mesh for floor tiles. fn generate_hex_mesh(radius: f32, depth: f32) -> Mesh { let hexagon = RegularPolygon { sides: HEX_SIDES, @@ -54,6 +72,7 @@ fn generate_hex_mesh(radius: f32, depth: f32) -> Mesh { Mesh::from(prism_shape).rotated_by(rotation) } +/// Generates a square mesh for wall segments. fn generate_square_mesh(depth: f32, wall_size: f32) -> Mesh { let square = Rectangle::new(wall_size, wall_size); 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) } +/// Creates a glowing white material for default tile appearance. pub fn white_material() -> StandardMaterial { StandardMaterial { emissive: LinearRgba::new( diff --git a/src/maze/components.rs b/src/maze/components.rs index 0e3d2d2..537394e 100644 --- a/src/maze/components.rs +++ b/src/maze/components.rs @@ -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 super::GlobalMazeConfig; use bevy::prelude::*; use hexlab::Maze; use hexx::{Hex, HexLayout, HexOrientation}; @@ -19,17 +24,27 @@ pub struct Tile; #[reflect(Component)] 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)] #[reflect(Component)] pub struct MazeConfig { + /// Radius of the hexagonal maze pub radius: u16, + /// Starting position in hex coordinates pub start_pos: Hex, + /// Ending position in hex coordinates pub end_pos: Hex, + /// Random seed for maze generation pub seed: u64, + /// Layout configuration for hex-to-world space conversion pub layout: HexLayout, } impl MazeConfig { + /// Creates a new maze configuration with the specified parameters. fn new( radius: u16, orientation: HexOrientation, @@ -71,6 +86,7 @@ impl MazeConfig { } } + /// Updates the maze configuration with new global settings. pub fn update(&mut self, global_conig: &GlobalMazeConfig) { 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(radius: u16, rng: &mut R) -> Hex { let radius = radius as i32; loop { diff --git a/src/maze/mod.rs b/src/maze/mod.rs index e8f9481..5f2abc1 100644 --- a/src/maze/mod.rs +++ b/src/maze/mod.rs @@ -4,7 +4,7 @@ pub mod errors; pub mod events; pub mod resources; mod systems; -pub mod triggers; +mod triggers; use bevy::{ecs::system::RunSystemOnce, prelude::*}; use components::HexMaze; diff --git a/src/maze/triggers/common.rs b/src/maze/triggers/common.rs index 378d6c8..2ee978e 100644 --- a/src/maze/triggers/common.rs +++ b/src/maze/triggers/common.rs @@ -1,10 +1,23 @@ -use crate::maze::{ - components::MazeConfig, - errors::{MazeError, MazeResult}, -}; +//! Common maze generation utilities. +use crate::maze::{components::MazeConfig, errors::MazeError}; use hexlab::prelude::*; -pub fn generate_maze(config: &MazeConfig) -> MazeResult { +/// 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` - 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 { MazeBuilder::new() .with_radius(config.radius) .with_seed(config.seed) diff --git a/src/maze/triggers/despawn.rs b/src/maze/triggers/despawn.rs index e478c19..15f8700 100644 --- a/src/maze/triggers/despawn.rs +++ b/src/maze/triggers/despawn.rs @@ -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 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, mut commands: Commands, query: Query<(Entity, &Floor)>, diff --git a/src/maze/triggers/mod.rs b/src/maze/triggers/mod.rs index e7a2d2f..657f989 100644 --- a/src/maze/triggers/mod.rs +++ b/src/maze/triggers/mod.rs @@ -1,7 +1,7 @@ pub mod common; mod despawn; mod respawn; -pub mod spawn; +mod spawn; use bevy::prelude::*; use despawn::despawn_maze; diff --git a/src/maze/triggers/respawn.rs b/src/maze/triggers/respawn.rs index fd8f957..ac9bc47 100644 --- a/src/maze/triggers/respawn.rs +++ b/src/maze/triggers/respawn.rs @@ -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 crate::{ floor::components::Floor, @@ -6,7 +11,15 @@ use crate::{ use bevy::prelude::*; 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, mut commands: Commands, mut meshes: ResMut>, diff --git a/src/maze/triggers/spawn.rs b/src/maze/triggers/spawn.rs index c8ab16d..63e1c68 100644 --- a/src/maze/triggers/spawn.rs +++ b/src/maze/triggers/spawn.rs @@ -1,7 +1,14 @@ +//! Maze spawning and rendering functionality. +//! +//! Module handles the creation and visualization of hexagonal mazes. + use super::common::generate_maze; use crate::{ constants::FLOOR_Y_OFFSET, - floor::components::{CurrentFloor, Floor}, + floor::{ + components::{CurrentFloor, Floor}, + events::TransitionFloor, + }, maze::{ assets::MazeAssets, components::{HexMaze, MazeConfig, Tile, Wall}, @@ -17,13 +24,15 @@ use hexlab::prelude::{Tile as HexTile, *}; use hexx::HexOrientation; 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, mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, maze_query: Query<(Entity, &Floor, &Maze)>, global_config: Res, + mut event_writer: EventWriter, ) { 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 { - 1 => 0, - _ => FLOOR_Y_OFFSET, + 1 => 0, // Ground/Initial floor (floor 1) is at y=0 + _ => FLOOR_Y_OFFSET, // Other floors are offset vertically } as f32; let entity = commands @@ -56,7 +66,7 @@ pub(crate) fn spawn_maze( Visibility::Visible, StateScoped(Screen::Gameplay), )) - .insert_if(CurrentFloor, || *floor == 1) + .insert_if(CurrentFloor, || *floor == 1) // Only floor 1 gets CurrentFloor .id(); let assets = MazeAssets::new(&mut meshes, &mut materials, &global_config); @@ -68,8 +78,14 @@ pub(crate) fn spawn_maze( 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( commands: &mut Commands, 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( parent: &mut ChildBuilder, 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 }; + // Select material based on tile position: start, end, or default let material = match tile.pos() { pos if pos == maze_config.start_pos => assets .custom_materials @@ -123,12 +141,14 @@ pub(super) fn spawn_single_hex_tile( .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( parent: &mut ChildBuilder, assets: &MazeAssets, walls: &Walls, global_config: &GlobalMazeConfig, ) { + // Base rotation for wall alignment (90 degrees counter-clockwise) let z_rotation = Quat::from_rotation_z(-FRAC_PI_2); let y_offset = global_config.height / 2.; @@ -137,12 +157,25 @@ fn spawn_walls( 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; + // 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 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); + // 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 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) { parent.spawn(( Name::new("Wall"), diff --git a/src/player/assets.rs b/src/player/assets.rs index 0f8345a..4de7e08 100644 --- a/src/player/assets.rs +++ b/src/player/assets.rs @@ -17,3 +17,32 @@ pub(super) fn blue_material() -> StandardMaterial { ..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>, +} + +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::(); + 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), + ], + } + } +} diff --git a/src/player/mod.rs b/src/player/mod.rs index 480168d..6414c08 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -4,12 +4,16 @@ pub mod events; mod systems; mod triggers; +use assets::PlayerAssets; use bevy::{ecs::system::RunSystemOnce, prelude::*}; use components::Player; use events::{DespawnPlayer, RespawnPlayer, SpawnPlayer}; +use crate::asset_tracking::LoadResource; + pub(super) fn plugin(app: &mut App) { app.register_type::() + .load_resource::() .add_event::() .add_event::() .add_event::() diff --git a/src/player/systems/mod.rs b/src/player/systems/mod.rs index 9e892ea..ae65cb4 100644 --- a/src/player/systems/mod.rs +++ b/src/player/systems/mod.rs @@ -1,22 +1,31 @@ mod input; mod movement; pub mod setup; +mod sound_effect; mod vertical_transition; -use crate::screens::Screen; +use crate::{screens::Screen, AppSet}; use bevy::prelude::*; use input::player_input; use movement::player_movement; +use sound_effect::play_movement_sound; use vertical_transition::handle_floor_transition; +use super::assets::PlayerAssets; + pub(super) fn plugin(app: &mut App) { app.add_systems( Update, ( - player_input, - player_movement.after(player_input), - handle_floor_transition, + player_input.in_set(AppSet::RecordInput), + player_movement, + handle_floor_transition.in_set(AppSet::RecordInput), + (play_movement_sound) + .chain() + .run_if(resource_exists::) + .in_set(AppSet::Update), ) + .chain() .run_if(in_state(Screen::Gameplay)), ); } diff --git a/src/player/systems/movement.rs b/src/player/systems/movement.rs index d47b8dd..d69d55f 100644 --- a/src/player/systems/movement.rs +++ b/src/player/systems/movement.rs @@ -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::{ constants::MOVEMENT_THRESHOLD, floor::components::CurrentFloor, maze::components::MazeConfig, player::components::{CurrentPosition, MovementSpeed, MovementTarget, Player}, }; + use bevy::prelude::*; 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