first committ, still fighting with... a lot! 😅
147
.cargo/config_fast_builds.toml
Normal file
|
@ -0,0 +1,147 @@
|
|||
# Copy this file to `config.toml` to speed up your builds.
|
||||
#
|
||||
# # Faster linker
|
||||
#
|
||||
# One of the slowest aspects of compiling large Rust programs is the linking time. This file configures an
|
||||
# alternate linker that may improve build times. When choosing a new linker, you have two options:
|
||||
#
|
||||
# ## LLD
|
||||
#
|
||||
# LLD is a linker from the LLVM project that supports Linux, Windows, MacOS, and WASM. It has the greatest
|
||||
# platform support and the easiest installation process. It is enabled by default in this file for Linux
|
||||
# and Windows. On MacOS, the default linker yields higher performance than LLD and is used instead.
|
||||
#
|
||||
# To install, please scroll to the corresponding table for your target (eg. `[target.x86_64-pc-windows-msvc]`
|
||||
# for Windows) and follow the steps under `LLD linker`.
|
||||
#
|
||||
# For more information, please see LLD's website at <https://lld.llvm.org>.
|
||||
#
|
||||
# ## Mold
|
||||
#
|
||||
# Mold is a newer linker written by one of the authors of LLD. It boasts even greater performance, specifically
|
||||
# through its high parallelism, though it only supports Linux.
|
||||
#
|
||||
# Mold is disabled by default in this file. If you wish to enable it, follow the installation instructions for
|
||||
# your corresponding target, disable LLD by commenting out its `-Clink-arg=...` line, and enable Mold by
|
||||
# *uncommenting* its `-Clink-arg=...` line.
|
||||
#
|
||||
# There is a fork of Mold named Sold that supports MacOS, but it is unmaintained and is about the same speed as
|
||||
# the default ld64 linker. For this reason, it is not included in this file.
|
||||
#
|
||||
# For more information, please see Mold's repository at <https://github.com/rui314/mold>.
|
||||
#
|
||||
# # Nightly configuration
|
||||
#
|
||||
# Be warned that the following features require nightly Rust, which is experimental and may contain bugs. If you
|
||||
# are having issues, skip this section and use stable Rust instead.
|
||||
#
|
||||
# There are a few unstable features that can improve performance. To use them, first install nightly Rust
|
||||
# through Rustup:
|
||||
#
|
||||
# ```
|
||||
# rustup toolchain install nightly
|
||||
# ```
|
||||
#
|
||||
# Finally, uncomment the lines under the `Nightly` heading for your corresponding target table (eg.
|
||||
# `[target.x86_64-unknown-linux-gnu]` for Linux) to enable the following features:
|
||||
#
|
||||
# ## `share-generics`
|
||||
#
|
||||
# Usually rustc builds each crate separately, then combines them all together at the end. `share-generics` forces
|
||||
# crates to share monomorphized generic code, so they do not duplicate work.
|
||||
#
|
||||
# In other words, instead of crate 1 generating `Foo<String>` and crate 2 generating `Foo<String>` separately,
|
||||
# only one crate generates `Foo<String>` and the other adds on to the pre-exiting work.
|
||||
#
|
||||
# Note that you may have some issues with this flag on Windows. If compiling fails due to the 65k symbol limit,
|
||||
# you may have to disable this setting. For more information and possible solutions to this error, see
|
||||
# <https://github.com/bevyengine/bevy/issues/1110>.
|
||||
#
|
||||
# ## `threads`
|
||||
#
|
||||
# This option enables rustc's parallel frontend, which improves performance when parsing, type checking, borrow
|
||||
# checking, and more. We currently set `threads=0`, which defaults to the amount of cores in your CPU.
|
||||
#
|
||||
# For more information, see the blog post at <https://blog.rust-lang.org/2023/11/09/parallel-rustc.html>.
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = [
|
||||
# LLD linker
|
||||
#
|
||||
# You may need to install it:
|
||||
#
|
||||
# - Ubuntu: `sudo apt-get install lld clang`
|
||||
# - Fedora: `sudo dnf install lld clang`
|
||||
# - Arch: `sudo pacman -S lld clang`
|
||||
"-Clink-arg=-fuse-ld=lld",
|
||||
|
||||
# Mold linker
|
||||
#
|
||||
# You may need to install it:
|
||||
#
|
||||
# - Ubuntu: `sudo apt-get install mold clang`
|
||||
# - Fedora: `sudo dnf install mold clang`
|
||||
# - Arch: `sudo pacman -S mold clang`
|
||||
# "-Clink-arg=-fuse-ld=/usr/bin/mold",
|
||||
|
||||
# Nightly
|
||||
# "-Zshare-generics=y",
|
||||
# "-Zthreads=0",
|
||||
]
|
||||
|
||||
[target.x86_64-apple-darwin]
|
||||
rustflags = [
|
||||
# LLD linker
|
||||
#
|
||||
# The default ld64 linker is faster, you should continue using it instead.
|
||||
#
|
||||
# You may need to install it:
|
||||
#
|
||||
# Brew: `brew install llvm`
|
||||
# Manually: <https://lld.llvm.org/MachO/index.html>
|
||||
# "-Clink-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld",
|
||||
|
||||
# Nightly
|
||||
# "-Zshare-generics=y",
|
||||
# "-Zthreads=0",
|
||||
]
|
||||
|
||||
[target.aarch64-apple-darwin]
|
||||
rustflags = [
|
||||
# LLD linker
|
||||
#
|
||||
# The default ld64 linker is faster, you should continue using it instead.
|
||||
#
|
||||
# You may need to install it:
|
||||
#
|
||||
# Brew: `brew install llvm`
|
||||
# Manually: <https://lld.llvm.org/MachO/index.html>
|
||||
# "-Clink-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld",
|
||||
|
||||
# Nightly
|
||||
# "-Zshare-generics=y",
|
||||
# "-Zthreads=0",
|
||||
]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
# LLD linker
|
||||
#
|
||||
# You may need to install it:
|
||||
#
|
||||
# ```
|
||||
# cargo install -f cargo-binutils
|
||||
# rustup component add llvm-tools
|
||||
# ```
|
||||
linker = "rust-lld.exe"
|
||||
rustdocflags = ["-Clinker=rust-lld.exe"]
|
||||
rustflags = [
|
||||
# Nightly
|
||||
# "-Zshare-generics=n", # This needs to be off if you use dynamic linking on Windows.
|
||||
# "-Zthreads=0",
|
||||
]
|
||||
|
||||
# Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only'
|
||||
# In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains.
|
||||
# [profile.dev]
|
||||
# debug = 1
|
15
.editorconfig
Normal file
|
@ -0,0 +1,15 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[.github/workflows/*.{yaml,yml}]
|
||||
indent_size = 2
|
180
.github/workflows/ci.yaml
vendored
Normal file
|
@ -0,0 +1,180 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Reduce compile time and cache size.
|
||||
RUSTFLAGS: -Dwarnings -Zshare-generics=y -Zthreads=0
|
||||
RUSTDOCFLAGS: -Dwarnings -Zshare-generics=y -Zthreads=0
|
||||
# Use the same Rust toolchain across jobs so they can share a cache.
|
||||
toolchain: nightly-2025-04-03
|
||||
|
||||
jobs:
|
||||
# Check formatting.
|
||||
format:
|
||||
name: Format
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.toolchain }}
|
||||
components: rustfmt
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
# Check documentation.
|
||||
docs:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.toolchain }}
|
||||
|
||||
- name: Restore Rust cache
|
||||
id: cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci
|
||||
save-if: false
|
||||
|
||||
- name: Install build dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev
|
||||
|
||||
- name: Check documentation
|
||||
run: cargo doc --locked --workspace --profile ci --all-features --document-private-items --no-deps
|
||||
|
||||
# Run Clippy lints.
|
||||
clippy-lints:
|
||||
name: Clippy lints
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.toolchain }}
|
||||
components: clippy
|
||||
|
||||
- name: Restore Rust cache
|
||||
id: cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install build dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev
|
||||
|
||||
- name: Run Clippy lints
|
||||
run: cargo clippy --locked --workspace --all-targets --profile ci --all-features
|
||||
|
||||
# Run Bevy lints.
|
||||
bevy-lints:
|
||||
name: Bevy lints
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain (plus bevy_lint)
|
||||
uses: TheBevyFlock/bevy_cli/bevy_lint@lint-v0.3.0
|
||||
|
||||
- name: Restore Rust cache
|
||||
id: cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci
|
||||
save-if: false
|
||||
|
||||
- name: Install build dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev
|
||||
|
||||
- name: Run Bevy lints
|
||||
run: bevy_lint --locked --workspace --all-targets --profile ci --all-features
|
||||
|
||||
# Run tests.
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up environment
|
||||
run: echo "RUSTFLAGS=${RUSTFLAGS:+$RUSTFLAGS }-Zcodegen-backend=cranelift" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.toolchain }}
|
||||
components: rustc-codegen-cranelift-preview
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: test
|
||||
cache-directories: ${{ env.LD_LIBRARY_PATH }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install build dependencies
|
||||
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --locked --workspace --all-targets --profile ci --no-fail-fast
|
||||
|
||||
# Check that the web build compiles.
|
||||
check-web:
|
||||
name: Check web
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.toolchain }}
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Restore Rust cache
|
||||
id: cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: web-ci
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install build dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev
|
||||
|
||||
- name: Check web
|
||||
run: cargo check --config 'profile.web.inherits="dev"' --profile ci --no-default-features --features dev --target wasm32-unknown-unknown
|
335
.github/workflows/release.yaml
vendored
Normal file
|
@ -0,0 +1,335 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
# Trigger this workflow when a tag is pushed in the format `v1.2.3`.
|
||||
push:
|
||||
tags:
|
||||
# Pattern syntax: <https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet>.
|
||||
- "v[0-9]+.[0-9]+.[0-9]+*"
|
||||
# Trigger this workflow manually via workflow dispatch.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version number in the format `v1.2.3`"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Configure the release workflow by editing the following values.
|
||||
env:
|
||||
# The base filename of the binary produced by `cargo build`.
|
||||
cargo_build_binary_name: bevy-jam-6
|
||||
|
||||
# The path to the assets directory.
|
||||
assets_path: assets
|
||||
|
||||
# Whether to build and package a release for a given target platform.
|
||||
build_for_web: true
|
||||
build_for_linux: true
|
||||
build_for_windows: true
|
||||
build_for_macos: true
|
||||
|
||||
# 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: RobertoMaurizzi/chain-reaction-collapse
|
||||
|
||||
############
|
||||
# ADVANCED #
|
||||
############
|
||||
|
||||
# The ID of the app produced by this workflow.
|
||||
# Applies to macOS releases.
|
||||
# Must contain only A-Z, a-z, 0-9, hyphen, and period: <https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier>.
|
||||
app_id: roberto-maurizzi.bevy-jam-6
|
||||
|
||||
# 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-jam-6
|
||||
|
||||
# The name of the `.zip` or `.dmg` file produced by this workflow.
|
||||
# Defaults to `app_binary_name` if commented out.
|
||||
#app_package_name: bevy-jam-6
|
||||
|
||||
# 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 Jam 6
|
||||
|
||||
# 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 Jam 6
|
||||
|
||||
# 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
|
||||
|
||||
# Enabling this only helps with consecutive releases to the same tag (and takes up cache storage space).
|
||||
# See: <https://github.com/orgs/community/discussions/27059>.
|
||||
use_github_cache: false
|
||||
|
||||
# Reduce compile time.
|
||||
RUSTFLAGS: -Dwarnings -Zshare-generics=y -Zthreads=0
|
||||
|
||||
jobs:
|
||||
# Forward some environment variables as outputs of this job.
|
||||
# This is needed because the `env` context can't be used in the `if:` condition of a job:
|
||||
# <https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability>
|
||||
forward-env:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Do nothing
|
||||
run: "true"
|
||||
outputs:
|
||||
upload_to_itch: ${{ env.upload_to_itch }}
|
||||
|
||||
# Determine the version number for this workflow.
|
||||
get-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Determine version number
|
||||
id: tag
|
||||
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "${GITHUB_OUTPUT}"
|
||||
outputs:
|
||||
# Use the input from workflow dispatch, or fall back to the git tag.
|
||||
version: ${{ inputs.version || steps.tag.outputs.tag }}
|
||||
|
||||
# Build and package a release for each platform.
|
||||
build:
|
||||
needs:
|
||||
- get-version
|
||||
env:
|
||||
version: ${{ needs.get-version.outputs.version }}
|
||||
# Avoid rate-limiting. See: <https://github.com/cargo-bins/cargo-binstall/issues/2045>.
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: web
|
||||
targets: wasm32-unknown-unknown
|
||||
package_ext: .zip
|
||||
runner: ubuntu-latest
|
||||
|
||||
- platform: linux
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
package_ext: .zip
|
||||
runner: ubuntu-latest
|
||||
|
||||
- platform: windows
|
||||
targets: x86_64-pc-windows-msvc
|
||||
binary_ext: .exe
|
||||
package_ext: .zip
|
||||
runner: windows-latest
|
||||
|
||||
- platform: macos
|
||||
targets: x86_64-apple-darwin aarch64-apple-darwin
|
||||
app_suffix: .app/Contents/MacOS
|
||||
package_ext: .dmg
|
||||
runner: macos-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
# Required to create a GitHub release: <https://docs.github.com/en/rest/releases/releases#create-a-release>.
|
||||
contents: write
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- name: Set up environment
|
||||
run: |
|
||||
# Default values:
|
||||
echo "app_binary_name=${app_binary_name:=${{ env.cargo_build_binary_name }}}" >> "${GITHUB_ENV}"
|
||||
echo "app_package_name=${app_package_name:=${app_binary_name}}" >> "${GITHUB_ENV}"
|
||||
echo "app_display_name=${app_display_name:=${app_package_name}}" >> "${GITHUB_ENV}"
|
||||
echo "app_short_name=${app_short_name:=${app_display_name}}" >> "${GITHUB_ENV}"
|
||||
|
||||
# File paths:
|
||||
echo "app=tmp/app/${app_package_name}"'${{ matrix.app_suffix }}' >> "${GITHUB_ENV}"
|
||||
echo "package=${app_package_name}-"'${{ matrix.platform }}${{ matrix.package_ext }}' >> "${GITHUB_ENV}"
|
||||
|
||||
# macOS environment:
|
||||
if [ '${{ matrix.platform }}' = 'macos' ]; then
|
||||
echo 'MACOSX_DEPLOYMENT_TARGET=11.0' >> "${GITHUB_ENV}" # macOS 11.0 Big Sur is the first version to support universal binaries.
|
||||
echo "SDKROOT=$(xcrun --sdk macosx --show-sdk-path)" >> "${GITHUB_ENV}"
|
||||
fi
|
||||
|
||||
# Check if building for this platform is enabled.
|
||||
echo 'is_platform_enabled=${{
|
||||
(matrix.platform == 'web' && env.build_for_web == 'true') ||
|
||||
(matrix.platform == 'linux' && env.build_for_linux == 'true') ||
|
||||
(matrix.platform == 'windows' && env.build_for_windows == 'true') ||
|
||||
(matrix.platform == 'macos' && env.build_for_macos == 'true')
|
||||
}}' >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Checkout repository
|
||||
if: ${{ env.is_platform_enabled == 'true' }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: ${{ env.git_lfs }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
if: ${{ env.is_platform_enabled == 'true' }}
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
targets: ${{ matrix.targets }}
|
||||
|
||||
- name: Restore Rust cache
|
||||
if: ${{ env.is_platform_enabled == 'true' && env.use_github_cache == 'true' }}
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: release
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install build dependencies (Linux)
|
||||
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'linux' }}
|
||||
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
|
||||
|
||||
- name: Prepare output directories
|
||||
if: ${{ env.is_platform_enabled == 'true' }}
|
||||
run: rm -rf tmp; mkdir -p tmp/binary '${{ env.app }}'
|
||||
|
||||
- name: Install cargo-binstall
|
||||
if: ${{ env.is_platform_enabled == 'true' }}
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Install Bevy CLI
|
||||
if: ${{ env.is_platform_enabled == 'true' }}
|
||||
run: cargo binstall --locked --no-confirm --force --git='https://github.com/TheBevyFlock/bevy_cli' bevy_cli
|
||||
|
||||
- name: Build and add web bundle to app (Web)
|
||||
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'web' }}
|
||||
run: |
|
||||
cargo binstall --locked --no-confirm --force wasm-bindgen-cli
|
||||
cargo binstall --locked --no-confirm --force wasm-opt
|
||||
bevy build --locked --release --features='${{ matrix.features }}' --yes web --bundle
|
||||
mv 'target/bevy_web/web-release/${{ env.cargo_build_binary_name }}' '${{ env.app }}'
|
||||
|
||||
- name: Build and add binaries to app (non-Web)
|
||||
if: ${{ env.is_platform_enabled == 'true' && matrix.platform != 'web' }}
|
||||
run: |
|
||||
for target in ${{ matrix.targets }}; do
|
||||
bevy build --locked --release --target="${target}" --features='${{ matrix.features }}'
|
||||
mv target/"${target}"/release/'${{ env.cargo_build_binary_name }}${{ matrix.binary_ext }}' tmp/binary/"${target}"'${{ matrix.binary_ext }}'
|
||||
done
|
||||
if [ '${{ matrix.platform }}' = 'macos' ]; then
|
||||
lipo tmp/binary/*'${{ matrix.binary_ext }}' -create -output '${{ env.app }}/${{ env.app_binary_name }}${{ matrix.binary_ext }}'
|
||||
else
|
||||
mv tmp/binary/*'${{ matrix.binary_ext }}' '${{ env.app }}/${{ env.app_binary_name }}${{ matrix.binary_ext }}'
|
||||
fi
|
||||
|
||||
- name: Add assets to app (non-Web)
|
||||
if: ${{ env.is_platform_enabled == 'true' && matrix.platform != 'web' }}
|
||||
run: cp -R ./'${{ env.assets_path }}' '${{ env.app }}' || true # Ignore error if assets folder does not exist.
|
||||
|
||||
- name: Add metadata to app (macOS)
|
||||
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'macos' }}
|
||||
run: |
|
||||
cat >'${{ env.app }}/../Info.plist' <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${{ env.app_display_name }}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${{ env.app_binary_name }}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${{ env.app_id }}</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${{ env.app_short_name }}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${{ env.version }}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${{ env.version }}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
- name: Package app (non-Windows)
|
||||
if: ${{ env.is_platform_enabled == 'true' && matrix.platform != 'windows' }}
|
||||
working-directory: tmp/app
|
||||
run: |
|
||||
if [ '${{ matrix.platform }}' = 'macos' ]; then
|
||||
ln -s /Applications .
|
||||
hdiutil create -fs HFS+ -volname '${{ env.app_package_name }}' -srcfolder . '${{ env.package }}'
|
||||
else
|
||||
zip --recurse-paths '${{ env.package }}' '${{ env.app_package_name }}'
|
||||
fi
|
||||
|
||||
- name: Package app (Windows)
|
||||
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'windows' }}
|
||||
working-directory: tmp/app
|
||||
shell: pwsh
|
||||
run: Compress-Archive -Path '${{ env.app_package_name }}' -DestinationPath '${{ env.package }}'
|
||||
|
||||
- name: Upload package to workflow artifacts
|
||||
if: ${{ env.is_platform_enabled == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: tmp/app/${{ env.package }}
|
||||
name: package-${{ matrix.platform }}
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload package to GitHub release
|
||||
if: ${{ env.is_platform_enabled == 'true' && env.upload_to_github == 'true' }}
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: tmp/app/${{ env.package }}
|
||||
asset_name: ${{ env.package }}
|
||||
release_name: ${{ env.version }}
|
||||
tag: ${{ env.version }}
|
||||
overwrite: true
|
||||
|
||||
# Upload all packages to itch.io.
|
||||
upload-to-itch:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- forward-env
|
||||
- get-version
|
||||
- build
|
||||
if: ${{ needs.forward-env.outputs.upload_to_itch != '' }}
|
||||
|
||||
steps:
|
||||
- name: Download all packages
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: package-*
|
||||
path: tmp
|
||||
|
||||
- name: Install butler
|
||||
run: |
|
||||
curl -L -o butler.zip 'https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default'
|
||||
unzip butler.zip
|
||||
chmod +x butler
|
||||
./butler -V
|
||||
|
||||
- name: Upload all packages to itch.io
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
run: |
|
||||
for channel in $(ls tmp); do
|
||||
./butler push \
|
||||
--fix-permissions \
|
||||
--userversion='${{ needs.get-version.outputs.version }}' \
|
||||
tmp/"${channel}"/* \
|
||||
'${{ env.upload_to_itch }}':"${channel#package-}"
|
||||
done
|
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Rust builds
|
||||
/target
|
||||
# This file contains environment-specific configuration like linker settings
|
||||
.cargo/config.toml
|
8
.idea/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
20
.idea/runConfigurations/Run_Native_Debug.xml
generated
Normal file
|
@ -0,0 +1,20 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Native Debug" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<!-- We use -\-no-default-features here because otherwise debugging will fail with dynamic linking.-->
|
||||
<option name="command" value="run --no-default-features" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
<option name="requiredFeatures" value="true" />
|
||||
<option name="allFeatures" value="false" />
|
||||
<option name="withSudo" value="false" />
|
||||
<option name="buildTarget" value="REMOTE" />
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
<method v="2">
|
||||
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
19
.idea/runConfigurations/Run_Native_dev.xml
generated
Normal file
|
@ -0,0 +1,19 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Native dev" type="ShConfigurationType" folderName="Build">
|
||||
<option name="SCRIPT_TEXT" value="bevy run" />
|
||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
||||
<option name="SCRIPT_PATH" value="" />
|
||||
<option name="SCRIPT_OPTIONS" value="" />
|
||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
||||
<option name="INTERPRETER_PATH" value="/bin/bash" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
||||
<option name="EXECUTE_SCRIPT_FILE" value="false" />
|
||||
<envs>
|
||||
<env name="RUST_BACKTRACE" value="full" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
17
.idea/runConfigurations/Run_Native_release.xml
generated
Normal file
|
@ -0,0 +1,17 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Native release" type="ShConfigurationType" folderName="Build">
|
||||
<option name="SCRIPT_TEXT" value="bevy run --release" />
|
||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
||||
<option name="SCRIPT_PATH" value="" />
|
||||
<option name="SCRIPT_OPTIONS" value="" />
|
||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
||||
<option name="INTERPRETER_PATH" value="/bin/bash" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
||||
<option name="EXECUTE_SCRIPT_FILE" value="false" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
19
.idea/runConfigurations/Run_Web_dev.xml
generated
Normal file
|
@ -0,0 +1,19 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Web dev" type="ShConfigurationType" folderName="Build">
|
||||
<option name="SCRIPT_TEXT" value="bevy run --yes web" />
|
||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
||||
<option name="SCRIPT_PATH" value="" />
|
||||
<option name="SCRIPT_OPTIONS" value="" />
|
||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
||||
<option name="INTERPRETER_PATH" value="/bin/bash" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
||||
<option name="EXECUTE_SCRIPT_FILE" value="false" />
|
||||
<envs>
|
||||
<env name="RUST_BACKTRACE" value="full" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
17
.idea/runConfigurations/Run_Web_release.xml
generated
Normal file
|
@ -0,0 +1,17 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Web release" type="ShConfigurationType" folderName="Build">
|
||||
<option name="SCRIPT_TEXT" value="bevy run --yes --release web" />
|
||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
||||
<option name="SCRIPT_PATH" value="" />
|
||||
<option name="SCRIPT_OPTIONS" value="" />
|
||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
||||
<option name="INTERPRETER_PATH" value="/bin/bash" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
||||
<option name="EXECUTE_SCRIPT_FILE" value="false" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
68
.vscode/bevy.code-snippets
vendored
Normal file
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"Bevy: New top-level function Plugin": {
|
||||
"scope": "rust",
|
||||
"prefix": "plugin",
|
||||
"body": [
|
||||
"use bevy::prelude::*;",
|
||||
"",
|
||||
"pub(super) fn plugin(app: &mut App) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"Bevy: New Component": {
|
||||
"scope": "rust",
|
||||
"prefix": "component",
|
||||
"body": [
|
||||
"#[derive(Component, Reflect, Debug)]",
|
||||
"#[reflect(Component)]",
|
||||
"struct $1;"
|
||||
]
|
||||
},
|
||||
"Bevy: New Resource": {
|
||||
"scope": "rust",
|
||||
"prefix": "resource",
|
||||
"body": [
|
||||
"#[derive(Resource, Reflect, Debug, Default)]",
|
||||
"#[reflect(Resource)]",
|
||||
"struct $1;"
|
||||
]
|
||||
},
|
||||
"Bevy: New Event": {
|
||||
"scope": "rust",
|
||||
"prefix": "event",
|
||||
"body": [
|
||||
"#[derive(Event, Debug)]",
|
||||
"struct $1;"
|
||||
]
|
||||
},
|
||||
"Bevy: New SystemSet": {
|
||||
"scope": "rust",
|
||||
"prefix": "systemset",
|
||||
"body": [
|
||||
"#[derive(SystemSet, Copy, Clone, Eq, PartialEq, Hash, Debug)]",
|
||||
"enum $1 {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"Bevy: New Schedule": {
|
||||
"scope": "rust",
|
||||
"prefix": "schedule",
|
||||
"body": [
|
||||
"#[derive(ScheduleLabel, Copy, Clone, Eq, PartialEq, Hash, Debug)]",
|
||||
"struct $1;"
|
||||
]
|
||||
},
|
||||
"Bevy: New States": {
|
||||
"scope": "rust",
|
||||
"prefix": "states",
|
||||
"body": [
|
||||
"#[derive(States, Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]",
|
||||
"enum $1 {",
|
||||
"\t#[default]",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
9
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"fill-labs.dependi",
|
||||
"editorconfig.editorconfig",
|
||||
"tamasfe.even-better-toml",
|
||||
"rust-lang.rust-analyzer",
|
||||
"a5huynh.vscode-ron"
|
||||
]
|
||||
}
|
9
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
// Allow `rust-analyzer` and `cargo` to run simultaneously.
|
||||
// This uses extra storage space, so consider commenting it out.
|
||||
"rust-analyzer.cargo.targetDir": true,
|
||||
// Display the directory of `mod.rs` files in the tab above the text editor.
|
||||
"workbench.editor.customLabels.patterns": {
|
||||
"**/mod.rs": "${dirname}/mod.rs"
|
||||
},
|
||||
}
|
84
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Run native dev",
|
||||
"type": "process",
|
||||
"command": "bevy",
|
||||
"args": [
|
||||
"run"
|
||||
],
|
||||
"options": {
|
||||
"env": {
|
||||
"RUST_BACKTRACE": "full"
|
||||
}
|
||||
},
|
||||
"presentation": {
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run native release",
|
||||
"type": "process",
|
||||
"command": "bevy",
|
||||
"args": [
|
||||
"run",
|
||||
"--release"
|
||||
],
|
||||
"presentation": {
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "Run web dev",
|
||||
"type": "process",
|
||||
"command": "bevy",
|
||||
"args": [
|
||||
"run",
|
||||
"--yes",
|
||||
"web"
|
||||
],
|
||||
"options": {
|
||||
"env": {
|
||||
"RUST_BACKTRACE": "full"
|
||||
}
|
||||
},
|
||||
"presentation": {
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
],
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "Run web release",
|
||||
"type": "process",
|
||||
"command": "bevy",
|
||||
"args": [
|
||||
"run",
|
||||
"--yes",
|
||||
"--release",
|
||||
"web"
|
||||
],
|
||||
"presentation": {
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
],
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
6352
Cargo.lock
generated
Normal file
129
Cargo.toml
Normal file
|
@ -0,0 +1,129 @@
|
|||
[package]
|
||||
name = "chain-reaction-collapse"
|
||||
authors = ["RobertoMaurizzi <roberto.maurizzi@gmail.com>"]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bevy = { version = "0.16", features = ["wayland"] }
|
||||
rand = "0.8"
|
||||
# Compile low-severity logs out of native builds for performance.
|
||||
log = { version = "0.4", features = [
|
||||
"max_level_debug",
|
||||
"release_max_level_warn",
|
||||
] }
|
||||
# Compile low-severity logs out of web builds for performance.
|
||||
tracing = { version = "0.1", features = [
|
||||
"max_level_debug",
|
||||
"release_max_level_warn",
|
||||
] }
|
||||
bevy_ecs_ldtk = "0.12.0"
|
||||
bevy-inspector-egui = { version = "0.31.0", optional = true }
|
||||
|
||||
# Your web builds will start failing if you add a dependency that pulls in `getrandom` v0.3+.
|
||||
# To fix this, you should tell `getrandom` to use the `wasm_js` backend on Wasm.
|
||||
# See: <https://docs.rs/getrandom/0.3.3/getrandom/#webassembly-support>.
|
||||
#[target.wasm32-unknown-unknown.dependencies]
|
||||
#getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
# In addition to enabling the `wasm_js` feature, you need to include `--cfg 'getrandom_backend="wasm_js"'`
|
||||
# in your rustflags for both local and CI/CD web builds, taking into account that rustflags specified in
|
||||
# multiple places are NOT combined (see <https://github.com/rust-lang/cargo/issues/5376>).
|
||||
# Alternatively, you can opt out of the rustflags check with this patch:
|
||||
#[patch.crates-io]
|
||||
#getrandom = { git = "https://github.com/benfrankel/getrandom" }
|
||||
|
||||
[features]
|
||||
# Default to a native dev build.
|
||||
default = ["dev_native"]
|
||||
dev = [
|
||||
# Improve compile times for dev builds by linking Bevy as a dynamic library.
|
||||
"bevy/dynamic_linking",
|
||||
"bevy/bevy_dev_tools",
|
||||
"bevy/bevy_ui_debug",
|
||||
# Improve error messages coming from Bevy
|
||||
"bevy/track_location",
|
||||
]
|
||||
dev_native = [
|
||||
"dev",
|
||||
# Enable asset hot reloading for native dev builds.
|
||||
"bevy/file_watcher",
|
||||
# Enable embedded asset hot reloading for native dev builds.
|
||||
"bevy/embedded_watcher",
|
||||
]
|
||||
inspector = ["bevy-inspector-egui"]
|
||||
|
||||
|
||||
[package.metadata.bevy_cli.release]
|
||||
# Disable dev features for release builds.
|
||||
default-features = false
|
||||
|
||||
[package.metadata.bevy_cli.web]
|
||||
# Disable native features for web builds.
|
||||
default-features = false
|
||||
|
||||
[package.metadata.bevy_cli.web.dev]
|
||||
features = ["dev"]
|
||||
|
||||
|
||||
[lints.rust]
|
||||
# Mark `bevy_lint` as a valid `cfg`, as it is set when the Bevy linter runs.
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(bevy_lint)"] }
|
||||
|
||||
[lints.clippy]
|
||||
# Bevy supplies arguments to systems via dependency injection, so it's natural for systems to
|
||||
# request more than 7 arguments, which would undesirably trigger this lint.
|
||||
too_many_arguments = "allow"
|
||||
# Queries may access many components, which would undesirably trigger this lint.
|
||||
type_complexity = "allow"
|
||||
# Make sure macros use their standard braces, such as `[]` for `bevy_ecs::children!`.
|
||||
nonstandard_macro_braces = "warn"
|
||||
|
||||
# You can configure the warning levels of Bevy lints here. For a list of all lints, see:
|
||||
# <https://thebevyflock.github.io/bevy_cli/bevy_lint/lints/>
|
||||
[package.metadata.bevy_lint]
|
||||
# panicking_methods = "deny"
|
||||
# pedantic = "warn"
|
||||
|
||||
|
||||
# Compile with Performance Optimizations:
|
||||
# <https://bevyengine.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations>
|
||||
|
||||
# Enable a small amount of optimization in the dev profile.
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
# Enable a large amount of optimization in the dev profile for dependencies.
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
# Remove expensive debug assertions due to <https://github.com/bevyengine/bevy/issues/14291>
|
||||
[profile.dev.package.wgpu-types]
|
||||
debug-assertions = false
|
||||
|
||||
[profile.release]
|
||||
# Compile the entire crate as one unit.
|
||||
# Slows compile times, marginal improvements.
|
||||
codegen-units = 1
|
||||
# Do a second optimization pass over the entire program, including dependencies.
|
||||
# Slows compile times, marginal improvements.
|
||||
lto = "thin"
|
||||
|
||||
# This profile will be used by `bevy run web` automatically.
|
||||
[profile.web-release]
|
||||
# Default to release profile values.
|
||||
inherits = "release"
|
||||
# Optimize with size in mind (also try "z", sometimes it is better).
|
||||
# Slightly slows compile times, great improvements to file size and runtime performance.
|
||||
opt-level = "s"
|
||||
# Strip all debugging information from the binary to slightly reduce file size.
|
||||
strip = "debuginfo"
|
||||
|
||||
# Optimize for build time in CI.
|
||||
[profile.ci]
|
||||
inherits = "dev"
|
||||
opt-level = 0
|
||||
debug = "line-tables-only"
|
||||
codegen-units = 4
|
||||
|
||||
[profile.ci.package."*"]
|
||||
opt-level = 0
|
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Chain Reaction Collapse
|
||||
|
||||
This project was generated using the [Bevy New 2D](https://github.com/TheBevyFlock/bevy_new_2d) template.
|
||||
Check out the [documentation](https://github.com/TheBevyFlock/bevy_new_2d/blob/main/README.md) to get started!
|
BIN
assets/atlas/MV Icons Complete Sheet Free - ALL.png
Executable file
After Width: | Height: | Size: 405 KiB |
BIN
assets/atlas/NuclearBlaze_by_deepnight.png
Normal file
After Width: | Height: | Size: 8 KiB |
BIN
assets/atlas/SunnyLand-player.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
assets/atlas/SunnyLand_by_Ansimuz-extended.png
Executable file
After Width: | Height: | Size: 21 KiB |
BIN
assets/audio/music/Fluffing A Duck.ogg
Normal file
BIN
assets/audio/music/Monkeys Spinning Monkeys.ogg
Normal file
BIN
assets/audio/sound_effects/button_click.ogg
Normal file
BIN
assets/audio/sound_effects/button_hover.ogg
Normal file
BIN
assets/audio/sound_effects/step1.ogg
Normal file
BIN
assets/audio/sound_effects/step2.ogg
Normal file
BIN
assets/audio/sound_effects/step3.ogg
Normal file
BIN
assets/audio/sound_effects/step4.ogg
Normal file
BIN
assets/images/SunnyLand-player.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
assets/images/ducky.png
Normal file
After Width: | Height: | Size: 956 B |
BIN
assets/images/researcher.png
Normal file
After Width: | Height: | Size: 256 KiB |
BIN
assets/images/splash.png
Normal file
After Width: | Height: | Size: 15 KiB |
2506
assets/levels/collectathon.ldtk
Normal file
5695
assets/levels/world.ldtk
Normal file
BIN
assets/spritesheets/backslash.png
Normal file
After Width: | Height: | Size: 16 KiB |
222
assets/spritesheets/credits.json
Normal file
|
@ -0,0 +1,222 @@
|
|||
{
|
||||
"colors": {
|
||||
"name": "Liberated Palette",
|
||||
"authors": [
|
||||
"Eliza Wyatt (DeathsDarling)"
|
||||
],
|
||||
"urls": [
|
||||
"https://github.com/ElizaWy/LPC/wiki/Style-Guide#color-palette"
|
||||
],
|
||||
"licenses": [
|
||||
"OGA-BY 3.0"
|
||||
],
|
||||
"notes": [
|
||||
"Original palette made for the LPC set, by Eliza Wyatt."
|
||||
]
|
||||
},
|
||||
"anatomy.body.male": {
|
||||
"notes": [
|
||||
"see details at https://opengameart.org/content/lpc-character-bases",
|
||||
"2023-12 [napsio]: minimized colors to 6 for each material and fixed broken colors"
|
||||
],
|
||||
"authors": [
|
||||
"napsio",
|
||||
"bluecarrot16",
|
||||
"Benjamin K. Smith (BenCreating)",
|
||||
"Evert",
|
||||
"Eliza Wyatt (ElizaWy)",
|
||||
"TheraHedwig",
|
||||
"MuffinElZangano",
|
||||
"Durrani",
|
||||
"Johannes Sjölund (wulax)",
|
||||
"Stephen Challener (Redshrike)"
|
||||
],
|
||||
"licenses": [
|
||||
"CC-BY-SA 3.0",
|
||||
"GPL 3.0"
|
||||
],
|
||||
"urls": [
|
||||
"https://opengameart.org/content/lpc-jump-expanded",
|
||||
"https://opengameart.org/content/lpc-revised-character-basics",
|
||||
"https://opengameart.org/content/liberated-pixel-cup-lpc-base-assets-sprites-map-tiles",
|
||||
"https://opengameart.org/content/lpc-be-seated",
|
||||
"https://opengameart.org/content/lpc-runcycle-for-male-muscular-and-pregnant-character-bases-with-modular-heads",
|
||||
"https://opengameart.org/content/lpc-medieval-fantasy-character-sprites",
|
||||
"https://opengameart.org/content/lpc-character-bases",
|
||||
"https://opengameart.org/content/lpc-male-jumping-animation-by-durrani",
|
||||
"https://opengameart.org/content/lpc-runcycle-and-diagonal-walkcycle"
|
||||
],
|
||||
"name": "Male"
|
||||
},
|
||||
"anatomy.head.human_male": {
|
||||
"notes": [
|
||||
"original head by Redshrike",
|
||||
"tweaks by BenCreating",
|
||||
"modular version by bluecarrot16",
|
||||
"2023-12 [napsio]: minimized colors to 6 for each material and fixed broken colors"
|
||||
],
|
||||
"authors": [
|
||||
"napsio",
|
||||
"bluecarrot16",
|
||||
"Benjamin K. Smith (BenCreating)",
|
||||
"Stephen Challener (Redshrike)"
|
||||
],
|
||||
"licenses": [
|
||||
"OGA-BY 3.0",
|
||||
"CC-BY-SA 3.0",
|
||||
"GPL 3.0"
|
||||
],
|
||||
"urls": [
|
||||
"https://opengameart.org/content/liberated-pixel-cup-lpc-base-assets-sprites-map-tiles",
|
||||
"https://opengameart.org/content/lpc-character-bases"
|
||||
],
|
||||
"name": "Human Male"
|
||||
},
|
||||
"anatomy.nose.straight": {
|
||||
"notes": [
|
||||
"2023-12 [napsio]: minimized colors to 6 for each material and fixed broken colors"
|
||||
],
|
||||
"authors": [
|
||||
"napsio",
|
||||
"Thane Brimhall (pennomi)",
|
||||
"Matthew Krohn (makrohn)"
|
||||
],
|
||||
"licenses": [
|
||||
"GPL 3.0",
|
||||
"CC-BY-SA 3.0"
|
||||
],
|
||||
"urls": [
|
||||
"https://opengameart.org/content/lpc-base-character-expressions"
|
||||
],
|
||||
"name": "Straight"
|
||||
},
|
||||
"anatomy.hair.messy3": {
|
||||
"notes": [
|
||||
"2023-12 [napsio]: minimized colors to 6 for each material and fixed broken colors"
|
||||
],
|
||||
"authors": [
|
||||
"napsio",
|
||||
"Fabzy",
|
||||
"bluecarrot16"
|
||||
],
|
||||
"licenses": [
|
||||
"CC-BY-SA 3.0"
|
||||
],
|
||||
"urls": [
|
||||
"https://opengameart.org/content/lpc-hair",
|
||||
"https://opengameart.org/content/the-revolution-hair"
|
||||
],
|
||||
"name": "Messy3"
|
||||
},
|
||||
"clothes.torso.longsleeve": {
|
||||
"notes": [
|
||||
"original by ElizaWy",
|
||||
"edited to v3 bases by bluecarrot16",
|
||||
"original by wulax",
|
||||
"recolors and cleanup by JaidynReiman",
|
||||
"further recolors by bluecarrot16",
|
||||
"edited to pregnant v3 bases by bluecarrot16",
|
||||
"teen body by Redshrike",
|
||||
"teen shirt by ElizaWy derived from base",
|
||||
"teen edited by bluecarrot16 to v3 bases",
|
||||
"2023-12 [napsio]: minimized colors to 6 for each material and fixed broken colors"
|
||||
],
|
||||
"authors": [
|
||||
"napsio",
|
||||
"bluecarrot16",
|
||||
"ElizaWy",
|
||||
"Stephen Challener (Redshrike)",
|
||||
"David Conway Jr. (JaidynReiman)",
|
||||
"Johannes Sjölund (wulax)"
|
||||
],
|
||||
"licenses": [
|
||||
"OGA-BY 3.0",
|
||||
"CC-BY-SA 3.0",
|
||||
"GPL 3.0"
|
||||
],
|
||||
"urls": [
|
||||
"https://opengameart.org/content/lpc-7-womens-shirts",
|
||||
"https://opengameart.org/content/liberated-pixel-cup-lpc-base-assets-sprites-map-tiles",
|
||||
"https://opengameart.org/content/lpc-medieval-fantasy-character-sprites",
|
||||
"http://opengameart.org/content/lpc-clothing-updates"
|
||||
],
|
||||
"name": "Longsleeve"
|
||||
},
|
||||
"clothes.torso3.collared": {
|
||||
"notes": [
|
||||
"2023-12 [napsio]: minimized colors to 6 for each material and fixed broken colors"
|
||||
],
|
||||
"authors": [
|
||||
"napsio",
|
||||
"bluecarrot16"
|
||||
],
|
||||
"licenses": [
|
||||
"OGA-BY 3.0",
|
||||
"GPL 3.0"
|
||||
],
|
||||
"urls": [
|
||||
"https://opengameart.org/content/lpc-gentleman",
|
||||
"https://opengameart.org/content/lpc-pirates"
|
||||
],
|
||||
"name": "Collared"
|
||||
},
|
||||
"clothes.feet.shoes": {
|
||||
"notes": [
|
||||
"original by wulax",
|
||||
"edited for female base by Joe White",
|
||||
"edited for v3 base by bluecarrot16",
|
||||
"2023-12 [napsio]: minimized colors to 6 for each material and fixed broken colors"
|
||||
],
|
||||
"authors": [
|
||||
"napsio",
|
||||
"Joe White",
|
||||
"Johannes Sjölund (wulax)"
|
||||
],
|
||||
"licenses": [
|
||||
"CC-BY-SA 3.0",
|
||||
"GPL 3.0"
|
||||
],
|
||||
"urls": [
|
||||
"https://opengameart.org/content/lpc-medieval-fantasy-character-sprites",
|
||||
"http://opengameart.org/content/lpc-clothing-updates"
|
||||
],
|
||||
"name": "Shoes"
|
||||
},
|
||||
"clothes.legs.pants": {
|
||||
"notes": [
|
||||
"original male pants by wulax",
|
||||
"edited for female by Joe White",
|
||||
"recolors by JaidynReiman",
|
||||
"walkcycle adapted to pregnant base by ElizaWy",
|
||||
"remaining animations, recolors and edits to v3 base by bluecarrot16",
|
||||
"teem body by Redshrike",
|
||||
"teen legs by ElizaWy derived from base",
|
||||
"2023-12 [napsio]: minimized colors to 6 for each material and fixed broken colors"
|
||||
],
|
||||
"authors": [
|
||||
"napsio",
|
||||
"bluecarrot16",
|
||||
"ElizaWy",
|
||||
"David Conway Jr. (JaidynReiman)",
|
||||
"Joe White",
|
||||
"Matthew Krohn (makrohn)",
|
||||
"Johannes Sjölund (wulax)",
|
||||
"Nila122",
|
||||
"Stephen Challener (Redshrike)"
|
||||
],
|
||||
"licenses": [
|
||||
"CC-BY-SA 3.0",
|
||||
"GPL 3.0"
|
||||
],
|
||||
"urls": [
|
||||
"https://opengameart.org/content/liberated-pixel-cup-lpc-base-assets-sprites-map-tiles",
|
||||
"https://opengameart.org/content/lpc-medieval-fantasy-character-sprites",
|
||||
"http://opengameart.org/content/lpc-clothing-updates",
|
||||
"https://opengameart.org/content/more-lpc-clothes-and-hair",
|
||||
"https://opengameart.org/content/lpc-pregnancy-bases-maternity-wear",
|
||||
"https://opengameart.org/content/lpc-teen-unisex-base-clothes",
|
||||
"https://opengameart.org/content/lpc-clothes-for-children"
|
||||
],
|
||||
"name": "Pants"
|
||||
}
|
||||
}
|
BIN
assets/spritesheets/hurt.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/spritesheets/idle.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
176
assets/spritesheets/researcher.json
Normal file
|
@ -0,0 +1,176 @@
|
|||
{
|
||||
"bodyTypeName": "male",
|
||||
"url": "https://liberatedpixelcup.github.io/Universal-LPC-Spritesheet-Character-Generator/#?body=Body_color_light&head=Human_male_light&clothes=Longsleeve_blue&legs=Pants_black&eye_color=Eye_Color_blue&eyebrows=Thin_Eyebrows_ginger&nose=Straight_nose_light&hair=Plain_ginger&shoes=Revised_Shoes_brown&socks=Ankle_Socks_white&jacket=Collared_coat_tan",
|
||||
"spritesheets": "https://liberatedpixelcup.github.io/Universal-LPC-Spritesheet-Character-Generator/spritesheets/",
|
||||
"version": 1,
|
||||
"datetime": "6/2/2025, 10:57:16 AM",
|
||||
"credits": [
|
||||
{
|
||||
"fileName": "body/bodies/male/light.png",
|
||||
"licenses": "OGA-BY 3.0,CC-BY-SA 3.0,GPL 3.0",
|
||||
"authors": "bluecarrot16,JaidynReiman,Benjamin K. Smith (BenCreating),Evert,Eliza Wyatt (ElizaWy),TheraHedwig,MuffinElZangano,Durrani,Johannes Sjölund (wulax),Stephen Challener (Redshrike)",
|
||||
"urls": "https://opengameart.org/content/liberated-pixel-cup-lpc-base-assets-sprites-map-tiles,https://opengameart.org/content/lpc-medieval-fantasy-character-sprites,https://opengameart.org/content/lpc-male-jumping-animation-by-durrani,https://opengameart.org/content/lpc-runcycle-and-diagonal-walkcycle,https://opengameart.org/content/lpc-revised-character-basics,https://opengameart.org/content/lpc-be-seated,https://opengameart.org/content/lpc-runcycle-for-male-muscular-and-pregnant-character-bases-with-modular-heads,https://opengameart.org/content/lpc-jump-expanded,https://opengameart.org/content/lpc-character-bases",
|
||||
"notes": "see details at https://opengameart.org/content/lpc-character-bases; 'Thick' Male Revised Run/Climb by JaidynReiman (based on ElizaWy's LPC Revised)"
|
||||
},
|
||||
{
|
||||
"fileName": "head/heads/human/male/light.png",
|
||||
"licenses": "OGA-BY 3.0,CC-BY-SA 3.0,GPL 3.0",
|
||||
"authors": "bluecarrot16,Benjamin K. Smith (BenCreating),Stephen Challener (Redshrike)",
|
||||
"urls": "https://opengameart.org/content/liberated-pixel-cup-lpc-base-assets-sprites-map-tiles,https://opengameart.org/content/lpc-character-bases",
|
||||
"notes": "original head by Redshrike, tweaks by BenCreating, modular version by bluecarrot16"
|
||||
},
|
||||
{
|
||||
"fileName": "eyes/human/adult/default/blue.png",
|
||||
"licenses": "OGA-BY 3.0,CC-BY-SA 3.0,GPL 3.0",
|
||||
"authors": "JaidynReiman,Matthew Krohn (makrohn),Stephen Challener (Redshrike)",
|
||||
"urls": "https://opengameart.org/content/liberated-pixel-cup-lpc-base-assets-sprites-map-tiles",
|
||||
"notes": "original by Redshrike, mapped to all frames by Matthew Krohn & JaidynReiman"
|
||||
},
|
||||
{
|
||||
"fileName": "head/nose/straight/adult/light.png",
|
||||
"licenses": "GPL 3.0,CC-BY-SA 3.0",
|
||||
"authors": "Thane Brimhall (pennomi),laetissima,Matthew Krohn (makrohn)",
|
||||
"urls": "https://opengameart.org/content/lpc-base-character-expressions",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"fileName": "eyes/eyebrows/thin/adult/ginger.png",
|
||||
"licenses": "OGA-BY 3.0",
|
||||
"authors": "ElizaWy",
|
||||
"urls": "https://github.com/ElizaWy/LPC/tree/main/Characters/Hair,https://opengameart.org/content/lpc-expanded-sit-run-jump-more",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"fileName": "hair/plain/adult/ginger.png",
|
||||
"licenses": "OGA-BY 3.0,CC-BY-SA 3.0,GPL 3.0",
|
||||
"authors": "JaidynReiman,Manuel Riecke (MrBeast),Joe White",
|
||||
"urls": "https://opengameart.org/content/liberated-pixel-cup-lpc-base-assets-sprites-map-tiles,https://opengameart.org/content/ponytail-and-plain-hairstyles,https://opengameart.org/content/lpc-expanded-hair",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"fileName": "torso/clothes/longsleeve/longsleeve/male/blue.png",
|
||||
"licenses": "OGA-BY 3.0,CC-BY-SA 3.0,GPL 3.0",
|
||||
"authors": "JaidynReiman,Johannes Sjölund (wulax)",
|
||||
"urls": "https://opengameart.org/content/lpc-medieval-fantasy-character-sprites,http://opengameart.org/content/lpc-clothing-updates",
|
||||
"notes": "original by wulax, recolors and cleanup by JaidynReiman, further recolors by bluecarrot16"
|
||||
},
|
||||
{
|
||||
"fileName": "torso/jacket/collared/male/tan.png",
|
||||
"licenses": "CC-BY-SA 3.0,GPL 3.0",
|
||||
"authors": "bluecarrot16",
|
||||
"urls": "https://opengameart.org/content/lpc-gentleman,https://opengameart.org/content/lpc-pirates",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"fileName": "legs/pants/male/black.png",
|
||||
"licenses": "OGA-BY 3.0,GPL 3.0,CC-BY-SA 3.0",
|
||||
"authors": "bluecarrot16,JaidynReiman,ElizaWy,Matthew Krohn (makrohn),Johannes Sjölund (wulax),Stephen Challener (Redshrike)",
|
||||
"urls": "https://opengameart.org/content/liberated-pixel-cup-lpc-base-assets-sprites-map-tiles,https://opengameart.org/content/lpc-medieval-fantasy-character-sprites,https://opengameart.org/content/lpc-expanded-pants",
|
||||
"notes": "original male pants by wulax, recolors and edits to v3 base by bluecarrot16, climb/jump/run/sit/emotes/revised combat by JaidynReiman based on ElizaWy's LPC Revised"
|
||||
},
|
||||
{
|
||||
"fileName": "feet/shoes/revised/male/brown.png",
|
||||
"licenses": "OGA-BY 3.0,GPL 3.0",
|
||||
"authors": "JaidynReiman,ElizaWy,Bluecarrot16,Stephen Challener (Redshrike),Johannes Sjölund (wulax)",
|
||||
"urls": "https://github.com/ElizaWy/LPC/tree/main/Characters/Clothing,https://opengameart.org/content/lpc-expanded-socks-shoes",
|
||||
"notes": "original overalls and shoes by ElizaWy, base animations adapted from v3 overalls by bluecarrot16, shoes by JaidynReiman"
|
||||
},
|
||||
{
|
||||
"fileName": "feet/socks/ankle/male/white.png",
|
||||
"licenses": "OGA-BY 3.0,GPL 3.0",
|
||||
"authors": "JaidynReiman,ElizaWy,Bluecarrot16,Stephen Challener (Redshrike),Johannes Sjölund (wulax)",
|
||||
"urls": "https://github.com/ElizaWy/LPC/tree/main/Characters/Clothing,https://opengameart.org/content/lpc-expanded-socks-shoes",
|
||||
"notes": "original overalls and thin socks by ElizaWy, base animations adapted from v3 overalls by bluecarrot16, socks by JaidynReiman"
|
||||
}
|
||||
],
|
||||
"layers": [
|
||||
{
|
||||
"fileName": "body/bodies/male/light.png",
|
||||
"zPos": 10,
|
||||
"parentName": "body",
|
||||
"name": "Body_color",
|
||||
"variant": "light",
|
||||
"supportedAnimations": "spellcast,thrust,walk,slash,shoot,hurt,watering,idle,jump,run,sit,emote,climb,combat,1h_slash,1h_backslash,1h_halfslash"
|
||||
},
|
||||
{
|
||||
"fileName": "feet/socks/ankle/male/white.png",
|
||||
"zPos": 14,
|
||||
"parentName": "socks",
|
||||
"name": "Ankle_Socks",
|
||||
"variant": "white",
|
||||
"supportedAnimations": "spellcast,thrust,walk,slash,shoot,hurt,watering,idle,jump,run,sit,emote,climb,combat,1h_slash,1h_backslash,1h_halfslash"
|
||||
},
|
||||
{
|
||||
"fileName": "feet/shoes/revised/male/brown.png",
|
||||
"zPos": 15,
|
||||
"parentName": "shoes",
|
||||
"name": "Revised_Shoes",
|
||||
"variant": "brown",
|
||||
"supportedAnimations": "spellcast,thrust,walk,slash,shoot,hurt,watering,idle,jump,run,sit,emote,climb,combat,1h_slash,1h_backslash,1h_halfslash"
|
||||
},
|
||||
{
|
||||
"fileName": "legs/pants/male/black.png",
|
||||
"zPos": 20,
|
||||
"parentName": "legs",
|
||||
"name": "Pants",
|
||||
"variant": "black",
|
||||
"supportedAnimations": "spellcast,thrust,walk,slash,shoot,hurt,watering,idle,jump,run,sit,emote,climb,combat,1h_slash,1h_backslash,1h_halfslash"
|
||||
},
|
||||
{
|
||||
"fileName": "torso/clothes/longsleeve/longsleeve/male/blue.png",
|
||||
"zPos": 35,
|
||||
"parentName": "clothes",
|
||||
"name": "Longsleeve",
|
||||
"variant": "blue",
|
||||
"supportedAnimations": "spellcast,thrust,walk,slash,shoot,hurt,watering"
|
||||
},
|
||||
{
|
||||
"fileName": "torso/jacket/collared/male/tan.png",
|
||||
"zPos": 55,
|
||||
"parentName": "jacket",
|
||||
"name": "Collared_coat",
|
||||
"variant": "tan",
|
||||
"supportedAnimations": "spellcast,thrust,walk,slash,shoot,hurt,watering"
|
||||
},
|
||||
{
|
||||
"fileName": "head/heads/human/male/light.png",
|
||||
"zPos": 100,
|
||||
"parentName": "head",
|
||||
"name": "Human_male",
|
||||
"variant": "light",
|
||||
"supportedAnimations": "spellcast,thrust,walk,slash,shoot,hurt,watering,idle,jump,run,sit,emote,climb,combat,1h_slash,1h_backslash,1h_halfslash"
|
||||
},
|
||||
{
|
||||
"fileName": "eyes/human/adult/default/blue.png",
|
||||
"zPos": 105,
|
||||
"parentName": "eye_color",
|
||||
"name": "Eye_Color",
|
||||
"variant": "blue",
|
||||
"supportedAnimations": "spellcast,thrust,walk,slash,shoot,hurt,watering,idle,jump,run,sit,emote,climb,combat,1h_slash,1h_backslash,1h_halfslash"
|
||||
},
|
||||
{
|
||||
"fileName": "head/nose/straight/adult/light.png",
|
||||
"zPos": 105,
|
||||
"parentName": "nose",
|
||||
"name": "Straight_nose",
|
||||
"variant": "light",
|
||||
"supportedAnimations": "spellcast,thrust,walk,slash,shoot,hurt,watering,idle,jump,run,sit,emote,climb,combat,1h_slash,1h_backslash,1h_halfslash"
|
||||
},
|
||||
{
|
||||
"fileName": "eyes/eyebrows/thin/adult/ginger.png",
|
||||
"zPos": 106,
|
||||
"parentName": "eyebrows",
|
||||
"name": "Thin_Eyebrows",
|
||||
"variant": "ginger",
|
||||
"supportedAnimations": "spellcast,thrust,walk,slash,shoot,hurt,watering,idle,jump,run,sit,emote,climb,combat,1h_slash,1h_backslash,1h_halfslash"
|
||||
},
|
||||
{
|
||||
"fileName": "hair/plain/adult/ginger.png",
|
||||
"zPos": 120,
|
||||
"parentName": "hair",
|
||||
"name": "Plain",
|
||||
"variant": "ginger",
|
||||
"supportedAnimations": "spellcast,thrust,walk,slash,shoot,hurt,watering,idle,jump,run,sit,emote,climb,combat,1h_slash,1h_backslash,1h_halfslash"
|
||||
}
|
||||
]
|
||||
}
|
BIN
assets/spritesheets/researcher.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
assets/spritesheets/rod.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
assets/spritesheets/shoot.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
assets/spritesheets/slash.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/spritesheets/spell.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/spritesheets/thrust.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
assets/spritesheets/walk.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/spritesheets/whip.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/tilemaps/TopDown_by_deepnight.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
2
clippy.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Require `bevy_ecs::children!` to use `[]` braces, instead of `()` or `{}`.
|
||||
standard-macro-braces = [{ name = "children", brace = "[" }]
|
71
src/asset_tracking.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
//! A high-level way to load collections of asset handles as resources.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.init_resource::<ResourceHandles>();
|
||||
app.add_systems(PreUpdate, load_resource_assets);
|
||||
}
|
||||
|
||||
pub trait LoadResource {
|
||||
/// This will load the [`Resource`] as an [`Asset`]. When all of its asset dependencies
|
||||
/// have been loaded, it will be inserted as a resource. This ensures that the resource only
|
||||
/// exists when the assets are ready.
|
||||
fn load_resource<T: Resource + Asset + Clone + FromWorld>(&mut self) -> &mut Self;
|
||||
}
|
||||
|
||||
impl LoadResource for App {
|
||||
fn load_resource<T: Resource + Asset + Clone + FromWorld>(&mut self) -> &mut Self {
|
||||
self.init_asset::<T>();
|
||||
let world = self.world_mut();
|
||||
let value = T::from_world(world);
|
||||
let assets = world.resource::<AssetServer>();
|
||||
let handle = assets.add(value);
|
||||
let mut handles = world.resource_mut::<ResourceHandles>();
|
||||
handles
|
||||
.waiting
|
||||
.push_back((handle.untyped(), |world, handle| {
|
||||
let assets = world.resource::<Assets<T>>();
|
||||
if let Some(value) = assets.get(handle.id().typed::<T>()) {
|
||||
world.insert_resource(value.clone());
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A function that inserts a loaded resource.
|
||||
type InsertLoadedResource = fn(&mut World, &UntypedHandle);
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct ResourceHandles {
|
||||
// Use a queue for waiting assets so they can be cycled through and moved to
|
||||
// `finished` one at a time.
|
||||
waiting: VecDeque<(UntypedHandle, InsertLoadedResource)>,
|
||||
finished: Vec<UntypedHandle>,
|
||||
}
|
||||
|
||||
impl ResourceHandles {
|
||||
/// Returns true if all requested [`Asset`]s have finished loading and are available as [`Resource`]s.
|
||||
pub fn is_all_done(&self) -> bool {
|
||||
self.waiting.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn load_resource_assets(world: &mut World) {
|
||||
world.resource_scope(|world, mut resource_handles: Mut<ResourceHandles>| {
|
||||
world.resource_scope(|world, assets: Mut<AssetServer>| {
|
||||
for _ in 0..resource_handles.waiting.len() {
|
||||
let (handle, insert_fn) = resource_handles.waiting.pop_front().unwrap();
|
||||
if assets.is_loaded_with_dependencies(&handle) {
|
||||
insert_fn(world, &handle);
|
||||
resource_handles.finished.push(handle);
|
||||
} else {
|
||||
resource_handles.waiting.push_back((handle, insert_fn));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
47
src/audio.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.register_type::<Music>();
|
||||
app.register_type::<SoundEffect>();
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
apply_global_volume.run_if(resource_changed::<GlobalVolume>),
|
||||
);
|
||||
}
|
||||
|
||||
/// An organizational marker component that should be added to a spawned [`AudioPlayer`] if it's in the
|
||||
/// general "music" category (e.g. global background music, soundtrack).
|
||||
///
|
||||
/// This can then be used to query for and operate on sounds in that category.
|
||||
#[derive(Component, Reflect, Default)]
|
||||
#[reflect(Component)]
|
||||
pub struct Music;
|
||||
|
||||
/// A music audio instance.
|
||||
pub fn music(handle: Handle<AudioSource>) -> impl Bundle {
|
||||
(AudioPlayer(handle), PlaybackSettings::LOOP, Music)
|
||||
}
|
||||
|
||||
/// An organizational marker component that should be added to a spawned [`AudioPlayer`] if it's in the
|
||||
/// general "sound effect" category (e.g. footsteps, the sound of a magic spell, a door opening).
|
||||
///
|
||||
/// This can then be used to query for and operate on sounds in that category.
|
||||
#[derive(Component, Reflect, Default)]
|
||||
#[reflect(Component)]
|
||||
pub struct SoundEffect;
|
||||
|
||||
/// A sound effect audio instance.
|
||||
pub fn sound_effect(handle: Handle<AudioSource>) -> impl Bundle {
|
||||
(AudioPlayer(handle), PlaybackSettings::DESPAWN, SoundEffect)
|
||||
}
|
||||
|
||||
/// [`GlobalVolume`] doesn't apply to already-running audio entities, so this system will update them.
|
||||
fn apply_global_volume(
|
||||
global_volume: Res<GlobalVolume>,
|
||||
mut audio_query: Query<(&PlaybackSettings, &mut AudioSink)>,
|
||||
) {
|
||||
for (playback, mut sink) in &mut audio_query {
|
||||
sink.set_volume(global_volume.volume * playback.volume);
|
||||
}
|
||||
}
|
203
src/demo/animation.rs
Normal file
|
@ -0,0 +1,203 @@
|
|||
//! Player sprite animation.
|
||||
//! This is based on multiple examples and may be very different for your game.
|
||||
//! - [Sprite flipping](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_flipping.rs)
|
||||
//! - [Sprite animation](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs)
|
||||
//! - [Timers](https://github.com/bevyengine/bevy/blob/latest/examples/time/timers.rs)
|
||||
|
||||
use bevy::prelude::*;
|
||||
use rand::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
AppSystems, PausableSystems,
|
||||
audio::sound_effect,
|
||||
demo::{movement::MovementController, player::PlayerAssets},
|
||||
};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
// Animate and play sound effects based on controls.
|
||||
app.register_type::<PlayerAnimation>();
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_animation_timer.in_set(AppSystems::TickTimers),
|
||||
(
|
||||
update_animation_movement,
|
||||
update_animation_atlas,
|
||||
trigger_step_sound_effect,
|
||||
)
|
||||
.chain()
|
||||
.run_if(resource_exists::<PlayerAssets>)
|
||||
.in_set(AppSystems::Update),
|
||||
)
|
||||
.in_set(PausableSystems),
|
||||
);
|
||||
}
|
||||
|
||||
/// Update the sprite direction and animation state (idling/walking).
|
||||
fn update_animation_movement(
|
||||
mut player_query: Query<(&MovementController, &mut Sprite, &mut PlayerAnimation)>,
|
||||
) {
|
||||
for (controller, mut sprite, mut animation) in &mut player_query {
|
||||
let dx = controller.intent.x;
|
||||
if dx != 0.0 {
|
||||
sprite.flip_x = dx < 0.0;
|
||||
}
|
||||
|
||||
let animation_state = if controller.intent == Vec2::ZERO {
|
||||
PlayerAnimationState::Idling
|
||||
} else {
|
||||
PlayerAnimationState::Walking
|
||||
};
|
||||
animation.update_state(animation_state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the animation timer.
|
||||
fn update_animation_timer(time: Res<Time>, mut query: Query<&mut PlayerAnimation>) {
|
||||
for mut animation in &mut query {
|
||||
animation.update_timer(time.delta());
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the texture atlas to reflect changes in the animation.
|
||||
fn update_animation_atlas(mut query: Query<(&PlayerAnimation, &mut Sprite)>) {
|
||||
for (animation, mut sprite) in &mut query {
|
||||
let Some(atlas) = sprite.texture_atlas.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
if animation.changed() {
|
||||
atlas.index = animation.get_atlas_index();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If the player is moving, play a step sound effect synchronized with the
|
||||
/// animation.
|
||||
fn trigger_step_sound_effect(
|
||||
mut commands: Commands,
|
||||
player_assets: Res<PlayerAssets>,
|
||||
mut step_query: Query<&PlayerAnimation>,
|
||||
) {
|
||||
for animation in &mut step_query {
|
||||
if animation.state == PlayerAnimationState::Walking
|
||||
&& animation.changed()
|
||||
&& (animation.frame == 2 || animation.frame == 5)
|
||||
{
|
||||
let rng = &mut rand::thread_rng();
|
||||
let random_step = player_assets.steps.choose(rng).unwrap().clone();
|
||||
commands.spawn(sound_effect(random_step));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Component that tracks player's animation state.
|
||||
/// It is tightly bound to the texture atlas we use.
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct PlayerAnimation {
|
||||
timer: Timer,
|
||||
frame: usize,
|
||||
state: PlayerAnimationState,
|
||||
}
|
||||
|
||||
#[derive(Reflect, PartialEq)]
|
||||
pub enum PlayerAnimationState {
|
||||
Idling,
|
||||
Walking,
|
||||
Thrusting,
|
||||
Hurting,
|
||||
}
|
||||
|
||||
impl PlayerAnimation {
|
||||
/// The number of idle frames.
|
||||
const IDLE_FRAMES: usize = 1;
|
||||
/// The duration of each idle frame.
|
||||
const IDLE_INTERVAL: Duration = Duration::from_millis(500);
|
||||
/// The number of walking frames.
|
||||
const WALKING_FRAMES: usize = 8;
|
||||
/// The duration of each walking frame.
|
||||
const WALKING_INTERVAL: Duration = Duration::from_millis(50);
|
||||
const THRUST_FRAMES: usize = 8;
|
||||
const THRUST_INTERVAL: Duration = Duration::from_millis(50);
|
||||
const HURT_FRAMES: usize = 4;
|
||||
const HURT_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
fn idling() -> Self {
|
||||
Self {
|
||||
timer: Timer::new(Self::IDLE_INTERVAL, TimerMode::Repeating),
|
||||
frame: 0,
|
||||
state: PlayerAnimationState::Idling,
|
||||
}
|
||||
}
|
||||
|
||||
fn walking() -> Self {
|
||||
Self {
|
||||
timer: Timer::new(Self::WALKING_INTERVAL, TimerMode::Repeating),
|
||||
frame: 0,
|
||||
state: PlayerAnimationState::Walking,
|
||||
}
|
||||
}
|
||||
|
||||
fn thrusting() -> Self {
|
||||
Self {
|
||||
timer: Timer::new(Self::THRUST_INTERVAL, TimerMode::Once),
|
||||
frame: 0,
|
||||
state: PlayerAnimationState::Thrusting,
|
||||
}
|
||||
}
|
||||
|
||||
fn hurting() -> Self {
|
||||
Self {
|
||||
timer: Timer::new(Self::HURT_INTERVAL, TimerMode::Once),
|
||||
frame: 0,
|
||||
state: PlayerAnimationState::Hurting,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self::idling()
|
||||
}
|
||||
|
||||
/// Update animation timers.
|
||||
pub fn update_timer(&mut self, delta: Duration) {
|
||||
self.timer.tick(delta);
|
||||
if !self.timer.finished() {
|
||||
return;
|
||||
}
|
||||
self.frame = (self.frame + 1)
|
||||
% match self.state {
|
||||
PlayerAnimationState::Idling => Self::IDLE_FRAMES,
|
||||
PlayerAnimationState::Walking => Self::WALKING_FRAMES,
|
||||
PlayerAnimationState::Thrusting => Self::THRUST_FRAMES,
|
||||
PlayerAnimationState::Hurting => Self::HURT_FRAMES,
|
||||
};
|
||||
}
|
||||
|
||||
/// Update animation state if it changes.
|
||||
pub fn update_state(&mut self, state: PlayerAnimationState) {
|
||||
if self.state != state {
|
||||
match state {
|
||||
PlayerAnimationState::Idling => *self = Self::idling(),
|
||||
PlayerAnimationState::Walking => *self = Self::walking(),
|
||||
PlayerAnimationState::Thrusting => *self = Self::thrusting(),
|
||||
PlayerAnimationState::Hurting => *self = Self::hurting(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether animation changed this tick.
|
||||
pub fn changed(&self) -> bool {
|
||||
self.timer.finished()
|
||||
}
|
||||
|
||||
/// Return sprite index in the atlas.
|
||||
pub fn get_atlas_index(&self) -> usize {
|
||||
match self.state {
|
||||
PlayerAnimationState::Idling => self.frame,
|
||||
PlayerAnimationState::Walking => 4 + self.frame,
|
||||
PlayerAnimationState::Thrusting => 12 + self.frame,
|
||||
PlayerAnimationState::Hurting => 20 + self.frame,
|
||||
}
|
||||
}
|
||||
}
|
379
src/demo/level.rs
Normal file
|
@ -0,0 +1,379 @@
|
|||
//! Spawn the main level.
|
||||
|
||||
use bevy::{
|
||||
input::mouse::AccumulatedMouseMotion,
|
||||
platform::collections::{HashMap, HashSet},
|
||||
prelude::*,
|
||||
window::PrimaryWindow,
|
||||
};
|
||||
use bevy_ecs_ldtk::{ldtk::NeighbourLevel, prelude::*};
|
||||
|
||||
use crate::{
|
||||
asset_tracking::LoadResource, audio::music, demo::player::PlayerAssets, screens::Screen,
|
||||
};
|
||||
|
||||
use super::player::Player;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_plugins(LdtkPlugin)
|
||||
.insert_resource(LevelSelection::iid("d53f9950-c640-11ed-8430-4942c04951ff"))
|
||||
.insert_resource(LdtkSettings {
|
||||
level_spawn_behavior: LevelSpawnBehavior::UseWorldTranslation {
|
||||
load_level_neighbors: true,
|
||||
},
|
||||
..default()
|
||||
})
|
||||
.init_resource::<LevelWalls>()
|
||||
.init_resource::<MultiLevelWalls>();
|
||||
|
||||
app.register_type::<PanLevel>();
|
||||
app.insert_resource(PanLevel::default());
|
||||
app.register_type::<LevelAssets>();
|
||||
app.load_resource::<LevelAssets>();
|
||||
app.register_type::<LevelWalls>();
|
||||
app.register_type::<MultiLevelWalls>();
|
||||
app.register_ldtk_int_cell::<WallBundle>(1);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
translate_grid_coords_entities,
|
||||
level_selection_follow_player,
|
||||
cache_wall_locations,
|
||||
pan_camera,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Resource, Default, Asset, Clone, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
struct PanLevel {
|
||||
offset: Vec2,
|
||||
}
|
||||
|
||||
#[derive(Resource, Asset, Clone, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct LevelAssets {
|
||||
#[dependency]
|
||||
music: Handle<AudioSource>,
|
||||
world: LdtkProjectHandle,
|
||||
}
|
||||
|
||||
impl FromWorld for LevelAssets {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let assets = world.resource::<AssetServer>();
|
||||
Self {
|
||||
music: assets.load("audio/music/Fluffing A Duck.ogg"),
|
||||
world: assets.load("levels/world.ldtk").into(),
|
||||
// world: assets.load("levels/collectathon.ldtk").into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const GRID_SIZE: i32 = 16;
|
||||
|
||||
fn pan_camera(
|
||||
mut pan: ResMut<PanLevel>,
|
||||
mouse_buttons: Res<ButtonInput<MouseButton>>,
|
||||
mouse_motion: Res<AccumulatedMouseMotion>,
|
||||
) {
|
||||
let delta = mouse_motion.delta;
|
||||
|
||||
if mouse_buttons.pressed(MouseButton::Middle) {
|
||||
pan.offset += delta;
|
||||
info!("pan offset: {}", pan.offset);
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_grid_coords_entities(
|
||||
mut grid_coords_entities: Query<(&mut Transform, &GridCoords), Changed<GridCoords>>,
|
||||
pan: Res<PanLevel>,
|
||||
) {
|
||||
for (mut transform, grid_coords) in grid_coords_entities.iter_mut() {
|
||||
transform.translation = (
|
||||
//pan.offset +
|
||||
bevy_ecs_ldtk::utils::grid_coords_to_translation(*grid_coords, IVec2::splat(GRID_SIZE))
|
||||
)
|
||||
.extend(transform.translation.z);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Component)]
|
||||
struct Wall;
|
||||
|
||||
#[derive(Default, Bundle, LdtkIntCell)]
|
||||
struct WallBundle {
|
||||
wall: Wall,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Eq, Hash, PartialEq, Reflect)]
|
||||
pub enum Direction {
|
||||
#[default]
|
||||
N,
|
||||
E,
|
||||
S,
|
||||
W,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Direction {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
"n" => Ok(Direction::N),
|
||||
"e" => Ok(Direction::E),
|
||||
"s" => Ok(Direction::S),
|
||||
"w" => Ok(Direction::W),
|
||||
_ => Err("Invalid direction string"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_neighbors(
|
||||
neighbors: &[NeighbourLevel],
|
||||
) -> Result<HashMap<Direction, LevelIid>, &'static str> {
|
||||
info!("got neighbors: {:?}", neighbors);
|
||||
let gino = neighbors
|
||||
.iter()
|
||||
// .map(|neighbor| {
|
||||
// let direction = Direction::try_from(neighbor.dir.as_str())?;
|
||||
// Ok((direction, LevelIid::from(neighbor.level_iid.clone())))
|
||||
// })
|
||||
.filter_map(|neighbor| {
|
||||
Direction::try_from(neighbor.dir.as_str())
|
||||
.ok()
|
||||
.map(|dir| (dir, LevelIid::from(neighbor.level_iid.clone())))
|
||||
})
|
||||
.collect();
|
||||
info!("converted to: {:?}", gino);
|
||||
Ok(gino)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Resource, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct LevelWalls {
|
||||
wall_locations: HashSet<GridCoords>,
|
||||
level_width: i32,
|
||||
level_height: i32,
|
||||
level_neighbours: HashMap<Direction, LevelIid>,
|
||||
}
|
||||
|
||||
impl LevelWalls {
|
||||
pub fn in_wall(&self, grid_coords: &GridCoords) -> bool {
|
||||
grid_coords.x < 0
|
||||
|| grid_coords.y < 0
|
||||
|| grid_coords.x >= self.level_width
|
||||
|| grid_coords.y >= self.level_height
|
||||
|| self.wall_locations.contains(grid_coords)
|
||||
}
|
||||
|
||||
pub fn debug_collisions(&self, player_pos: &GridCoords) {
|
||||
info!(
|
||||
"map for a level that is x: {} by y: {}",
|
||||
self.level_width, self.level_height
|
||||
);
|
||||
for y in (0..self.level_height) {
|
||||
for x in 0..self.level_width {
|
||||
let coords = GridCoords::new(x, y);
|
||||
if coords == *player_pos {
|
||||
print!("@");
|
||||
} else if self.in_wall(&coords) {
|
||||
print!("X");
|
||||
} else {
|
||||
print!("_");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Resource, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct MultiLevelWalls {
|
||||
cache: HashMap<LevelIid, LevelWalls>,
|
||||
}
|
||||
|
||||
impl MultiLevelWalls {
|
||||
pub fn in_wall(&self, level: &LevelIid, grid_coords: &GridCoords) -> bool {
|
||||
self.cache[level].in_wall(grid_coords)
|
||||
}
|
||||
pub fn debug_collisions(&self, level: &LevelIid, player_pos: &GridCoords) {
|
||||
if let Some(level) = self.cache.get(level) {
|
||||
level.debug_collisions(player_pos);
|
||||
} else {
|
||||
warn!("No walls cached for level: {:?}", level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A system that spawns the main level.
|
||||
pub fn spawn_level(
|
||||
mut commands: Commands,
|
||||
window: Single<&Window, With<PrimaryWindow>>,
|
||||
level_assets: Res<LevelAssets>,
|
||||
) {
|
||||
let half_size = window.size() / 2.0;
|
||||
|
||||
commands.spawn((
|
||||
Name::new("Ldtk level"),
|
||||
LdtkWorldBundle {
|
||||
ldtk_handle: level_assets.world.clone(),
|
||||
transform: Transform::from_xyz(-half_size.x, half_size.y, 0.0),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn _old_cache_wall_locations(
|
||||
level_selection: Res<LevelSelection>,
|
||||
mut level_walls: ResMut<LevelWalls>,
|
||||
mut level_events: EventReader<LevelEvent>,
|
||||
walls: Query<&GridCoords, With<Wall>>,
|
||||
ldtk_project_entities: Query<&LdtkProjectHandle>,
|
||||
ldtk_project_assets: Res<Assets<LdtkProject>>,
|
||||
) -> Result {
|
||||
for level_event in level_events.read() {
|
||||
if let LevelEvent::Spawned(level_iid) = level_event {
|
||||
let ldtk_project = ldtk_project_assets
|
||||
.get(ldtk_project_entities.single()?)
|
||||
.expect("LdtkProject should be loaded when level is spawned");
|
||||
let level = ldtk_project
|
||||
.get_raw_level_by_iid(level_iid.get())
|
||||
.expect("spawned level should exist in project");
|
||||
|
||||
let wall_locations = walls.iter().copied().collect();
|
||||
|
||||
info!(
|
||||
"loading level of dimension x: {} by y: {}",
|
||||
level.px_wid, level.px_hei
|
||||
);
|
||||
let new_level_walls = LevelWalls {
|
||||
wall_locations,
|
||||
level_width: level.px_wid / GRID_SIZE,
|
||||
level_height: level.px_hei / GRID_SIZE,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
*level_walls = new_level_walls;
|
||||
info!(
|
||||
"new level tile dimensions are x: {} y {}",
|
||||
level_walls.level_width, level_walls.level_height
|
||||
);
|
||||
level_walls.debug_collisions(&GridCoords::default());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cache_wall_locations(
|
||||
mut levels_wall_cache: ResMut<MultiLevelWalls>,
|
||||
mut level_events: EventReader<LevelEvent>,
|
||||
walls: Query<(&ChildOf, &GridCoords), With<Wall>>,
|
||||
ldtk_project_entities: Query<&LdtkProjectHandle>,
|
||||
ldtk_project_assets: Res<Assets<LdtkProject>>,
|
||||
) -> Result {
|
||||
let multi_level_walls = levels_wall_cache.into_inner();
|
||||
|
||||
for level_event in level_events.read() {
|
||||
if let LevelEvent::Spawned(level_iid) = level_event {
|
||||
let ldtk_project = ldtk_project_assets
|
||||
.get(ldtk_project_entities.single()?)
|
||||
.expect("LdtkProject should be loaded when level is spawned");
|
||||
let level = ldtk_project
|
||||
.get_raw_level_by_iid(level_iid.get())
|
||||
.expect("spawned level should exist in project");
|
||||
|
||||
let mut wall_locations = HashSet::<GridCoords>::default();
|
||||
info!("current level neighbours: {:?}", level.neighbours);
|
||||
if let Some(layers) = level.layer_instances.clone() {
|
||||
// info!("layers: {:?}", layers);
|
||||
layers.iter().for_each(|field| {
|
||||
info!("Layer field: {:?}", field.identifier);
|
||||
if field.identifier == "Walls" {
|
||||
info!("Found walls layer: {:?}", field.int_grid_csv);
|
||||
info!("Trying to format it");
|
||||
// FIXME: a .rev() here? It doesn't look necessary from what gets printed
|
||||
// remember to fix the supposed "map dragging" too
|
||||
for y in (0..(level.px_hei / GRID_SIZE)).rev() {
|
||||
for x in (0..(level.px_wid / GRID_SIZE)) {
|
||||
let index = (y * level.px_wid / GRID_SIZE + x) as usize;
|
||||
if let Some(value) = field.int_grid_csv.get(index) {
|
||||
if *value == 1 {
|
||||
print!("X");
|
||||
wall_locations.insert(GridCoords::new(x, y));
|
||||
} else {
|
||||
print!("_");
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// level.iter_fields().for_each(|field| {
|
||||
// info!("Field: {:?}", field);
|
||||
// });
|
||||
|
||||
// let wall_locations = walls.iter().map(|e| e.1).copied().collect();
|
||||
|
||||
info!(
|
||||
"loading level of dimension x: {} by y: {}",
|
||||
level.px_wid, level.px_hei
|
||||
);
|
||||
multi_level_walls.cache.insert(
|
||||
level_iid.clone(), // You'll need to clone the key since HashMap takes ownership
|
||||
LevelWalls {
|
||||
wall_locations,
|
||||
level_width: level.px_wid / GRID_SIZE,
|
||||
level_height: level.px_hei / GRID_SIZE,
|
||||
level_neighbours: convert_neighbors(&level.neighbours).unwrap_or_default(), // Convert neighbours to a HashMap
|
||||
},
|
||||
);
|
||||
|
||||
info!(
|
||||
"new level tile dimensions are x: {} y {}",
|
||||
multi_level_walls.cache[level_iid].level_width,
|
||||
multi_level_walls.cache[level_iid].level_height
|
||||
);
|
||||
multi_level_walls.cache[level_iid].debug_collisions(&GridCoords::default());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn level_selection_follow_player(
|
||||
players: Query<&GlobalTransform, With<Player>>,
|
||||
levels: Query<(&LevelIid, &GlobalTransform)>,
|
||||
ldtk_projects: Query<&LdtkProjectHandle>,
|
||||
ldtk_project_assets: Res<Assets<LdtkProject>>,
|
||||
mut level_selection: ResMut<LevelSelection>,
|
||||
) -> Result {
|
||||
if let Ok(player_transform) = players.single() {
|
||||
let ldtk_project = ldtk_project_assets
|
||||
.get(ldtk_projects.single()?)
|
||||
.expect("ldtk project should be loaded before player is spawned");
|
||||
|
||||
for (level_iid, level_transform) in levels.iter() {
|
||||
let level = ldtk_project
|
||||
.get_raw_level_by_iid(level_iid.get())
|
||||
.expect("level should exist in only project");
|
||||
|
||||
let level_bounds = Rect {
|
||||
min: Vec2::new(
|
||||
level_transform.translation().x,
|
||||
level_transform.translation().y,
|
||||
),
|
||||
max: Vec2::new(
|
||||
level_transform.translation().x + level.px_wid as f32,
|
||||
level_transform.translation().y + level.px_hei as f32,
|
||||
),
|
||||
};
|
||||
|
||||
if level_bounds.contains(player_transform.translation().truncate()) {
|
||||
*level_selection = LevelSelection::Iid(level_iid.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
20
src/demo/mod.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
//! Demo gameplay. All of these modules are only intended for demonstration
|
||||
//! purposes and should be replaced with your own game logic.
|
||||
//! Feel free to change the logic found here if you feel like tinkering around
|
||||
//! to get a feeling for the template.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
mod animation;
|
||||
pub mod level;
|
||||
mod movement;
|
||||
pub mod player;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_plugins((
|
||||
animation::plugin,
|
||||
level::plugin,
|
||||
movement::plugin,
|
||||
player::plugin,
|
||||
));
|
||||
}
|
133
src/demo/movement.rs
Normal file
|
@ -0,0 +1,133 @@
|
|||
//! Handle player input and translate it into movement through a character
|
||||
//! controller. A character controller is the collection of systems that govern
|
||||
//! the movement of characters.
|
||||
//!
|
||||
//! In our case, the character controller has the following logic:
|
||||
//! - Set [`MovementController`] intent based on directional keyboard input.
|
||||
//! This is done in the `player` module, as it is specific to the player
|
||||
//! character.
|
||||
//! - Apply movement based on [`MovementController`] intent and maximum speed.
|
||||
//! - Wrap the character within the window.
|
||||
//!
|
||||
//! Note that the implementation used here is limited for demonstration
|
||||
//! purposes. If you want to move the player in a smoother way,
|
||||
//! consider using a [fixed timestep](https://github.com/bevyengine/bevy/blob/main/examples/movement/physics_in_fixed_timestep.rs).
|
||||
|
||||
use bevy::{prelude::*, window::PrimaryWindow};
|
||||
use bevy_ecs_ldtk::{GridCoords, LevelSelection};
|
||||
|
||||
use crate::{AppSystems, PausableSystems};
|
||||
|
||||
use super::{
|
||||
level::{LevelWalls, MultiLevelWalls},
|
||||
player::Player,
|
||||
};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.register_type::<MovementController>();
|
||||
app.register_type::<ScreenWrap>();
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
(apply_movement, apply_screen_wrap)
|
||||
.chain()
|
||||
.in_set(AppSystems::Update)
|
||||
.in_set(PausableSystems),
|
||||
);
|
||||
}
|
||||
|
||||
/// These are the movement parameters for our character controller.
|
||||
/// For now, this is only used for a single player, but it could power NPCs or
|
||||
/// other players as well.
|
||||
#[derive(Component, Debug, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct MovementController {
|
||||
/// The direction the character wants to move in.
|
||||
pub intent: Vec2,
|
||||
|
||||
/// Maximum speed in world units per second.
|
||||
/// 1 world unit = 1 pixel when using the default 2D camera and no physics engine.
|
||||
pub max_speed: f32,
|
||||
}
|
||||
|
||||
impl Default for MovementController {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
intent: Vec2::ZERO,
|
||||
// 400 pixels per second is a nice default, but we can still vary this per character.
|
||||
max_speed: 40.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_movement(
|
||||
_time: Res<Time>,
|
||||
movement_query: Query<&MovementController, With<Player>>,
|
||||
player_transform_query: Query<&Transform, With<Player>>,
|
||||
mut players: Query<&mut GridCoords, With<Player>>,
|
||||
level_selection: Res<LevelSelection>,
|
||||
// level_walls: Res<LevelWalls>,
|
||||
level_walls: Res<MultiLevelWalls>,
|
||||
) {
|
||||
let level_selection_iid = match level_selection.as_ref() {
|
||||
LevelSelection::Iid(iid) => iid,
|
||||
_ => panic!("level should be selected by iid"),
|
||||
};
|
||||
|
||||
for controller in &movement_query {
|
||||
// no velocity... for now? It'd be nice to have different speed depending on the tile type
|
||||
// let velocity = controller.max_speed * controller.intent;
|
||||
|
||||
for mut player_grid_coords in players.iter_mut() {
|
||||
let movement_direction = if controller.intent.x == 1.0 {
|
||||
GridCoords::new(1, 0)
|
||||
} else if controller.intent.x == -1.0 {
|
||||
GridCoords::new(-1, 0)
|
||||
} else if controller.intent.y == 1.0 {
|
||||
GridCoords::new(0, 1)
|
||||
} else if controller.intent.y == -1.0 {
|
||||
GridCoords::new(0, -1)
|
||||
} else if controller.intent == Vec2::ZERO {
|
||||
// no movement
|
||||
continue;
|
||||
} else {
|
||||
// unrecognized intent, log a warning
|
||||
warn!("Unrecognized intent: {:?}", controller.intent);
|
||||
return;
|
||||
};
|
||||
info!("player old coords: {:?}", player_grid_coords);
|
||||
let destination = *player_grid_coords + movement_direction;
|
||||
info!("commanded movement player coords: {:?}", destination);
|
||||
info!("Level selection: {:?}", level_selection_iid);
|
||||
if !level_walls.in_wall(level_selection_iid, &destination) {
|
||||
*player_grid_coords = destination;
|
||||
info!("new player grid_coords: {:?}", player_grid_coords);
|
||||
info!(
|
||||
"new player screen_coords: {:?}",
|
||||
player_transform_query.single()
|
||||
);
|
||||
// transform.translation += velocity.extend(0.0) * time.delta_secs();
|
||||
} else {
|
||||
info!("SDENG!");
|
||||
}
|
||||
level_walls.debug_collisions(level_selection_iid, &player_grid_coords);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct ScreenWrap;
|
||||
|
||||
fn apply_screen_wrap(
|
||||
window: Single<&Window, With<PrimaryWindow>>,
|
||||
mut wrap_query: Query<&mut Transform, With<ScreenWrap>>,
|
||||
) {
|
||||
let size = window.size() + 256.0;
|
||||
let half_size = size / 2.0;
|
||||
for mut transform in &mut wrap_query {
|
||||
let position = transform.translation.xy();
|
||||
let wrapped = (position + half_size).rem_euclid(size) - half_size;
|
||||
transform.translation = wrapped.extend(transform.translation.z);
|
||||
}
|
||||
}
|
156
src/demo/player.rs
Normal file
|
@ -0,0 +1,156 @@
|
|||
//! Player-specific behavior.
|
||||
|
||||
use bevy::{
|
||||
image::{ImageLoaderSettings, ImageSampler},
|
||||
prelude::*,
|
||||
};
|
||||
use bevy_ecs_ldtk::prelude::*;
|
||||
|
||||
use crate::{
|
||||
AppSystems, PausableSystems,
|
||||
asset_tracking::LoadResource,
|
||||
demo::{
|
||||
animation::PlayerAnimation,
|
||||
movement::{MovementController, ScreenWrap},
|
||||
},
|
||||
};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.register_type::<Player>();
|
||||
|
||||
app.register_type::<PlayerAssets>();
|
||||
app.load_resource::<PlayerAssets>();
|
||||
app.register_ldtk_entity::<PlayerBundle>("Player");
|
||||
|
||||
// Record directional input as movement controls.
|
||||
app.add_systems(
|
||||
Update,
|
||||
record_player_directional_input
|
||||
.in_set(AppSystems::RecordInput)
|
||||
.in_set(PausableSystems),
|
||||
);
|
||||
app.add_systems(Update, process_player);
|
||||
}
|
||||
|
||||
/// The player character.
|
||||
// pub fn player(
|
||||
// max_speed: f32,
|
||||
// player_assets: &PlayerAssets,
|
||||
// texture_atlas_layouts: &mut Assets<TextureAtlasLayout>,
|
||||
// ) -> impl Bundle {
|
||||
// // A texture atlas is a way to split a single image into a grid of related images.
|
||||
// // You can learn more in this example: https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs
|
||||
// let layout =
|
||||
// TextureAtlasLayout::from_grid(UVec2::new(64, 64), 9, 54, Some(UVec2::splat(1)), None);
|
||||
// let texture_atlas_layout = texture_atlas_layouts.add(layout);
|
||||
// let player_animation = PlayerAnimation::new();
|
||||
//
|
||||
// (
|
||||
// Name::new("Player"),
|
||||
// Player,
|
||||
// Sprite {
|
||||
// image: player_assets.ducky.clone(),
|
||||
// texture_atlas: Some(TextureAtlas {
|
||||
// layout: texture_atlas_layout,
|
||||
// index: player_animation.get_atlas_index(),
|
||||
// }),
|
||||
// ..default()
|
||||
// },
|
||||
// // Transform::from_scale(Vec2::splat(1.0).extend(1.0)),
|
||||
// MovementController {
|
||||
// max_speed,
|
||||
// ..default()
|
||||
// },
|
||||
// ScreenWrap,
|
||||
// player_animation,
|
||||
// )
|
||||
// }
|
||||
|
||||
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Player;
|
||||
|
||||
#[derive(Default, Bundle, LdtkEntity)]
|
||||
struct PlayerBundle {
|
||||
#[sprite_sheet]
|
||||
sprite_sheet: Sprite,
|
||||
#[worldly]
|
||||
worldly: Worldly,
|
||||
#[grid_coords]
|
||||
grid_coords: GridCoords,
|
||||
// non-ecsldtk-related components
|
||||
player_comp: Player,
|
||||
movement: MovementController,
|
||||
}
|
||||
|
||||
fn record_player_directional_input(
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
mut controller_query: Query<&mut MovementController, With<Player>>,
|
||||
) {
|
||||
// Collect directional input.
|
||||
let mut intent = Vec2::ZERO;
|
||||
// TODO: check axes for gridcoords!
|
||||
if input.just_pressed(KeyCode::KeyW) || input.pressed(KeyCode::ArrowUp) {
|
||||
intent.y -= 1.0;
|
||||
}
|
||||
if input.just_pressed(KeyCode::KeyS) || input.pressed(KeyCode::ArrowDown) {
|
||||
intent.y += 1.0;
|
||||
}
|
||||
if input.just_pressed(KeyCode::KeyA) || input.pressed(KeyCode::ArrowLeft) {
|
||||
intent.x -= 1.0;
|
||||
}
|
||||
if input.just_pressed(KeyCode::KeyD) || input.pressed(KeyCode::ArrowRight) {
|
||||
intent.x += 1.0;
|
||||
}
|
||||
|
||||
// Normalize intent so that diagonal movement is the same speed as horizontal / vertical.
|
||||
// This should be omitted if the input comes from an analog stick instead.
|
||||
let intent = intent.normalize_or_zero();
|
||||
|
||||
// Apply movement intent to controllers.
|
||||
for mut controller in &mut controller_query {
|
||||
controller.intent = intent;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Asset, Clone, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct PlayerAssets {
|
||||
#[dependency]
|
||||
ducky: Handle<Image>,
|
||||
#[dependency]
|
||||
pub steps: Vec<Handle<AudioSource>>,
|
||||
}
|
||||
|
||||
impl FromWorld for PlayerAssets {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let assets = world.resource::<AssetServer>();
|
||||
Self {
|
||||
ducky: assets.load_with_settings(
|
||||
"spritesheets/researcher.png",
|
||||
|settings: &mut ImageLoaderSettings| {
|
||||
// Use `nearest` image sampling to preserve pixel art style.
|
||||
settings.sampler = ImageSampler::nearest();
|
||||
},
|
||||
),
|
||||
steps: vec![
|
||||
assets.load("audio/sound_effects/step1.ogg"),
|
||||
assets.load("audio/sound_effects/step2.ogg"),
|
||||
assets.load("audio/sound_effects/step3.ogg"),
|
||||
assets.load("audio/sound_effects/step4.ogg"),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_player(
|
||||
mut _commands: Commands,
|
||||
new_players: Query<(Entity, &GridCoords), Added<Player>>,
|
||||
) {
|
||||
for (player_entity, player_coords) in new_players.iter() {
|
||||
info!(
|
||||
"Spawned new player: {:?} at {:?}",
|
||||
player_entity, player_coords
|
||||
);
|
||||
}
|
||||
}
|
52
src/dev_tools.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
//! Development tools for the game. This plugin is only enabled in dev builds.
|
||||
|
||||
use bevy::{
|
||||
color::palettes::css::*,
|
||||
dev_tools::states::log_transitions,
|
||||
input::common_conditions::{input_just_pressed, input_toggle_active},
|
||||
prelude::*,
|
||||
ui::UiDebugOptions,
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "dev_native", feature = "inspector"))]
|
||||
use bevy_inspector_egui::{
|
||||
DefaultInspectorConfigPlugin, bevy_egui::EguiPlugin, quick::WorldInspectorPlugin,
|
||||
};
|
||||
|
||||
use crate::screens::Screen;
|
||||
|
||||
const TOGGLE_KEY: KeyCode = KeyCode::Backquote;
|
||||
const TOGGLE_ORIG: KeyCode = KeyCode::NumpadAdd;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
// activate inspector if feature is active
|
||||
#[cfg(all(feature = "dev_native", feature = "inspector"))]
|
||||
app.add_plugins((
|
||||
DefaultInspectorConfigPlugin,
|
||||
EguiPlugin {
|
||||
enable_multipass_for_primary_context: true,
|
||||
},
|
||||
WorldInspectorPlugin::default().run_if(input_toggle_active(false, KeyCode::Backquote)),
|
||||
));
|
||||
// Log `Screen` state transitions.
|
||||
app.add_systems(Update, log_transitions::<Screen>);
|
||||
|
||||
// Toggle the debug overlay for UI.
|
||||
app.add_systems(
|
||||
Update,
|
||||
toggle_debug_ui.run_if(input_just_pressed(TOGGLE_KEY)),
|
||||
);
|
||||
app.add_systems(
|
||||
Update,
|
||||
toggle_origin.run_if(input_just_pressed(TOGGLE_ORIG)),
|
||||
);
|
||||
}
|
||||
|
||||
fn toggle_debug_ui(mut options: ResMut<UiDebugOptions>) {
|
||||
options.toggle();
|
||||
}
|
||||
|
||||
fn toggle_origin(mut gizmos: Gizmos) {
|
||||
info!("cucu?");
|
||||
gizmos.cross_2d(Vec2::new(-0., 0.), 12., FUCHSIA);
|
||||
}
|
126
src/main.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
// Support configuring Bevy lints within code.
|
||||
#![cfg_attr(bevy_lint, feature(register_tool), register_tool(bevy))]
|
||||
// Disable console on Windows for non-dev builds.
|
||||
#![cfg_attr(not(feature = "dev"), windows_subsystem = "windows")]
|
||||
|
||||
mod asset_tracking;
|
||||
mod audio;
|
||||
mod demo;
|
||||
#[cfg(feature = "dev")]
|
||||
mod dev_tools;
|
||||
mod menus;
|
||||
mod screens;
|
||||
mod theme;
|
||||
|
||||
use bevy::{asset::AssetMetaCheck, prelude::*, window::PrimaryWindow};
|
||||
|
||||
fn main() -> AppExit {
|
||||
App::new().add_plugins(AppPlugin).run()
|
||||
}
|
||||
|
||||
pub struct AppPlugin;
|
||||
|
||||
impl Plugin for AppPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// Add Bevy plugins.
|
||||
app.add_plugins(
|
||||
DefaultPlugins
|
||||
.set(AssetPlugin {
|
||||
// Wasm builds will check for meta files (that don't exist) if this isn't set.
|
||||
// This causes errors and even panics on web build on itch.
|
||||
// See https://github.com/bevyengine/bevy_github_ci_template/issues/48.
|
||||
meta_check: AssetMetaCheck::Never,
|
||||
..default()
|
||||
})
|
||||
.set(WindowPlugin {
|
||||
primary_window: Window {
|
||||
title: "Chain Reaction Collapse".to_string(),
|
||||
fit_canvas_to_parent: true,
|
||||
..default()
|
||||
}
|
||||
.into(),
|
||||
..default()
|
||||
}),
|
||||
);
|
||||
|
||||
// Add other plugins.
|
||||
app.add_plugins((
|
||||
asset_tracking::plugin,
|
||||
audio::plugin,
|
||||
demo::plugin,
|
||||
#[cfg(feature = "dev")]
|
||||
dev_tools::plugin,
|
||||
menus::plugin,
|
||||
screens::plugin,
|
||||
theme::plugin,
|
||||
));
|
||||
|
||||
// Order new `AppSystems` variants by adding them here:
|
||||
app.configure_sets(
|
||||
Update,
|
||||
(
|
||||
AppSystems::TickTimers,
|
||||
AppSystems::RecordInput,
|
||||
AppSystems::Update,
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
|
||||
// Set up the `Pause` state.
|
||||
app.init_state::<Pause>();
|
||||
app.configure_sets(Update, PausableSystems.run_if(in_state(Pause(false))));
|
||||
|
||||
// Spawn the main camera.
|
||||
app.add_systems(Startup, spawn_camera);
|
||||
}
|
||||
}
|
||||
|
||||
/// High-level groupings of systems for the app in the `Update` schedule.
|
||||
/// When adding a new variant, make sure to order it in the `configure_sets`
|
||||
/// call above.
|
||||
#[derive(SystemSet, Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
enum AppSystems {
|
||||
/// Tick timers.
|
||||
TickTimers,
|
||||
/// Record player input.
|
||||
RecordInput,
|
||||
/// Do everything else (consider splitting this into further variants).
|
||||
Update,
|
||||
}
|
||||
|
||||
/// Whether or not the game is paused.
|
||||
#[derive(States, Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
|
||||
#[states(scoped_entities)]
|
||||
struct Pause(pub bool);
|
||||
|
||||
/// A system set for systems that shouldn't run while the game is paused.
|
||||
#[derive(SystemSet, Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
struct PausableSystems;
|
||||
|
||||
// fn apply_screen_wrap(
|
||||
// window: Single<&Window, With<PrimaryWindow>>,
|
||||
// mut wrap_query: Query<&mut Transform, With<ScreenWrap>>,
|
||||
// ) {
|
||||
// let size = window.size() + 256.0;
|
||||
// let half_size = size / 2.0;
|
||||
// for mut transform in &mut wrap_query {
|
||||
// let position = transform.translation.xy();
|
||||
// let wrapped = (position + half_size).rem_euclid(size) - half_size;
|
||||
// transform.translation = wrapped.extend(transform.translation.z);
|
||||
// }
|
||||
// }
|
||||
|
||||
fn spawn_camera(mut commands: Commands, window: Single<&Window, With<PrimaryWindow>>) {
|
||||
let half_size = window.size() / 2.0;
|
||||
|
||||
commands.spawn((
|
||||
Name::new("Camera"),
|
||||
Camera2d,
|
||||
Projection::Orthographic(OrthographicProjection {
|
||||
scale: 0.5,
|
||||
..OrthographicProjection::default_2d()
|
||||
}),
|
||||
// FIXME: what do I need to transform the camera for?
|
||||
// Transform::from_xyz(-half_size.x, half_size.y, 0.0),
|
||||
));
|
||||
}
|
113
src/menus/credits.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
//! The credits menu.
|
||||
|
||||
use bevy::{
|
||||
ecs::spawn::SpawnIter, input::common_conditions::input_just_pressed, prelude::*, ui::Val::*,
|
||||
};
|
||||
|
||||
use crate::{asset_tracking::LoadResource, audio::music, menus::Menu, theme::prelude::*};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Menu::Credits), spawn_credits_menu);
|
||||
app.add_systems(
|
||||
Update,
|
||||
go_back.run_if(in_state(Menu::Credits).and(input_just_pressed(KeyCode::Escape))),
|
||||
);
|
||||
|
||||
app.register_type::<CreditsAssets>();
|
||||
app.load_resource::<CreditsAssets>();
|
||||
app.add_systems(OnEnter(Menu::Credits), start_credits_music);
|
||||
}
|
||||
|
||||
fn spawn_credits_menu(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
widget::ui_root("Credits Menu"),
|
||||
GlobalZIndex(2),
|
||||
StateScoped(Menu::Credits),
|
||||
children![
|
||||
widget::header("Created by"),
|
||||
created_by(),
|
||||
widget::header("Assets"),
|
||||
assets(),
|
||||
widget::button("Back", go_back_on_click),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
fn created_by() -> impl Bundle {
|
||||
grid(vec![
|
||||
["Joe Shmoe", "Implemented alligator wrestling AI"],
|
||||
["Jane Doe", "Made the music for the alien invasion"],
|
||||
])
|
||||
}
|
||||
|
||||
fn assets() -> impl Bundle {
|
||||
grid(vec![
|
||||
["Ducky sprite", "CC0 by Caz Creates Games"],
|
||||
["Button SFX", "CC0 by Jaszunio15"],
|
||||
["Music", "CC BY 3.0 by Kevin MacLeod"],
|
||||
[
|
||||
"Bevy logo",
|
||||
"All rights reserved by the Bevy Foundation, permission granted for splash screen use when unmodified",
|
||||
],
|
||||
])
|
||||
}
|
||||
|
||||
fn grid(content: Vec<[&'static str; 2]>) -> impl Bundle {
|
||||
(
|
||||
Name::new("Grid"),
|
||||
Node {
|
||||
display: Display::Grid,
|
||||
row_gap: Px(10.0),
|
||||
column_gap: Px(30.0),
|
||||
grid_template_columns: RepeatedGridTrack::px(2, 400.0),
|
||||
..default()
|
||||
},
|
||||
Children::spawn(SpawnIter(content.into_iter().flatten().enumerate().map(
|
||||
|(i, text)| {
|
||||
(
|
||||
widget::label(text),
|
||||
Node {
|
||||
justify_self: if i % 2 == 0 {
|
||||
JustifySelf::End
|
||||
} else {
|
||||
JustifySelf::Start
|
||||
},
|
||||
..default()
|
||||
},
|
||||
)
|
||||
},
|
||||
))),
|
||||
)
|
||||
}
|
||||
|
||||
fn go_back_on_click(_: Trigger<Pointer<Click>>, mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(Menu::Main);
|
||||
}
|
||||
|
||||
fn go_back(mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(Menu::Main);
|
||||
}
|
||||
|
||||
#[derive(Resource, Asset, Clone, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
struct CreditsAssets {
|
||||
#[dependency]
|
||||
music: Handle<AudioSource>,
|
||||
}
|
||||
|
||||
impl FromWorld for CreditsAssets {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let assets = world.resource::<AssetServer>();
|
||||
Self {
|
||||
music: assets.load("audio/music/Monkeys Spinning Monkeys.ogg"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_credits_music(mut commands: Commands, credits_music: Res<CreditsAssets>) {
|
||||
commands.spawn((
|
||||
Name::new("Credits Music"),
|
||||
StateScoped(Menu::Credits),
|
||||
music(credits_music.music.clone()),
|
||||
));
|
||||
}
|
55
src/menus/main.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
//! The main menu (seen on the title screen).
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{asset_tracking::ResourceHandles, menus::Menu, screens::Screen, theme::widget};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Menu::Main), spawn_main_menu);
|
||||
}
|
||||
|
||||
fn spawn_main_menu(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
widget::ui_root("Main Menu"),
|
||||
GlobalZIndex(2),
|
||||
StateScoped(Menu::Main),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
children![
|
||||
widget::button("Play", enter_loading_or_gameplay_screen),
|
||||
widget::button("Settings", open_settings_menu),
|
||||
widget::button("Credits", open_credits_menu),
|
||||
widget::button("Exit", exit_app),
|
||||
],
|
||||
#[cfg(target_family = "wasm")]
|
||||
children![
|
||||
widget::button("Play", enter_loading_or_gameplay_screen),
|
||||
widget::button("Settings", open_settings_menu),
|
||||
widget::button("Credits", open_credits_menu),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
fn enter_loading_or_gameplay_screen(
|
||||
_: Trigger<Pointer<Click>>,
|
||||
resource_handles: Res<ResourceHandles>,
|
||||
mut next_screen: ResMut<NextState<Screen>>,
|
||||
) {
|
||||
if resource_handles.is_all_done() {
|
||||
next_screen.set(Screen::Gameplay);
|
||||
} else {
|
||||
next_screen.set(Screen::Loading);
|
||||
}
|
||||
}
|
||||
|
||||
fn open_settings_menu(_: Trigger<Pointer<Click>>, mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(Menu::Settings);
|
||||
}
|
||||
|
||||
fn open_credits_menu(_: Trigger<Pointer<Click>>, mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(Menu::Credits);
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn exit_app(_: Trigger<Pointer<Click>>, mut app_exit: EventWriter<AppExit>) {
|
||||
app_exit.write(AppExit::Success);
|
||||
}
|
30
src/menus/mod.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
//! The game's menus and transitions between them.
|
||||
|
||||
mod credits;
|
||||
mod main;
|
||||
mod pause;
|
||||
mod settings;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.init_state::<Menu>();
|
||||
|
||||
app.add_plugins((
|
||||
credits::plugin,
|
||||
main::plugin,
|
||||
settings::plugin,
|
||||
pause::plugin,
|
||||
));
|
||||
}
|
||||
|
||||
#[derive(States, Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
|
||||
#[states(scoped_entities)]
|
||||
pub enum Menu {
|
||||
#[default]
|
||||
None,
|
||||
Main,
|
||||
Credits,
|
||||
Settings,
|
||||
Pause,
|
||||
}
|
43
src/menus/pause.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
//! The pause menu.
|
||||
|
||||
use bevy::{input::common_conditions::input_just_pressed, prelude::*};
|
||||
|
||||
use crate::{menus::Menu, screens::Screen, theme::widget};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Menu::Pause), spawn_pause_menu);
|
||||
app.add_systems(
|
||||
Update,
|
||||
go_back.run_if(in_state(Menu::Pause).and(input_just_pressed(KeyCode::Escape))),
|
||||
);
|
||||
}
|
||||
|
||||
fn spawn_pause_menu(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
widget::ui_root("Pause Menu"),
|
||||
GlobalZIndex(2),
|
||||
StateScoped(Menu::Pause),
|
||||
children![
|
||||
widget::header("Game paused"),
|
||||
widget::button("Continue", close_menu),
|
||||
widget::button("Settings", open_settings_menu),
|
||||
widget::button("Quit to title", quit_to_title),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
fn open_settings_menu(_: Trigger<Pointer<Click>>, mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(Menu::Settings);
|
||||
}
|
||||
|
||||
fn close_menu(_: Trigger<Pointer<Click>>, mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(Menu::None);
|
||||
}
|
||||
|
||||
fn quit_to_title(_: Trigger<Pointer<Click>>, mut next_screen: ResMut<NextState<Screen>>) {
|
||||
next_screen.set(Screen::Title);
|
||||
}
|
||||
|
||||
fn go_back(mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(Menu::None);
|
||||
}
|
125
src/menus/settings.rs
Normal file
|
@ -0,0 +1,125 @@
|
|||
//! The settings menu.
|
||||
//!
|
||||
//! Additional settings and accessibility options should go here.
|
||||
|
||||
use bevy::{audio::Volume, input::common_conditions::input_just_pressed, prelude::*, ui::Val::*};
|
||||
|
||||
use crate::{menus::Menu, screens::Screen, theme::prelude::*};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Menu::Settings), spawn_settings_menu);
|
||||
app.add_systems(
|
||||
Update,
|
||||
go_back.run_if(in_state(Menu::Settings).and(input_just_pressed(KeyCode::Escape))),
|
||||
);
|
||||
|
||||
app.register_type::<GlobalVolumeLabel>();
|
||||
app.add_systems(
|
||||
Update,
|
||||
update_global_volume_label.run_if(in_state(Menu::Settings)),
|
||||
);
|
||||
}
|
||||
|
||||
fn spawn_settings_menu(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
widget::ui_root("Settings Menu"),
|
||||
GlobalZIndex(2),
|
||||
StateScoped(Menu::Settings),
|
||||
children![
|
||||
widget::header("Settings"),
|
||||
settings_grid(),
|
||||
widget::button("Back", go_back_on_click),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
fn settings_grid() -> impl Bundle {
|
||||
(
|
||||
Name::new("Settings Grid"),
|
||||
Node {
|
||||
display: Display::Grid,
|
||||
row_gap: Px(10.0),
|
||||
column_gap: Px(30.0),
|
||||
grid_template_columns: RepeatedGridTrack::px(2, 400.0),
|
||||
..default()
|
||||
},
|
||||
children![
|
||||
(
|
||||
widget::label("Master Volume"),
|
||||
Node {
|
||||
justify_self: JustifySelf::End,
|
||||
..default()
|
||||
}
|
||||
),
|
||||
global_volume_widget(),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn global_volume_widget() -> impl Bundle {
|
||||
(
|
||||
Name::new("Global Volume Widget"),
|
||||
Node {
|
||||
justify_self: JustifySelf::Start,
|
||||
..default()
|
||||
},
|
||||
children![
|
||||
widget::button_small("-", lower_global_volume),
|
||||
(
|
||||
Name::new("Current Volume"),
|
||||
Node {
|
||||
padding: UiRect::horizontal(Px(10.0)),
|
||||
justify_content: JustifyContent::Center,
|
||||
..default()
|
||||
},
|
||||
children![(widget::label(""), GlobalVolumeLabel)],
|
||||
),
|
||||
widget::button_small("+", raise_global_volume),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
const MIN_VOLUME: f32 = 0.0;
|
||||
const MAX_VOLUME: f32 = 3.0;
|
||||
|
||||
fn lower_global_volume(_: Trigger<Pointer<Click>>, mut global_volume: ResMut<GlobalVolume>) {
|
||||
let linear = (global_volume.volume.to_linear() - 0.1).max(MIN_VOLUME);
|
||||
global_volume.volume = Volume::Linear(linear);
|
||||
}
|
||||
|
||||
fn raise_global_volume(_: Trigger<Pointer<Click>>, mut global_volume: ResMut<GlobalVolume>) {
|
||||
let linear = (global_volume.volume.to_linear() + 0.1).min(MAX_VOLUME);
|
||||
global_volume.volume = Volume::Linear(linear);
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
struct GlobalVolumeLabel;
|
||||
|
||||
fn update_global_volume_label(
|
||||
global_volume: Res<GlobalVolume>,
|
||||
mut label: Single<&mut Text, With<GlobalVolumeLabel>>,
|
||||
) {
|
||||
let percent = 100.0 * global_volume.volume.to_linear();
|
||||
label.0 = format!("{percent:3.0}%");
|
||||
}
|
||||
|
||||
fn go_back_on_click(
|
||||
_: Trigger<Pointer<Click>>,
|
||||
screen: Res<State<Screen>>,
|
||||
mut next_menu: ResMut<NextState<Menu>>,
|
||||
) {
|
||||
next_menu.set(if screen.get() == &Screen::Title {
|
||||
Menu::Main
|
||||
} else {
|
||||
Menu::Pause
|
||||
});
|
||||
}
|
||||
|
||||
fn go_back(screen: Res<State<Screen>>, mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(if screen.get() == &Screen::Title {
|
||||
Menu::Main
|
||||
} else {
|
||||
Menu::Pause
|
||||
});
|
||||
}
|
61
src/screens/gameplay.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
//! The screen state for the main gameplay.
|
||||
|
||||
use bevy::{input::common_conditions::input_just_pressed, prelude::*, ui::Val::*};
|
||||
|
||||
use crate::{Pause, demo::level::spawn_level, menus::Menu, screens::Screen};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Screen::Gameplay), spawn_level);
|
||||
|
||||
// Toggle pause on key press.
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
(pause, spawn_pause_overlay, open_pause_menu).run_if(
|
||||
in_state(Screen::Gameplay)
|
||||
.and(in_state(Menu::None))
|
||||
.and(input_just_pressed(KeyCode::KeyP).or(input_just_pressed(KeyCode::Escape))),
|
||||
),
|
||||
close_menu.run_if(
|
||||
in_state(Screen::Gameplay)
|
||||
.and(not(in_state(Menu::None)))
|
||||
.and(input_just_pressed(KeyCode::KeyP)),
|
||||
),
|
||||
),
|
||||
);
|
||||
app.add_systems(OnExit(Screen::Gameplay), (close_menu, unpause));
|
||||
app.add_systems(
|
||||
OnEnter(Menu::None),
|
||||
unpause.run_if(in_state(Screen::Gameplay)),
|
||||
);
|
||||
}
|
||||
|
||||
fn unpause(mut next_pause: ResMut<NextState<Pause>>) {
|
||||
next_pause.set(Pause(false));
|
||||
}
|
||||
|
||||
fn pause(mut next_pause: ResMut<NextState<Pause>>) {
|
||||
next_pause.set(Pause(true));
|
||||
}
|
||||
|
||||
fn spawn_pause_overlay(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
Name::new("Pause Overlay"),
|
||||
Node {
|
||||
width: Percent(100.0),
|
||||
height: Percent(100.0),
|
||||
..default()
|
||||
},
|
||||
GlobalZIndex(1),
|
||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
||||
StateScoped(Pause(true)),
|
||||
));
|
||||
}
|
||||
|
||||
fn open_pause_menu(mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(Menu::Pause);
|
||||
}
|
||||
|
||||
fn close_menu(mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(Menu::None);
|
||||
}
|
31
src/screens/loading.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
//! A loading screen during which game assets are loaded if necessary.
|
||||
//! This reduces stuttering, especially for audio on Wasm.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{asset_tracking::ResourceHandles, screens::Screen, theme::prelude::*};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Screen::Loading), spawn_loading_screen);
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
enter_gameplay_screen.run_if(in_state(Screen::Loading).and(all_assets_loaded)),
|
||||
);
|
||||
}
|
||||
|
||||
fn spawn_loading_screen(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
widget::ui_root("Loading Screen"),
|
||||
StateScoped(Screen::Loading),
|
||||
children![widget::label("Loading...")],
|
||||
));
|
||||
}
|
||||
|
||||
fn enter_gameplay_screen(mut next_screen: ResMut<NextState<Screen>>) {
|
||||
next_screen.set(Screen::Gameplay);
|
||||
}
|
||||
|
||||
fn all_assets_loaded(resource_handles: Res<ResourceHandles>) -> bool {
|
||||
resource_handles.is_all_done()
|
||||
}
|
30
src/screens/mod.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
//! The game's main screen states and transitions between them.
|
||||
|
||||
mod gameplay;
|
||||
mod loading;
|
||||
mod splash;
|
||||
mod title;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.init_state::<Screen>();
|
||||
|
||||
app.add_plugins((
|
||||
gameplay::plugin,
|
||||
loading::plugin,
|
||||
splash::plugin,
|
||||
title::plugin,
|
||||
));
|
||||
}
|
||||
|
||||
/// The game's main screen states.
|
||||
#[derive(States, Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
|
||||
#[states(scoped_entities)]
|
||||
pub enum Screen {
|
||||
#[default]
|
||||
Splash,
|
||||
Title,
|
||||
Loading,
|
||||
Gameplay,
|
||||
}
|
146
src/screens/splash.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
//! A splash screen that plays briefly at startup.
|
||||
|
||||
use bevy::{
|
||||
image::{ImageLoaderSettings, ImageSampler},
|
||||
input::common_conditions::input_just_pressed,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
use crate::{AppSystems, screens::Screen, theme::prelude::*};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
// Spawn splash screen.
|
||||
app.insert_resource(ClearColor(SPLASH_BACKGROUND_COLOR));
|
||||
app.add_systems(OnEnter(Screen::Splash), spawn_splash_screen);
|
||||
|
||||
// Animate splash screen.
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
tick_fade_in_out.in_set(AppSystems::TickTimers),
|
||||
apply_fade_in_out.in_set(AppSystems::Update),
|
||||
)
|
||||
.run_if(in_state(Screen::Splash)),
|
||||
);
|
||||
|
||||
// Add splash timer.
|
||||
app.register_type::<SplashTimer>();
|
||||
app.add_systems(OnEnter(Screen::Splash), insert_splash_timer);
|
||||
app.add_systems(OnExit(Screen::Splash), remove_splash_timer);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
tick_splash_timer.in_set(AppSystems::TickTimers),
|
||||
check_splash_timer.in_set(AppSystems::Update),
|
||||
)
|
||||
.run_if(in_state(Screen::Splash)),
|
||||
);
|
||||
|
||||
// Exit the splash screen early if the player hits escape.
|
||||
app.add_systems(
|
||||
Update,
|
||||
enter_title_screen
|
||||
.run_if(input_just_pressed(KeyCode::Escape).and(in_state(Screen::Splash))),
|
||||
);
|
||||
}
|
||||
|
||||
const SPLASH_BACKGROUND_COLOR: Color = Color::srgb(0.157, 0.157, 0.157);
|
||||
const SPLASH_DURATION_SECS: f32 = 1.8;
|
||||
const SPLASH_FADE_DURATION_SECS: f32 = 0.6;
|
||||
|
||||
fn spawn_splash_screen(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
commands.spawn((
|
||||
widget::ui_root("Splash Screen"),
|
||||
BackgroundColor(SPLASH_BACKGROUND_COLOR),
|
||||
StateScoped(Screen::Splash),
|
||||
children.
|
||||
"images/splash.png",
|
||||
|settings: &mut ImageLoaderSettings| {
|
||||
// Make an exception for the splash image in case
|
||||
// `ImagePlugin::default_nearest()` is used for pixel art.
|
||||
settings.sampler = ImageSampler::linear();
|
||||
},
|
||||
)),
|
||||
ImageNodeFadeInOut {
|
||||
total_duration: SPLASH_DURATION_SECS,
|
||||
fade_duration: SPLASH_FADE_DURATION_SECS,
|
||||
t: 0.0,
|
||||
},
|
||||
)],
|
||||
));
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
struct ImageNodeFadeInOut {
|
||||
/// Total duration in seconds.
|
||||
total_duration: f32,
|
||||
/// Fade duration in seconds.
|
||||
fade_duration: f32,
|
||||
/// Current progress in seconds, between 0 and [`Self::total_duration`].
|
||||
t: f32,
|
||||
}
|
||||
|
||||
impl ImageNodeFadeInOut {
|
||||
fn alpha(&self) -> f32 {
|
||||
// Normalize by duration.
|
||||
let t = (self.t / self.total_duration).clamp(0.0, 1.0);
|
||||
let fade = self.fade_duration / self.total_duration;
|
||||
|
||||
// Regular trapezoid-shaped graph, flat at the top with alpha = 1.0.
|
||||
((1.0 - (2.0 * t - 1.0).abs()) / fade).min(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_fade_in_out(time: Res<Time>, mut animation_query: Query<&mut ImageNodeFadeInOut>) {
|
||||
for mut anim in &mut animation_query {
|
||||
anim.t += time.delta_secs();
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_fade_in_out(mut animation_query: Query<(&ImageNodeFadeInOut, &mut ImageNode)>) {
|
||||
for (anim, mut image) in &mut animation_query {
|
||||
image.color.set_alpha(anim.alpha())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Debug, Clone, PartialEq, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
struct SplashTimer(Timer);
|
||||
|
||||
impl Default for SplashTimer {
|
||||
fn default() -> Self {
|
||||
Self(Timer::from_seconds(SPLASH_DURATION_SECS, TimerMode::Once))
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_splash_timer(mut commands: Commands) {
|
||||
commands.init_resource::<SplashTimer>();
|
||||
}
|
||||
|
||||
fn remove_splash_timer(mut commands: Commands) {
|
||||
commands.remove_resource::<SplashTimer>();
|
||||
}
|
||||
|
||||
fn tick_splash_timer(time: Res<Time>, mut timer: ResMut<SplashTimer>) {
|
||||
timer.0.tick(time.delta());
|
||||
}
|
||||
|
||||
fn check_splash_timer(timer: ResMut<SplashTimer>, mut next_screen: ResMut<NextState<Screen>>) {
|
||||
if timer.0.just_finished() {
|
||||
next_screen.set(Screen::Title);
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_title_screen(mut next_screen: ResMut<NextState<Screen>>) {
|
||||
next_screen.set(Screen::Title);
|
||||
}
|
18
src/screens/title.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
//! The title screen that appears after the splash screen.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{menus::Menu, screens::Screen};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(Screen::Title), open_main_menu);
|
||||
app.add_systems(OnExit(Screen::Title), close_menu);
|
||||
}
|
||||
|
||||
fn open_main_menu(mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(Menu::Main);
|
||||
}
|
||||
|
||||
fn close_menu(mut next_menu: ResMut<NextState<Menu>>) {
|
||||
next_menu.set(Menu::None);
|
||||
}
|
89
src/theme/interaction.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
use crate::{asset_tracking::LoadResource, audio::sound_effect};
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.register_type::<InteractionPalette>();
|
||||
app.add_systems(Update, apply_interaction_palette);
|
||||
|
||||
app.register_type::<InteractionAssets>();
|
||||
app.load_resource::<InteractionAssets>();
|
||||
app.add_observer(play_on_hover_sound_effect);
|
||||
app.add_observer(play_on_click_sound_effect);
|
||||
}
|
||||
|
||||
/// Palette for widget interactions. Add this to an entity that supports
|
||||
/// [`Interaction`]s, such as a button, to change its [`BackgroundColor`] based
|
||||
/// on the current interaction state.
|
||||
#[derive(Component, Debug, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct InteractionPalette {
|
||||
pub none: Color,
|
||||
pub hovered: Color,
|
||||
pub pressed: Color,
|
||||
}
|
||||
|
||||
fn apply_interaction_palette(
|
||||
mut palette_query: Query<
|
||||
(&Interaction, &InteractionPalette, &mut BackgroundColor),
|
||||
Changed<Interaction>,
|
||||
>,
|
||||
) {
|
||||
for (interaction, palette, mut background) in &mut palette_query {
|
||||
*background = match interaction {
|
||||
Interaction::None => palette.none,
|
||||
Interaction::Hovered => palette.hovered,
|
||||
Interaction::Pressed => palette.pressed,
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Asset, Clone, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
struct InteractionAssets {
|
||||
#[dependency]
|
||||
hover: Handle<AudioSource>,
|
||||
#[dependency]
|
||||
click: Handle<AudioSource>,
|
||||
}
|
||||
|
||||
impl FromWorld for InteractionAssets {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let assets = world.resource::<AssetServer>();
|
||||
Self {
|
||||
hover: assets.load("audio/sound_effects/button_hover.ogg"),
|
||||
click: assets.load("audio/sound_effects/button_click.ogg"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn play_on_hover_sound_effect(
|
||||
trigger: Trigger<Pointer<Over>>,
|
||||
mut commands: Commands,
|
||||
interaction_assets: Option<Res<InteractionAssets>>,
|
||||
interaction_query: Query<(), With<Interaction>>,
|
||||
) {
|
||||
let Some(interaction_assets) = interaction_assets else {
|
||||
return;
|
||||
};
|
||||
|
||||
if interaction_query.contains(trigger.target()) {
|
||||
commands.spawn(sound_effect(interaction_assets.hover.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
fn play_on_click_sound_effect(
|
||||
trigger: Trigger<Pointer<Click>>,
|
||||
mut commands: Commands,
|
||||
interaction_assets: Option<Res<InteractionAssets>>,
|
||||
interaction_query: Query<(), With<Interaction>>,
|
||||
) {
|
||||
let Some(interaction_assets) = interaction_assets else {
|
||||
return;
|
||||
};
|
||||
|
||||
if interaction_query.contains(trigger.target()) {
|
||||
commands.spawn(sound_effect(interaction_assets.click.clone()));
|
||||
}
|
||||
}
|
19
src/theme/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
//! Reusable UI widgets & theming.
|
||||
|
||||
// Unused utilities may trigger this lints undesirably.
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod interaction;
|
||||
pub mod palette;
|
||||
pub mod widget;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub mod prelude {
|
||||
pub use super::{interaction::InteractionPalette, palette as ui_palette, widget};
|
||||
}
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_plugins(interaction::plugin);
|
||||
}
|
16
src/theme/palette.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
/// #ddd369
|
||||
pub const LABEL_TEXT: Color = Color::srgb(0.867, 0.827, 0.412);
|
||||
|
||||
/// #fcfbcc
|
||||
pub const HEADER_TEXT: Color = Color::srgb(0.988, 0.984, 0.800);
|
||||
|
||||
/// #ececec
|
||||
pub const BUTTON_TEXT: Color = Color::srgb(0.925, 0.925, 0.925);
|
||||
/// #4666bf
|
||||
pub const BUTTON_BACKGROUND: Color = Color::srgb(0.275, 0.400, 0.750);
|
||||
/// #6299d1
|
||||
pub const BUTTON_HOVERED_BACKGROUND: Color = Color::srgb(0.384, 0.600, 0.820);
|
||||
/// #3d4999
|
||||
pub const BUTTON_PRESSED_BACKGROUND: Color = Color::srgb(0.239, 0.286, 0.600);
|
135
src/theme/widget.rs
Normal file
|
@ -0,0 +1,135 @@
|
|||
//! Helper functions for creating common widgets.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use bevy::{
|
||||
ecs::{spawn::SpawnWith, system::IntoObserverSystem},
|
||||
prelude::*,
|
||||
ui::Val::*,
|
||||
};
|
||||
|
||||
use crate::theme::{interaction::InteractionPalette, palette::*};
|
||||
|
||||
/// A root UI node that fills the window and centers its content.
|
||||
pub fn ui_root(name: impl Into<Cow<'static, str>>) -> impl Bundle {
|
||||
(
|
||||
Name::new(name),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
width: Percent(100.0),
|
||||
height: Percent(100.0),
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::Center,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: Px(20.0),
|
||||
..default()
|
||||
},
|
||||
// Don't block picking events for other UI roots.
|
||||
Pickable::IGNORE,
|
||||
)
|
||||
}
|
||||
|
||||
/// A simple header label. Bigger than [`label`].
|
||||
pub fn header(text: impl Into<String>) -> impl Bundle {
|
||||
(
|
||||
Name::new("Header"),
|
||||
Text(text.into()),
|
||||
TextFont::from_font_size(40.0),
|
||||
TextColor(HEADER_TEXT),
|
||||
)
|
||||
}
|
||||
|
||||
/// A simple text label.
|
||||
pub fn label(text: impl Into<String>) -> impl Bundle {
|
||||
(
|
||||
Name::new("Label"),
|
||||
Text(text.into()),
|
||||
TextFont::from_font_size(24.0),
|
||||
TextColor(LABEL_TEXT),
|
||||
)
|
||||
}
|
||||
|
||||
/// A large rounded button with text and an action defined as an [`Observer`].
|
||||
pub fn button<E, B, M, I>(text: impl Into<String>, action: I) -> impl Bundle
|
||||
where
|
||||
E: Event,
|
||||
B: Bundle,
|
||||
I: IntoObserverSystem<E, B, M>,
|
||||
{
|
||||
button_base(
|
||||
text,
|
||||
action,
|
||||
(
|
||||
Node {
|
||||
width: Px(380.0),
|
||||
height: Px(80.0),
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::Center,
|
||||
..default()
|
||||
},
|
||||
BorderRadius::MAX,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// A small square button with text and an action defined as an [`Observer`].
|
||||
pub fn button_small<E, B, M, I>(text: impl Into<String>, action: I) -> impl Bundle
|
||||
where
|
||||
E: Event,
|
||||
B: Bundle,
|
||||
I: IntoObserverSystem<E, B, M>,
|
||||
{
|
||||
button_base(
|
||||
text,
|
||||
action,
|
||||
Node {
|
||||
width: Px(30.0),
|
||||
height: Px(30.0),
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::Center,
|
||||
..default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// A simple button with text and an action defined as an [`Observer`]. The button's layout is provided by `button_bundle`.
|
||||
fn button_base<E, B, M, I>(
|
||||
text: impl Into<String>,
|
||||
action: I,
|
||||
button_bundle: impl Bundle,
|
||||
) -> impl Bundle
|
||||
where
|
||||
E: Event,
|
||||
B: Bundle,
|
||||
I: IntoObserverSystem<E, B, M>,
|
||||
{
|
||||
let text = text.into();
|
||||
let action = IntoObserverSystem::into_system(action);
|
||||
(
|
||||
Name::new("Button"),
|
||||
Node::default(),
|
||||
Children::spawn(SpawnWith(|parent: &mut ChildSpawner| {
|
||||
parent
|
||||
.spawn((
|
||||
Name::new("Button Inner"),
|
||||
Button,
|
||||
BackgroundColor(BUTTON_BACKGROUND),
|
||||
InteractionPalette {
|
||||
none: BUTTON_BACKGROUND,
|
||||
hovered: BUTTON_HOVERED_BACKGROUND,
|
||||
pressed: BUTTON_PRESSED_BACKGROUND,
|
||||
},
|
||||
children![(
|
||||
Name::new("Button Text"),
|
||||
Text(text),
|
||||
TextFont::from_font_size(40.0),
|
||||
TextColor(BUTTON_TEXT),
|
||||
// Don't bubble picking events from the text up to the button.
|
||||
Pickable::IGNORE,
|
||||
)],
|
||||
))
|
||||
.insert(button_bundle)
|
||||
.observe(action);
|
||||
})),
|
||||
)
|
||||
}
|