├── .github └── workflows │ ├── deploy-page.yaml │ └── release.yaml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── audio │ └── sfx │ │ ├── drone.ogg │ │ ├── echo1.ogg │ │ ├── echo2.ogg │ │ ├── enemy_alert1.ogg │ │ ├── enemy_alert2.ogg │ │ ├── enemy_death1.ogg │ │ ├── enemy_death2.ogg │ │ ├── level_cleared.ogg │ │ ├── player_death1.ogg │ │ └── player_death2.ogg ├── fonts │ └── Spaceport_2006.otf ├── sprites.svg └── textures │ ├── bevy.png │ ├── button.png │ ├── charge.png │ ├── circle.png │ ├── circle_outline.png │ ├── controls_arrows.png │ ├── echo_ping.png │ ├── player.png │ ├── portal.png │ ├── spiky.png │ └── wave.png ├── build.rs ├── build ├── macos │ ├── AppIcon.iconset │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── create_icns.sh │ ├── icon_1024x1024.png │ └── src │ │ └── Game.app │ │ └── Contents │ │ ├── Info.plist │ │ └── Resources │ │ └── AppIcon.icns ├── web │ ├── sound.js │ └── styles.css └── windows │ ├── icon.ico │ └── icon.rc ├── credits ├── CREDITS.md └── licenses │ └── Bevy_MIT_License.md ├── index.html ├── itch ├── cover.png ├── cover.xcf └── echo_1.gif ├── rust-toolchain.toml ├── src ├── agent │ ├── agent.rs │ └── mod.rs ├── animation │ ├── mod.rs │ ├── tween.rs │ ├── tween_events.rs │ ├── tween_lenses.rs │ └── tween_macros.rs ├── assets │ ├── audio.rs │ ├── fonts.rs │ ├── mod.rs │ ├── textures.rs │ └── window_icon.rs ├── audio │ ├── mod.rs │ └── sfx.rs ├── debug │ └── mod.rs ├── echolocation │ ├── echolocation.rs │ ├── mod.rs │ └── wave.rs ├── enemy │ └── mod.rs ├── input │ ├── actions.rs │ ├── cooldown.rs │ ├── mod.rs │ └── mouse.rs ├── io │ ├── mod.rs │ └── save.rs ├── level │ ├── level.rs │ ├── map.rs │ └── mod.rs ├── lib.rs ├── main.rs ├── physics │ └── mod.rs ├── player │ ├── mod.rs │ └── player.rs ├── render │ ├── camera.rs │ ├── mod.rs │ └── palette.rs ├── state │ ├── mod.rs │ ├── pause.rs │ ├── reset.rs │ └── state.rs ├── time │ ├── mod.rs │ └── time.rs ├── tools │ ├── ecs.rs │ ├── enum_tools.rs │ ├── math.rs │ └── mod.rs └── ui │ ├── button.rs │ ├── game_over.rs │ ├── menu.rs │ ├── mod.rs │ ├── pause.rs │ ├── splash.rs │ └── tutorial.rs └── todo.md /.github/workflows/deploy-page.yaml: -------------------------------------------------------------------------------- 1 | name: deploy-github-page 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | build-web: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | - name: Install rust toolchain 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: nightly 20 | override: true 21 | - name: Install Dependencies 22 | run: sudo apt-get update; sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev 23 | - name: Install trunk 24 | uses: jetli/trunk-action@v0.1.0 25 | with: 26 | version: 'latest' 27 | - name: Add wasm target 28 | run: | 29 | rustup target add wasm32-unknown-unknown 30 | - name: Build Release 31 | run: | 32 | trunk build --release --public-url "${GITHUB_REPOSITORY#*/}" 33 | - name: optimize Wasm 34 | uses: NiklasEi/wasm-opt-action@v2 35 | with: 36 | file: dist/*.wasm 37 | - name: Deploy to GitHub Pages 38 | uses: JamesIves/github-pages-deploy-action@v4.2.5 39 | with: 40 | branch: gh-pages 41 | folder: dist 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release-flow 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+*" 7 | 8 | # ToDo: adapt names 9 | env: 10 | # heads-up: this value is used as a pattern in an sed command as a workaround for a trunk issue 11 | # if you use special characters, take a look at the 'Make paths relative' step in the 'build-web' job 12 | GAME_EXECUTABLE_NAME: ranos 13 | GAME_OSX_APP_NAME: ranos 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | build-macOS: 20 | runs-on: macos-latest 21 | 22 | env: 23 | # macOS 11.0 Big Sur is the first version to support universal binaries 24 | MACOSX_DEPLOYMENT_TARGET: 11.0 25 | steps: 26 | - name: Get tag 27 | id: tag 28 | uses: dawidd6/action-get-tag@v1 29 | - name: Checkout repository 30 | uses: actions/checkout@v2 31 | - name: Remove build script 32 | run: | 33 | rm build.rs 34 | - name: Install rust toolchain for Apple Silicon 35 | uses: actions-rs/toolchain@v1 36 | with: 37 | toolchain: nightly 38 | target: aarch64-apple-darwin 39 | override: true 40 | - name: Build release for Apple Silicon 41 | run: | 42 | SDKROOT=$(xcrun -sdk macosx --show-sdk-path) cargo build --release --target=aarch64-apple-darwin 43 | - name: Install rust toolchain for Apple x86 44 | uses: actions-rs/toolchain@v1 45 | with: 46 | toolchain: nightly 47 | target: x86_64-apple-darwin 48 | override: true 49 | - name: Build release for x86 Apple 50 | run: | 51 | SDKROOT=$(xcrun -sdk macosx --show-sdk-path) cargo build --release --target=x86_64-apple-darwin 52 | - name: Create Universal Binary 53 | run: | 54 | lipo -create -output target/release/${{ env.GAME_EXECUTABLE_NAME }} target/aarch64-apple-darwin/release/${{ env.GAME_EXECUTABLE_NAME }} target/x86_64-apple-darwin/release/${{ env.GAME_EXECUTABLE_NAME }} 55 | - name: Create release 56 | run: | 57 | mkdir -p build/macos/src/Game.app/Contents/MacOS/assets 58 | cp -r assets/ build/macos/src/Game.app/Contents/MacOS/assets 59 | cp -r credits/ build/macos/src/Game.app/Contents/MacOS/credits 60 | cp target/release/${{ env.GAME_EXECUTABLE_NAME }} build/macos/src/Game.app/Contents/MacOS/ 61 | strip build/macos/src/Game.app/Contents/MacOS/${{ env.GAME_EXECUTABLE_NAME }} 62 | mv build/macos/src/Game.app build/macos/src/${{ env.GAME_OSX_APP_NAME }}.app 63 | ln -s /Applications build/macos/src/ 64 | hdiutil create -fs HFS+ -volname "${{ env.GAME_OSX_APP_NAME }}" -srcfolder build/macos/src ${{ env.GAME_EXECUTABLE_NAME }}.dmg 65 | - name: Upload release 66 | uses: svenstaro/upload-release-action@v2 67 | with: 68 | repo_token: ${{ secrets.GITHUB_TOKEN }} 69 | file: ${{ env.GAME_EXECUTABLE_NAME }}.dmg 70 | asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ steps.tag.outputs.tag }}_macOS.dmg 71 | tag: ${{ github.ref }} 72 | overwrite: true 73 | 74 | build-linux: 75 | runs-on: ubuntu-latest 76 | 77 | steps: 78 | - name: Get tag 79 | id: tag 80 | uses: dawidd6/action-get-tag@v1 81 | - name: Checkout repository 82 | uses: actions/checkout@v2 83 | - name: Install rust toolchain 84 | uses: actions-rs/toolchain@v1 85 | with: 86 | toolchain: nightly 87 | override: true 88 | - name: Install Dependencies 89 | run: sudo apt-get update; sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev 90 | - name: Build release 91 | run: | 92 | cargo build --release 93 | - name: Prepare release 94 | run: | 95 | strip target/release/${{ env.GAME_EXECUTABLE_NAME }} 96 | chmod +x target/release/${{ env.GAME_EXECUTABLE_NAME }} 97 | mv target/release/${{ env.GAME_EXECUTABLE_NAME }} . 98 | - name: Bundle release 99 | run: | 100 | tar -czf ${{ env.GAME_EXECUTABLE_NAME }}_linux.tar.gz ${{ env.GAME_EXECUTABLE_NAME }} assets credits 101 | - name: Upload release 102 | uses: svenstaro/upload-release-action@v2 103 | with: 104 | repo_token: ${{ secrets.GITHUB_TOKEN }} 105 | file: ${{ env.GAME_EXECUTABLE_NAME }}_linux.tar.gz 106 | asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ steps.tag.outputs.tag }}_linux.tar.gz 107 | tag: ${{ github.ref }} 108 | overwrite: true 109 | 110 | build-windows: 111 | runs-on: windows-latest 112 | 113 | steps: 114 | - name: Get tag 115 | id: tag 116 | uses: dawidd6/action-get-tag@v1 117 | - name: Checkout repository 118 | uses: actions/checkout@v2 119 | - name: Install rust toolchain 120 | uses: actions-rs/toolchain@v1 121 | with: 122 | toolchain: nightly 123 | override: true 124 | - name: Build release 125 | run: | 126 | cargo build --release 127 | - name: Prepare release 128 | run: | 129 | mkdir target/release/assets && cp -r assets target/release/assets 130 | mkdir target/release/credits && cp -r credits target/release/credits 131 | - name: Zip release 132 | uses: vimtor/action-zip@v1 133 | with: 134 | files: target/release/assets/ target/release/credits/ target/release/${{ env.GAME_EXECUTABLE_NAME }}.exe 135 | dest: ${{ env.GAME_EXECUTABLE_NAME }}_windows.zip 136 | - name: Upload release 137 | uses: svenstaro/upload-release-action@v2 138 | with: 139 | repo_token: ${{ secrets.GITHUB_TOKEN }} 140 | file: ${{ env.GAME_EXECUTABLE_NAME }}_windows.zip 141 | asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ steps.tag.outputs.tag }}_windows.zip 142 | tag: ${{ github.ref }} 143 | overwrite: true 144 | 145 | build-web: 146 | runs-on: ubuntu-latest 147 | 148 | steps: 149 | - name: Get tag 150 | id: tag 151 | uses: dawidd6/action-get-tag@v1 152 | - name: Checkout repository 153 | uses: actions/checkout@v2 154 | - name: Install rust toolchain 155 | uses: actions-rs/toolchain@v1 156 | with: 157 | toolchain: nightly 158 | override: true 159 | - name: Install Dependencies 160 | run: sudo apt-get update; sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev 161 | - name: Install trunk 162 | uses: jetli/trunk-action@v0.1.0 163 | with: 164 | version: latest 165 | - name: Add wasm target 166 | run: | 167 | rustup target add wasm32-unknown-unknown 168 | - name: Build Release 169 | run: | 170 | trunk build --release 171 | - name: optimize Wasm 172 | uses: NiklasEi/wasm-opt-action@v2 173 | with: 174 | file: dist/*.wasm 175 | # Trunk cannot import assets from relative paths (see e.g. https://github.com/thedodd/trunk/issues/395) 176 | # On sites like itch.io, we don't know on which base path the game gets served, so we need to rewrite all links to be relative 177 | - name: Make paths relative 178 | run: | 179 | sed -i 's/\/index/.\/index/g' dist/index.html 180 | sed -i 's/\/${{ env.GAME_EXECUTABLE_NAME }}/.\/${{ env.GAME_EXECUTABLE_NAME }}/g' dist/index.html 181 | - name: Zip release 182 | uses: vimtor/action-zip@v1 183 | with: 184 | files: dist/ 185 | dest: ${{ env.GAME_EXECUTABLE_NAME }}_web.zip 186 | - name: Upload release 187 | uses: svenstaro/upload-release-action@v2 188 | with: 189 | repo_token: ${{ secrets.GITHUB_TOKEN }} 190 | file: ${{ env.GAME_EXECUTABLE_NAME }}_web.zip 191 | asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ steps.tag.outputs.tag }}_web.zip 192 | tag: ${{ github.ref }} 193 | overwrite: true 194 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | .DS_Store 4 | 5 | dist/ 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | // this config bypasses waiting for debugger attachment, increasing speed 8 | { 9 | "name": "Quick Launch", 10 | "type": "node-terminal", 11 | "request": "launch", 12 | "command": "cargo run --features dev" 13 | }, 14 | { 15 | "type": "lldb", 16 | "request": "launch", 17 | "name": "Debug", 18 | "cargo": { 19 | "args": [ 20 | "build", 21 | "--bin=ranos", 22 | "--package=ranos", 23 | "--features", 24 | "dev" 25 | ], 26 | "filter": { 27 | "name": "ranos", 28 | "kind": "bin" 29 | } 30 | }, 31 | "args": [], 32 | "cwd": "${workspaceFolder}", 33 | "env": { 34 | "CARGO_MANIFEST_DIR": "${workspaceFolder}" 35 | }, 36 | "linux": { 37 | "env": { 38 | "LD_LIBRARY_PATH": "${env:LD_LIBRARY_PATH}:${workspaceFolder}/target/debug:${env:HOME}/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib:${env:HOME}/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib" 39 | } 40 | } 41 | }, 42 | { 43 | "type": "lldb", 44 | "request": "launch", 45 | "name": "Debug unit tests", 46 | "cargo": { 47 | "args": [ 48 | "test", 49 | "--no-run", 50 | "--bin=ranos", 51 | "--package=ranos", 52 | "--features", 53 | "dev" 54 | ], 55 | "filter": { 56 | "name": "ranos", 57 | "kind": "bin" 58 | } 59 | }, 60 | "args": [], 61 | "cwd": "${workspaceFolder}", 62 | "env": { 63 | "CARGO_MANIFEST_DIR": "${workspaceFolder}", 64 | "LD_LIBRARY_PATH": "${env:LD_LIBRARY_PATH}:${workspaceFolder}/target/debug:${env:HOME}/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib:${env:HOME}/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib" 65 | } 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ranos" 3 | version = "0.1.0" 4 | publish = false 5 | edition = "2021" 6 | exclude = ["dist", "build", "assets", "credits"] 7 | rust-version = "1.70.0" 8 | 9 | [profile.dev.package."*"] 10 | opt-level = 3 11 | 12 | [profile.dev] 13 | opt-level = 1 14 | 15 | [profile.release] 16 | lto = true 17 | codegen-units = 1 18 | 19 | # The profile that 'cargo dist' will build with 20 | [profile.dist] 21 | inherits = "release" 22 | lto = "thin" 23 | 24 | [features] 25 | dev = ["bevy/bevy_dylib"] 26 | 27 | # Bevy defaults minus audio and some other not needed things 28 | # see https://github.com/bevyengine/bevy/blob/main/Cargo.toml#L31-L54 29 | default = [ 30 | "bevy/animation", 31 | "bevy/bevy_asset", 32 | "bevy/bevy_scene", 33 | "bevy/bevy_winit", 34 | "bevy/bevy_core_pipeline", 35 | "bevy/bevy_render", 36 | "bevy/bevy_sprite", 37 | "bevy/bevy_text", 38 | "bevy/bevy_ui", 39 | "bevy/png", 40 | "bevy/hdr", 41 | "bevy/zstd", 42 | "bevy/x11", 43 | "bevy/ktx2", 44 | "bevy/filesystem_watcher", 45 | "bevy/tonemapping_luts", 46 | ] 47 | 48 | [dependencies] 49 | bevy = { version = "0.10", default-features = false } 50 | bevy_kira_audio = { version = "0.15", features = ["mp3"] } 51 | bevy_asset_loader = { version = "0.15", features = [ 52 | "standard_dynamic_assets", 53 | "progress_tracking", 54 | ] } 55 | rand = "0.8.3" 56 | seldom_fn_plugin = "0.3.0" 57 | bevy_tweening = { git = "https://github.com/SecretPocketCat/bevy_tweening", branch = "generic_completed_payload" } 58 | bevycheck = "0.5.2" 59 | bevy_pkv = "0.7.0" 60 | serde = "1.0.157" 61 | leafwing-input-manager = "0.9.1" 62 | iyes_progress = "0.8.0" 63 | bevy_rapier2d = { version = "*", features = ["debug-render-2d"] } 64 | 65 | # keep the following in sync with Bevy's dependencies 66 | winit = { version = "0.28", default-features = false } 67 | image = { version = "0.24", default-features = false } 68 | bevy_framepace = "0.12.1" 69 | bevy_editor_pls = "0.4.0" 70 | paste = "1.0.12" 71 | bracket-geometry = "0.8.7" 72 | bracket-algorithm-traits = "0.8.7" 73 | bracket-pathfinding = "0.8.7" 74 | interpolation = "0.2.0" 75 | 76 | [build-dependencies] 77 | embed-resource = "1.4" 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ranos](/itch/echo_1.gif) -------------------------------------------------------------------------------- /assets/audio/sfx/drone.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/audio/sfx/drone.ogg -------------------------------------------------------------------------------- /assets/audio/sfx/echo1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/audio/sfx/echo1.ogg -------------------------------------------------------------------------------- /assets/audio/sfx/echo2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/audio/sfx/echo2.ogg -------------------------------------------------------------------------------- /assets/audio/sfx/enemy_alert1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/audio/sfx/enemy_alert1.ogg -------------------------------------------------------------------------------- /assets/audio/sfx/enemy_alert2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/audio/sfx/enemy_alert2.ogg -------------------------------------------------------------------------------- /assets/audio/sfx/enemy_death1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/audio/sfx/enemy_death1.ogg -------------------------------------------------------------------------------- /assets/audio/sfx/enemy_death2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/audio/sfx/enemy_death2.ogg -------------------------------------------------------------------------------- /assets/audio/sfx/level_cleared.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/audio/sfx/level_cleared.ogg -------------------------------------------------------------------------------- /assets/audio/sfx/player_death1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/audio/sfx/player_death1.ogg -------------------------------------------------------------------------------- /assets/audio/sfx/player_death2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/audio/sfx/player_death2.ogg -------------------------------------------------------------------------------- /assets/fonts/Spaceport_2006.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/fonts/Spaceport_2006.otf -------------------------------------------------------------------------------- /assets/sprites.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 38 | 42 | 46 | 47 | 49 | 52 | 56 | 60 | 64 | 65 | 68 | 72 | 76 | 80 | 84 | 85 | 88 | 92 | 96 | 100 | 104 | 105 | 108 | 112 | 116 | 120 | 124 | 125 | 128 | 132 | 136 | 140 | 141 | 152 | 162 | 172 | 183 | 194 | 195 | 199 | 208 | 217 | 227 | 237 | 258 | 279 | 288 | 307 | 315 | 324 | 332 | 351 | SPACE 362 | 369 | 376 | 383 | 390 | 391 | 392 | -------------------------------------------------------------------------------- /assets/textures/bevy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/textures/bevy.png -------------------------------------------------------------------------------- /assets/textures/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/textures/button.png -------------------------------------------------------------------------------- /assets/textures/charge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/textures/charge.png -------------------------------------------------------------------------------- /assets/textures/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/textures/circle.png -------------------------------------------------------------------------------- /assets/textures/circle_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/textures/circle_outline.png -------------------------------------------------------------------------------- /assets/textures/controls_arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/textures/controls_arrows.png -------------------------------------------------------------------------------- /assets/textures/echo_ping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/textures/echo_ping.png -------------------------------------------------------------------------------- /assets/textures/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/textures/player.png -------------------------------------------------------------------------------- /assets/textures/portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/textures/portal.png -------------------------------------------------------------------------------- /assets/textures/spiky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/textures/spiky.png -------------------------------------------------------------------------------- /assets/textures/wave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/assets/textures/wave.png -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate embed_resource; 2 | use std::env; 3 | 4 | fn main() { 5 | let target = env::var("TARGET").unwrap(); 6 | if target.contains("windows") { 7 | // on windows we will set our game icon as icon for the executable 8 | embed_resource::compile("build/windows/icon.rc"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /build/macos/AppIcon.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/AppIcon.iconset/icon_128x128.png -------------------------------------------------------------------------------- /build/macos/AppIcon.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/AppIcon.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /build/macos/AppIcon.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/AppIcon.iconset/icon_16x16.png -------------------------------------------------------------------------------- /build/macos/AppIcon.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/AppIcon.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /build/macos/AppIcon.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/AppIcon.iconset/icon_256x256.png -------------------------------------------------------------------------------- /build/macos/AppIcon.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/AppIcon.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /build/macos/AppIcon.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/AppIcon.iconset/icon_32x32.png -------------------------------------------------------------------------------- /build/macos/AppIcon.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/AppIcon.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /build/macos/AppIcon.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/AppIcon.iconset/icon_512x512.png -------------------------------------------------------------------------------- /build/macos/AppIcon.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/AppIcon.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /build/macos/create_icns.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | rm -rf AppIcon.iconset/* 4 | mkdir -p AppIcon.iconset 5 | sips -z 16 16 icon_1024x1024.png --out AppIcon.iconset/icon_16x16.png 6 | sips -z 32 32 icon_1024x1024.png --out AppIcon.iconset/icon_16x16@2x.png 7 | sips -z 32 32 icon_1024x1024.png --out AppIcon.iconset/icon_32x32.png 8 | sips -z 64 64 icon_1024x1024.png --out AppIcon.iconset/icon_32x32@2x.png 9 | sips -z 128 128 icon_1024x1024.png --out AppIcon.iconset/icon_128x128.png 10 | sips -z 256 256 icon_1024x1024.png --out AppIcon.iconset/icon_128x128@2x.png 11 | sips -z 256 256 icon_1024x1024.png --out AppIcon.iconset/icon_256x256.png 12 | sips -z 512 512 icon_1024x1024.png --out AppIcon.iconset/icon_256x256@2x.png 13 | sips -z 512 512 icon_1024x1024.png --out AppIcon.iconset/icon_512x512.png 14 | cp icon_1024x1024.png AppIcon.iconset/icon_512x512@2x.png 15 | iconutil -c icns AppIcon.iconset 16 | mkdir -p src/Game.app/Contents/Resources 17 | mv AppIcon.icns src/Game.app/Contents/Resources/ 18 | -------------------------------------------------------------------------------- /build/macos/icon_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/icon_1024x1024.png -------------------------------------------------------------------------------- /build/macos/src/Game.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ranos 9 | CFBundleExecutable 10 | ranos 11 | CFBundleIconFile 12 | AppIcon.icns 13 | CFBundleIdentifier 14 | spc.ranos 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | ranos 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 23 | 0.1.0 24 | CFBundleSupportedPlatforms 25 | 26 | MacOSX 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /build/macos/src/Game.app/Contents/Resources/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/macos/src/Game.app/Contents/Resources/AppIcon.icns -------------------------------------------------------------------------------- /build/web/sound.js: -------------------------------------------------------------------------------- 1 | // Insert hack to make sound autoplay on Chrome as soon as the user interacts with the tab: 2 | // https://developers.google.com/web/updates/2018/11/web-audio-autoplay#moving-forward 3 | 4 | // the following function keeps track of all AudioContexts and resumes them on the first user 5 | // interaction with the page. If the function is called and all contexts are already running, 6 | // it will remove itself from all event listeners. 7 | (function () { 8 | // An array of all contexts to resume on the page 9 | const audioContextList = []; 10 | 11 | // An array of various user interaction events we should listen for 12 | const userInputEventNames = [ 13 | "click", 14 | "contextmenu", 15 | "auxclick", 16 | "dblclick", 17 | "mousedown", 18 | "mouseup", 19 | "pointerup", 20 | "touchend", 21 | "keydown", 22 | "keyup", 23 | ]; 24 | 25 | // A proxy object to intercept AudioContexts and 26 | // add them to the array for tracking and resuming later 27 | self.AudioContext = new Proxy(self.AudioContext, { 28 | construct(target, args) { 29 | const result = new target(...args); 30 | audioContextList.push(result); 31 | return result; 32 | }, 33 | }); 34 | 35 | // To resume all AudioContexts being tracked 36 | function resumeAllContexts(_event) { 37 | let count = 0; 38 | 39 | audioContextList.forEach((context) => { 40 | if (context.state !== "running") { 41 | context.resume(); 42 | } else { 43 | count++; 44 | } 45 | }); 46 | 47 | // If all the AudioContexts have now resumed then we unbind all 48 | // the event listeners from the page to prevent unnecessary resume attempts 49 | // Checking count > 0 ensures that the user interaction happens AFTER the game started up 50 | if (count > 0 && count === audioContextList.length) { 51 | userInputEventNames.forEach((eventName) => { 52 | document.removeEventListener(eventName, resumeAllContexts); 53 | }); 54 | } 55 | } 56 | 57 | // We bind the resume function for each user interaction 58 | // event on the page 59 | userInputEventNames.forEach((eventName) => { 60 | document.addEventListener(eventName, resumeAllContexts); 61 | }); 62 | })(); 63 | -------------------------------------------------------------------------------- /build/web/styles.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | background: repeating-linear-gradient( 7 | 135deg, 8 | black 0, 9 | black 2px, 10 | white 2px, 11 | white 20px 12 | ); 13 | margin: 0; 14 | } 15 | 16 | .game-container { 17 | width: 100%; 18 | height: 100%; 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | #bevy { 25 | background-color: white; 26 | width: 800px; 27 | height: 600px; 28 | } 29 | -------------------------------------------------------------------------------- /build/windows/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/build/windows/icon.ico -------------------------------------------------------------------------------- /build/windows/icon.rc: -------------------------------------------------------------------------------- 1 | app_icon ICON "icon.ico" 2 | -------------------------------------------------------------------------------- /credits/CREDITS.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | ## Assets 4 | 5 | * Bevy icon: [MIT License](licenses/Bevy_MIT_License.md); 6 | * [Spaceport 2006 font](https://www.dafont.com/spaceport-2006.font); 7 | * [DEEP MAZE PALETTE](https://lospec.com/palette-list/deep-maze); 8 | * [SFX and music](https://github.com/adsick); 9 | 10 | ## Original template 11 | 12 | * [bevy_game_template](https://github.com/NiklasEi/bevy_game_template) 13 | 14 | -------------------------------------------------------------------------------- /credits/licenses/Bevy_MIT_License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ranos 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | Javascript and support for canvas is required 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /itch/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/itch/cover.png -------------------------------------------------------------------------------- /itch/cover.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/itch/cover.xcf -------------------------------------------------------------------------------- /itch/echo_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meepleek/echo_nomical/78777d1c501439d61055a96dca39040fda2212ce/itch/echo_1.gif -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2023-03-16" 3 | -------------------------------------------------------------------------------- /src/agent/agent.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use bevy::prelude::*; 4 | use bevy_rapier2d::prelude::*; 5 | use bevy_tweening::*; 6 | use interpolation::*; 7 | 8 | use crate::{level::level::Wall, physics::check_collision_start_pair, time::time::*}; 9 | 10 | #[derive(Component, Deref, DerefMut, Default, Reflect)] 11 | pub struct MovementDirection(pub Vec2); 12 | 13 | #[derive(Component, Reflect)] 14 | pub struct MovementDirectionEasing { 15 | time_to_ease: f32, 16 | time: f32, 17 | #[reflect(ignore)] 18 | ease: EaseFunction, 19 | eased_dir: Vec2, 20 | } 21 | 22 | impl MovementDirectionEasing { 23 | pub fn new(time_to_ease: f32) -> Self { 24 | Self::with_ease_fn(time_to_ease, EaseFunction::SineInOut) 25 | } 26 | 27 | pub fn with_ease_fn(time_to_ease: f32, ease: EaseFunction) -> Self { 28 | Self { 29 | time_to_ease, 30 | time: 0., 31 | ease, 32 | eased_dir: Vec2::ZERO, 33 | } 34 | } 35 | 36 | pub fn reset(&mut self) { 37 | self.time = 0. 38 | } 39 | } 40 | 41 | #[derive(Component, Deref, DerefMut, Default)] 42 | pub struct Speed(pub f32); 43 | 44 | /* How long does it take to damp the movement direction */ 45 | #[derive(Component, Deref, DerefMut, Default)] 46 | pub struct Damping(pub f32); 47 | 48 | #[derive(Component, Deref, DerefMut, Default)] 49 | pub struct AgentRotation(pub f32); 50 | 51 | #[derive(Component)] 52 | pub struct StopOnCollision(PhantomData); 53 | 54 | impl StopOnCollision { 55 | pub fn new() -> Self { 56 | Self(PhantomData::default()) 57 | } 58 | } 59 | 60 | #[derive(Component, Deref, DerefMut)] 61 | pub struct DespawnParent(pub Entity); 62 | 63 | #[derive(Component, Deref, DerefMut, Default)] 64 | pub struct Age(pub f32); 65 | 66 | pub(super) fn move_agents( 67 | mut velocity_q: Query<( 68 | &MovementDirection, 69 | Option<&MovementDirectionEasing>, 70 | &Speed, 71 | &mut Transform, 72 | Option<&mut KinematicCharacterController>, 73 | )>, 74 | time: ScaledTime, 75 | ) { 76 | for (dir, eased, speed, mut trans, char_cont) in velocity_q.iter_mut() { 77 | let vel = eased.map_or(dir.0, |e| e.eased_dir) * speed.0 * time.scaled_delta_seconds(); 78 | 79 | if let Some(mut char_cont) = char_cont { 80 | char_cont.translation = Some(vel); 81 | } else { 82 | trans.translation += vel.extend(0.); 83 | } 84 | } 85 | } 86 | 87 | pub(super) fn ease_direction( 88 | mut dir_easing_q: Query<(&MovementDirection, &mut MovementDirectionEasing)>, 89 | time: ScaledTime, 90 | ) { 91 | for (dir, mut ease_dir) in dir_easing_q.iter_mut() { 92 | let time_step = time.scaled_delta_seconds() 93 | * if ease_dir.eased_dir.length() < dir.0.length() { 94 | 1. 95 | } else { 96 | -1. 97 | }; 98 | 99 | ease_dir.time = (ease_dir.time + time_step).clamp(0., ease_dir.time_to_ease); 100 | ease_dir.eased_dir = dir.0 * ((ease_dir.time / ease_dir.time_to_ease).calc(ease_dir.ease)); 101 | } 102 | } 103 | 104 | pub(super) fn apply_damping( 105 | mut damping_q: Query<(&mut MovementDirection, &Damping)>, 106 | time: ScaledTime, 107 | ) { 108 | for (mut dir, damping) in damping_q.iter_mut() { 109 | let damping = dir.0 * (1. / damping.0) * time.scaled_delta_seconds(); 110 | dir.0 -= damping; 111 | } 112 | } 113 | 114 | pub(super) fn rotate(mut dir_q: Query<(&mut Transform, &AgentRotation)>, time: ScaledTime) { 115 | for (mut t, rotation) in &mut dir_q { 116 | t.rotate_local_z((rotation.0 * time.scaled_delta_seconds()).to_radians()); 117 | } 118 | } 119 | 120 | pub(super) fn age(mut age_q: Query<&mut Age>, time: ScaledTime) { 121 | for mut age in &mut age_q.iter_mut() { 122 | age.0 += time.scaled_delta_seconds(); 123 | } 124 | } 125 | 126 | pub(super) fn stop_on_wall_collision( 127 | mut collision_events: EventReader, 128 | stoppable_q: Query<(), With>>, 129 | stop_q: Query<(), With>, 130 | mut dir_q: Query<&mut MovementDirection>, 131 | ) { 132 | for coll in collision_events 133 | .iter() 134 | .filter_map(|ev| check_collision_start_pair(ev, &stoppable_q, &stop_q)) 135 | { 136 | if let Ok(mut dir) = dir_q.get_mut(coll.0) && dir.0.length() > 0. { 137 | dir.0 = Vec2::ZERO; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/agent/mod.rs: -------------------------------------------------------------------------------- 1 | use bevy::{prelude::*, transform::TransformSystem}; 2 | 3 | use crate::state::UnpausedGame; 4 | 5 | use self::agent::{ 6 | age, apply_damping, ease_direction, move_agents, rotate, stop_on_wall_collision, 7 | MovementDirection, MovementDirectionEasing, 8 | }; 9 | 10 | pub mod agent; 11 | 12 | pub fn agent_plugin(app: &mut App) { 13 | app.add_systems( 14 | (rotate, apply_damping, ease_direction, move_agents) 15 | .chain() 16 | .in_set(UnpausedGame) 17 | .in_base_set(CoreSet::PostUpdate) 18 | .before(TransformSystem::TransformPropagate), 19 | ) 20 | .add_system(stop_on_wall_collision.in_set(UnpausedGame)) 21 | .add_system(age.in_base_set(CoreSet::PreUpdate)); 22 | 23 | app.register_type::() 24 | .register_type::(); 25 | } 26 | -------------------------------------------------------------------------------- /src/animation/mod.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_tweening::{component_animator_system, TweeningPlugin}; 3 | 4 | mod tween; 5 | mod tween_events; 6 | pub mod tween_lenses; 7 | mod tween_macros; 8 | 9 | pub use tween::*; 10 | pub use tween_events::TweenDoneAction; 11 | 12 | pub fn animation_plugin(app: &mut App) { 13 | app.add_plugin(TweeningPlugin::::new()) 14 | .add_system(tween_events::on_tween_completed) 15 | .add_system(component_animator_system::) 16 | .add_system(component_animator_system::); 17 | } 18 | -------------------------------------------------------------------------------- /src/animation/tween.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use bevy::prelude::*; 4 | use bevy_tweening::*; 5 | use std::time::Duration; 6 | 7 | use super::tween_events::TweenDoneAction; 8 | use super::tween_lenses::*; 9 | use super::tween_macros::*; 10 | 11 | pub fn delay_tween( 12 | tween: Tween, 13 | delay_ms: u64, 14 | ) -> Sequence { 15 | if delay_ms > 0 { 16 | Delay::new(Duration::from_millis(delay_ms)).then(tween) 17 | } else { 18 | Sequence::new([tween]) 19 | } 20 | } 21 | 22 | relative_tween_fns!( 23 | translation, 24 | Transform, 25 | TweenDoneAction, 26 | TransformRelativePositionLens, 27 | Vec3, 28 | Vec3 29 | ); 30 | 31 | relative_tween_fns!( 32 | scale, 33 | Transform, 34 | TweenDoneAction, 35 | TransformRelativeScaleLens, 36 | Vec3, 37 | Vec3 38 | ); 39 | 40 | relative_tween_fns!( 41 | text_color, 42 | Text, 43 | TweenDoneAction, 44 | TextRelativeColorLens, 45 | Vec, 46 | Color 47 | ); 48 | 49 | relative_tween_fns!( 50 | spritesheet_color, 51 | TextureAtlasSprite, 52 | TweenDoneAction, 53 | SpriteSheetRelativeColorLens, 54 | Color, 55 | Color 56 | ); 57 | 58 | relative_tween_fns!( 59 | sprite_color, 60 | Sprite, 61 | TweenDoneAction, 62 | SpriteRelativeColorLens, 63 | Color, 64 | Color 65 | ); 66 | 67 | relative_tween_fns!( 68 | ui_bg_color, 69 | BackgroundColor, 70 | TweenDoneAction, 71 | UiBackgroundColorLens, 72 | Color, 73 | Color 74 | ); 75 | -------------------------------------------------------------------------------- /src/animation/tween_events.rs: -------------------------------------------------------------------------------- 1 | use bevy::{prelude::*, render::view::NoFrustumCulling}; 2 | use bevy_tweening::TweenCompleted; 3 | 4 | use crate::state::{AppState, PersistReset}; 5 | 6 | #[derive(Clone)] 7 | pub enum TweenDoneAction { 8 | #[allow(dead_code)] 9 | None, 10 | DespawnSelfRecursive, 11 | DespawnRecursive(Entity), 12 | ResetAndNextState(AppState), 13 | } 14 | 15 | pub fn on_tween_completed( 16 | mut cmd: Commands, 17 | mut ev_reader: EventReader>, 18 | entity_q: Query, 19 | reset_q: Query< 20 | Entity, 21 | ( 22 | Without, 23 | Without, 24 | Without, 25 | // this is just to not reset rapier debug as there's no actual usable marker 26 | Without, 27 | ), 28 | >, 29 | mut next_state: ResMut>, 30 | ) { 31 | for ev in ev_reader.iter() { 32 | match &ev.user_data { 33 | TweenDoneAction::None => {} 34 | TweenDoneAction::DespawnSelfRecursive => { 35 | if entity_q.contains(ev.entity) { 36 | cmd.entity(ev.entity).despawn_recursive(); 37 | } 38 | } 39 | TweenDoneAction::DespawnRecursive(e) => { 40 | if entity_q.contains(*e) { 41 | cmd.entity(*e).despawn_recursive(); 42 | } 43 | } 44 | TweenDoneAction::ResetAndNextState(next) => { 45 | for e in reset_q.iter() { 46 | cmd.entity(e).despawn_recursive(); 47 | } 48 | 49 | next_state.set(next.clone()); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/animation/tween_lenses.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_tweening::*; 3 | 4 | use super::tween_macros::*; 5 | 6 | relative_lens!(Transform, Vec3, TransformRelativeScaleLens, scale); 7 | relative_lens!(Transform, Vec3, TransformRelativePositionLens, translation); 8 | 9 | #[derive(Default)] 10 | pub struct TransformRelativeByPositionLens { 11 | start: Vec3, 12 | end: Vec3, 13 | move_by: Vec3, 14 | } 15 | 16 | impl TransformRelativeByPositionLens { 17 | #[allow(dead_code)] 18 | pub fn new(move_by: Vec3) -> Self { 19 | Self { 20 | move_by, 21 | start: Vec3::ZERO, 22 | end: Vec3::ZERO, 23 | } 24 | } 25 | } 26 | 27 | impl Lens for TransformRelativeByPositionLens { 28 | fn lerp(&mut self, target: &mut Transform, ratio: f32) { 29 | let value = self.start + (self.end - self.start) * ratio; 30 | target.translation = value; 31 | } 32 | 33 | fn update_on_tween_start(&mut self, target: &Transform) { 34 | self.start = target.translation; 35 | self.end = target.translation + self.move_by; 36 | } 37 | } 38 | 39 | #[derive(Default)] 40 | pub struct TextRelativeColorLens { 41 | pub start: Option>, 42 | pub end: Color, 43 | } 44 | 45 | impl TextRelativeColorLens { 46 | #[allow(dead_code)] 47 | pub fn relative(end: Color) -> Self { 48 | Self { start: None, end } 49 | } 50 | } 51 | 52 | impl Lens for TextRelativeColorLens { 53 | fn lerp(&mut self, target: &mut Text, ratio: f32) { 54 | for i in 0..target.sections.len() { 55 | if let Some(col) = self.start.as_ref().unwrap().get(i) { 56 | target.sections[i].style.color = lerp_color(*col, self.end, ratio); 57 | } 58 | } 59 | } 60 | 61 | fn update_on_tween_start(&mut self, target: &Text) { 62 | self.start 63 | .get_or_insert_with(|| target.sections.iter().map(|s| s.style.color).collect()); 64 | } 65 | } 66 | 67 | color_lens!(Sprite, SpriteRelativeColorLens, color); 68 | color_lens!(TextureAtlasSprite, SpriteSheetRelativeColorLens, color); 69 | color_lens!(BackgroundColor, UiBackgroundColorLens, 0); 70 | 71 | fn lerp_color(from: Color, to: Color, ratio: f32) -> Color { 72 | let start: Vec4 = from.into(); 73 | let end: Vec4 = to.into(); 74 | start.lerp(end, ratio).into() 75 | } 76 | -------------------------------------------------------------------------------- /src/animation/tween_macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! relative_lens_struct { 2 | ($lens:ident, $value:ty) => { 3 | #[derive(Default)] 4 | pub struct $lens { 5 | pub(super) start: Option<$value>, 6 | pub(super) end: $value, 7 | } 8 | 9 | impl $lens { 10 | #[allow(dead_code)] 11 | pub fn relative(end: $value) -> Self { 12 | Self { start: None, end } 13 | } 14 | 15 | #[allow(dead_code)] 16 | pub fn new(start: $value, end: $value) -> Self { 17 | Self { 18 | start: Some(start), 19 | end, 20 | } 21 | } 22 | } 23 | }; 24 | } 25 | 26 | pub(super) use relative_lens_struct; 27 | 28 | macro_rules! color_lens { 29 | ($component:ty, $lens:ident, $field:tt) => { 30 | relative_lens_struct!($lens, Color); 31 | 32 | impl Lens<$component> for $lens { 33 | fn lerp(&mut self, target: &mut $component, ratio: f32) { 34 | target.$field = lerp_color( 35 | self.start 36 | .expect("Lerping has started so initial values should have been set"), 37 | self.end, 38 | ratio, 39 | ); 40 | } 41 | 42 | fn update_on_tween_start(&mut self, target: &$component) { 43 | self.start.get_or_insert_with(|| target.$field); 44 | } 45 | } 46 | }; 47 | } 48 | 49 | pub(super) use color_lens; 50 | 51 | macro_rules! relative_lens { 52 | ($component:ty, $value:ty, $lens:ident, $field:tt) => { 53 | relative_lens_struct!($lens, $value); 54 | 55 | impl Lens<$component> for $lens { 56 | fn lerp(&mut self, target: &mut $component, ratio: f32) { 57 | let start = self.start.unwrap(); 58 | let value = start + (self.end - start) * ratio; 59 | target.scale = value; 60 | } 61 | 62 | fn update_on_tween_start(&mut self, target: &$component) { 63 | self.start.get_or_insert_with(|| target.$field); 64 | } 65 | } 66 | }; 67 | } 68 | 69 | pub(super) use relative_lens; 70 | 71 | macro_rules! relative_tween_fns { 72 | ($name:ident, $component:ty, $done_action:ty, $lens:ty, $value_start:ty, $value_end:ty) => { 73 | paste::paste! { 74 | pub fn []( 75 | start: $value_start, 76 | end: $value_end, 77 | duration_ms: u64, 78 | on_completed: $done_action, 79 | ) -> Tween<$component, $done_action> { 80 | []( 81 | Some(start), 82 | end, 83 | duration_ms, 84 | EaseFunction::QuadraticInOut, 85 | on_completed, 86 | ) 87 | } 88 | 89 | pub fn []( 90 | end: $value_end, 91 | duration_ms: u64, 92 | on_completed: $done_action, 93 | ) -> Tween<$component, $done_action> { 94 | []( 95 | None, 96 | end, 97 | duration_ms, 98 | EaseFunction::QuadraticInOut, 99 | on_completed, 100 | ) 101 | } 102 | 103 | pub fn []( 104 | start: $value_start, 105 | end: $value_end, 106 | duration_ms: u64, 107 | on_completed: $done_action, 108 | ) -> Animator<$component, $done_action> { 109 | Animator::new([]( 110 | start, 111 | end, 112 | duration_ms, 113 | on_completed, 114 | )) 115 | } 116 | 117 | pub fn []( 118 | end: $value_end, 119 | duration_ms: u64, 120 | on_completed: $done_action, 121 | ) -> Animator<$component, $done_action> { 122 | Animator::new([]( 123 | end, 124 | duration_ms, 125 | on_completed, 126 | )) 127 | } 128 | 129 | pub fn []( 130 | start: Option<$value_start>, 131 | end: $value_end, 132 | duration_ms: u64, 133 | ease: EaseFunction, 134 | on_completed: $done_action, 135 | ) -> Animator<$component, $done_action> { 136 | Animator::new([]( 137 | start, 138 | end, 139 | duration_ms, 140 | ease, 141 | on_completed, 142 | )) 143 | 144 | } 145 | 146 | pub fn []( 147 | start: Option<$value_start>, 148 | end: $value_end, 149 | duration_ms: u64, 150 | ease: EaseFunction, 151 | on_completed: $done_action, 152 | ) -> Tween<$component, $done_action> { 153 | Tween::new( 154 | ease, 155 | Duration::from_millis(duration_ms), 156 | $lens { 157 | start, 158 | end, 159 | }, 160 | ).with_completed_event(on_completed) 161 | } 162 | } 163 | }; 164 | } 165 | 166 | pub(super) use relative_tween_fns; 167 | -------------------------------------------------------------------------------- /src/assets/audio.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_asset_loader::prelude::*; 3 | use bevy_kira_audio::AudioSource; 4 | 5 | use super::add_dynamic_assets; 6 | 7 | #[derive(AssetCollection, Resource)] 8 | pub struct SfxAssets { 9 | // todo: fix echo 10 | #[asset(key = "echo", collection(typed))] 11 | pub echo: Vec>, 12 | 13 | #[asset(key = "player_death", collection(typed))] 14 | pub player_death: Vec>, 15 | 16 | #[asset(key = "enemy_alert", collection(typed))] 17 | pub enemy_alert: Vec>, 18 | 19 | #[asset(key = "enemy_death", collection(typed))] 20 | pub enemy_death: Vec>, 21 | 22 | #[asset(path = "audio/sfx/level_cleared.ogg")] 23 | pub level_cleared: Handle, 24 | 25 | #[asset(path = "audio/sfx/drone.ogg")] 26 | pub drone: Handle, 27 | } 28 | 29 | pub(super) fn setup_sfx_assets(mut dynamic_assets: ResMut) { 30 | for key in ["echo", "player_death", "enemy_alert", "enemy_death"] { 31 | add_dynamic_assets( 32 | &mut dynamic_assets, 33 | key, 34 | &format!("audio/sfx/{}", key), 35 | "ogg", 36 | 1..=2, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/assets/fonts.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_asset_loader::prelude::*; 3 | 4 | #[derive(AssetCollection, Resource)] 5 | pub struct FontAssets { 6 | #[asset(path = "fonts/Spaceport_2006.otf")] 7 | pub menu: Handle, 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use bevy::prelude::*; 4 | use bevy_asset_loader::prelude::*; 5 | use iyes_progress::{ProgressCounter, ProgressPlugin}; 6 | 7 | use crate::AppState; 8 | 9 | pub mod audio; 10 | pub mod fonts; 11 | pub mod textures; 12 | mod window_icon; 13 | 14 | use self::window_icon::set_window_icon; 15 | 16 | pub fn assets_plugin(app: &mut App) { 17 | app.add_startup_system(set_window_icon) 18 | .add_loading_state(LoadingState::new(AppState::Loading)) 19 | .add_collection_to_loading_state::<_, fonts::FontAssets>(AppState::Loading) 20 | .add_collection_to_loading_state::<_, audio::SfxAssets>(AppState::Loading) 21 | .add_collection_to_loading_state::<_, textures::TextureAssets>(AppState::Loading) 22 | .add_startup_system(audio::setup_sfx_assets) 23 | .add_plugin(ProgressPlugin::new(AppState::Loading)) 24 | .add_system(fade_to_splash.in_set(OnUpdate(AppState::Loading))); 25 | } 26 | 27 | fn add_dynamic_assets( 28 | dynamic_assets: &mut DynamicAssets, 29 | key: &str, 30 | file_prefix: &str, 31 | file_ext: &str, 32 | range: RangeInclusive, 33 | ) { 34 | dynamic_assets.register_asset( 35 | key, 36 | Box::new(StandardDynamicAsset::Files { 37 | paths: range 38 | .map(|i| format!("{file_prefix}{i}.{file_ext}")) 39 | .collect(), 40 | }), 41 | ); 42 | } 43 | 44 | fn fade_to_splash(progress: Res, mut fade_reset: ResMut>) { 45 | let progress = progress.progress(); 46 | if progress.total > 0 && progress.done == progress.total { 47 | fade_reset.set(if cfg!(debug_assertions) { 48 | AppState::Game 49 | } else { 50 | AppState::Splash 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/assets/textures.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_asset_loader::prelude::*; 3 | 4 | #[derive(AssetCollection, Resource)] 5 | pub struct TextureAssets { 6 | #[asset(path = "textures/bevy.png")] 7 | pub bevy: Handle, 8 | 9 | #[asset(path = "textures/player.png")] 10 | pub player: Handle, 11 | 12 | #[asset(path = "textures/echo_ping.png")] 13 | pub echo_ping: Handle, 14 | 15 | #[asset(path = "textures/wave.png")] 16 | pub wave: Handle, 17 | 18 | #[asset(path = "textures/spiky.png")] 19 | pub spiky: Handle, 20 | 21 | #[asset(path = "textures/charge.png")] 22 | pub charge: Handle, 23 | 24 | #[asset(path = "textures/portal.png")] 25 | pub portal: Handle, 26 | 27 | #[asset(path = "textures/button.png")] 28 | pub button: Handle, 29 | 30 | #[asset(path = "textures/controls_arrows.png")] 31 | pub controls_arrows: Handle, 32 | } 33 | -------------------------------------------------------------------------------- /src/assets/window_icon.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy::window::PrimaryWindow; 3 | use bevy::winit::WinitWindows; 4 | use std::io::Cursor; 5 | use winit::window::Icon; 6 | 7 | // Sets the icon on windows and X11 8 | pub(super) fn set_window_icon( 9 | windows: NonSend, 10 | primary_window: Query>, 11 | ) { 12 | let primary_entity = primary_window.single(); 13 | let primary = windows.get_window(primary_entity).unwrap(); 14 | let icon_buf = Cursor::new(include_bytes!( 15 | "../../build/macos/AppIcon.iconset/icon_256x256.png" 16 | )); 17 | 18 | if let Ok(image) = image::load(icon_buf, image::ImageFormat::Png) { 19 | let image = image.into_rgba8(); 20 | let (width, height) = image.dimensions(); 21 | let rgba = image.into_raw(); 22 | let icon = Icon::from_rgba(rgba, width, height).unwrap(); 23 | primary.set_window_icon(Some(icon)); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/audio/mod.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_kira_audio::AudioPlugin; 3 | use seldom_fn_plugin::FnPluginExt; 4 | 5 | pub mod sfx; 6 | 7 | pub fn audio_plugin(app: &mut App) { 8 | app.add_plugin(AudioPlugin).fn_plugin(sfx::sfx_plugin); 9 | } 10 | -------------------------------------------------------------------------------- /src/audio/sfx.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use bevy::input::mouse::MouseButtonInput; 4 | use bevy::{ecs::system::Resource, prelude::*}; 5 | use bevy_kira_audio::prelude::*; 6 | use rand::seq::SliceRandom; 7 | use rand::*; 8 | 9 | use crate::assets::audio::SfxAssets; 10 | use crate::echolocation::echolocation::EcholocationEv; 11 | use crate::enemy::EnemyEv; 12 | use crate::io::save::VolumeSettings; 13 | use crate::player::player::PlayerEv; 14 | use crate::state::AppState; 15 | 16 | // todo: use sfx + master volume 17 | pub(super) fn sfx_plugin(app: &mut bevy::prelude::App) { 18 | app.add_audio_channel::() 19 | .add_systems( 20 | ( 21 | play_sfx_on_evt::, 22 | play_sfx_on_evt::, 23 | play_sfx_on_evt::, 24 | ) 25 | .in_base_set(CoreSet::PostUpdate) 26 | .distributive_run_if(resource_exists::()), 27 | ) 28 | .add_system(set_sfx_volume.run_if(resource_changed::())) 29 | .add_system(play_drone.in_schedule(OnExit(AppState::Loading))); 30 | } 31 | 32 | #[derive(Resource)] 33 | struct SfxChannel; 34 | 35 | pub trait SfxEv { 36 | fn get_volume(&self) -> f64; 37 | fn get_sfx_handles(&self, sfx: &SfxAssets) -> Vec>; 38 | fn skip(&self) -> bool { 39 | false 40 | } 41 | } 42 | 43 | impl SfxEv for EcholocationEv { 44 | fn get_volume(&self) -> f64 { 45 | 1.0 46 | } 47 | 48 | fn get_sfx_handles(&self, sfx: &SfxAssets) -> Vec> { 49 | sfx.echo.clone() 50 | } 51 | } 52 | 53 | impl SfxEv for PlayerEv { 54 | fn get_volume(&self) -> f64 { 55 | match self { 56 | PlayerEv::ClearedLevel => 0.5, 57 | PlayerEv::Died => 0.9, 58 | } 59 | } 60 | 61 | fn get_sfx_handles(&self, sfx: &SfxAssets) -> Vec> { 62 | match self { 63 | PlayerEv::ClearedLevel => vec![sfx.level_cleared.clone()], 64 | PlayerEv::Died => sfx.player_death.clone(), 65 | } 66 | } 67 | } 68 | 69 | impl SfxEv for EnemyEv { 70 | fn get_volume(&self) -> f64 { 71 | match self { 72 | EnemyEv::Killed => 0.85, 73 | EnemyEv::Alarmed => 0.3, 74 | } 75 | } 76 | 77 | fn get_sfx_handles(&self, sfx: &SfxAssets) -> Vec> { 78 | match self { 79 | EnemyEv::Killed => sfx.enemy_death.clone(), 80 | EnemyEv::Alarmed => sfx.enemy_alert.clone(), 81 | } 82 | } 83 | } 84 | 85 | fn play_sfx_on_evt( 86 | mut ev_r: EventReader, 87 | audio: Res>, 88 | sfx: Res, 89 | volume: Res, 90 | ) { 91 | let mut rand = thread_rng(); 92 | 93 | for ev in ev_r.iter() { 94 | if ev.skip() { 95 | continue; 96 | } 97 | 98 | if let Some(sfx_handle) = ev.get_sfx_handles(&sfx).choose(&mut rand) { 99 | audio 100 | .play(sfx_handle.clone()) 101 | .with_volume(ev.get_volume() * volume.get_sfx_volume()); 102 | } 103 | } 104 | } 105 | 106 | fn set_sfx_volume(audio: Res>, volume: Res) { 107 | audio.set_volume(volume.get_sfx_volume()); 108 | } 109 | 110 | fn play_drone(audio: Res