├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── appveyor.yml ├── assets ├── DejaVuSans.ttf ├── audio │ └── pistol.ogg ├── character.json ├── character.png ├── maps │ ├── ammo.png │ ├── house.png │ ├── shape.png │ ├── terrain.png │ ├── tilemap.tmx │ └── tree.png ├── preview-gifs │ ├── hinterland-gl-2018-02-26.gif │ ├── hinterland-gl-2018-04-08.gif │ ├── hinterland-gl-2018-04-11.gif │ ├── hinterland-gl-2018-06-17.gif │ ├── hinterland-gl-2018-07-30.gif │ ├── hinterland-gl-2018-09-21.gif │ ├── hinterland-gl-2019-08-28.gif │ ├── hinterland-gl-2019-10-31.gif │ ├── zombie-shooter-gl-2018-01-13.gif │ └── zombie-shooter-gl-2018-01-24.gif ├── zombie.json └── zombie.png ├── ci ├── build_artifact.ps1 └── build_artifact.sh ├── clippy.toml └── src ├── audio └── mod.rs ├── bullet ├── bullets.rs ├── collision.rs └── mod.rs ├── character ├── character_stats.rs ├── controls.rs └── mod.rs ├── critter └── mod.rs ├── data └── mod.rs ├── game ├── constants.rs └── mod.rs ├── gfx_app ├── controls.rs ├── init.rs ├── mod.rs ├── mouse_controls.rs ├── renderer.rs └── system.rs ├── graphics ├── camera.rs ├── dimensions.rs ├── graphics_test.rs ├── mesh.rs ├── mod.rs ├── orientation.rs └── texture.rs ├── hud ├── font.rs ├── hud_objects.rs └── mod.rs ├── main.rs ├── shaders ├── bullet.f.glsl ├── bullet.v.glsl ├── character.f.glsl ├── character.v.glsl ├── mod.rs ├── static_element.f.glsl ├── static_element.v.glsl ├── terrain.f.glsl ├── terrain.v.glsl ├── text.f.glsl └── text.v.glsl ├── terrain ├── mod.rs ├── path_finding.rs └── tile_map.rs ├── terrain_object ├── mod.rs └── terrain_objects.rs ├── terrain_shape ├── mod.rs └── terrain_shape_objects.rs └── zombie ├── mod.rs └── zombies.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # For each file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | indent_style = space 12 | 13 | # For project specific files 14 | [*.{rs,toml,lock,glsl}] 15 | indent_size = 2 16 | max_line_length = 120 17 | trim_trailing_whitespace = true 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .DS_Store 3 | .idea/ 4 | *.iml 5 | /assets/data/ 6 | *.tgz 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | before_cache: 4 | - rm -rf /home/travis/.cargo/registry 5 | 6 | cache: 7 | directories: 8 | - /home/travis/.cargo 9 | 10 | git: 11 | depth: 5 12 | 13 | addons: 14 | apt: 15 | packages: 16 | - libasound2-dev 17 | 18 | matrix: 19 | include: 20 | # Linux 21 | - os: linux 22 | language: rust 23 | rust: stable 24 | env: PACKAGE_NAME="hinterland-$TRAVIS_OS_NAME.tar.gz" 25 | before_script: 26 | - rustup component add clippy-preview 27 | script: 28 | - cargo clippy -- -D warnings 29 | - cargo test 30 | - cargo build --release 31 | before_deploy: ci/build_artifact.sh 32 | 33 | # OSX 34 | - os: osx 35 | language: rust 36 | rust: stable 37 | env: 38 | - TARGET=x86_64-apple-darwin 39 | - PACKAGE_NAME="hinterland-$TRAVIS_OS_NAME.tar.gz" 40 | before_script: 41 | - rustup component add clippy-preview 42 | script: 43 | - cargo clippy -- -D warnings 44 | - cargo test 45 | - cargo build --release 46 | before_deploy: ci/build_artifact.sh 47 | 48 | # Windows 49 | - os: windows 50 | language: rust 51 | rust: stable 52 | env: 53 | - TARGET=x86_64-pc-windows-msvc 54 | - PACKAGE_NAME="hinterland-$TRAVIS_OS_NAME.zip" 55 | before_script: 56 | - rustup component add clippy-preview 57 | script: 58 | - cargo clippy -- -D warnings 59 | - cargo test 60 | - cargo build --release 61 | before_deploy: 62 | - powershell -executionpolicy bypass -File "ci\build_artifact.ps1" 63 | 64 | deploy: 65 | provider: releases 66 | skip_cleanup: true 67 | overwrite: true 68 | api_key: 69 | secure: p7m7Fe9rquO/GcLtbp+vu1HWUG+HxFr0DwBdfW96e/P8RAMH+R0u8QcNjb9n8/tH5K5XZcj2gMAthBjKSASNoXDiJ96ukru2m8hKugXt9QWirhwTJKFj1UUhQeSPFeK9aS99zwgnulYjdfEbWA7IomfH79GF9AKQMx78/fh9SLMz2C5SdfGSE6RXgw8/ASE4yef9UOO6ttCs2Vb3mUEw/mM/aZmGGZ49GCd4qBT2gHjzdXPXgCt18+Cqt/S/aYQia90AEHlK2DUX2w/IauPmNNOu4xqwijz8HXBfyBPxZerRBS0yVqUiMkNRYfLLdFgaWWGnjcOPUpHQMOLTDXO1LfLV0zNSAZPU8MT8cm9lFDJgCZWMYA+h+wxkFQgT6NSH0M/avjwkCpK1NLfhEcX79xKZ/Nv6FhZ+ZjeauVkNIeYL4v4SWteFfzMoCV0+oCmyYnyf1925p7MS5+qKpHUj6pBkJeS0Br17RyFLhGzFYht+5ejz1aStguHqcliYpioKikOa1WV0X+p9VpBH1RMWSCG/DpmZgtveX8kck8O4YKHHs7RYfH7k61TWh503f45Y51N7KlizgCUGmdQWz0ju7jU0e8yr7PZmiAPSYWX82t1D3a53atvethhOAMYc4+LbvPiT8TuHw/rrTEd7RUL41YfzN/KK9IyklhNQEDlE51I= 70 | file: "$PACKAGE_NAME" 71 | on: 72 | tags: true 73 | repo: Laastine/hinterland 74 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | Note: 6 | - OSX version works only when started from command line 7 | - Linux version needs libasound2-dev package installed. 8 | 9 | ## v0.3.12 10 | - Add lighting to game (day and night cycle) 11 | - Print config on start 12 | - Fix Windows build 13 | 14 | ## v0.3.11 15 | - Add command line argument parser 16 | 17 | ## v0.3.10 18 | - Bigger 128x128 map 19 | - Bullets are rendered by shooting direction instead of plain squares 20 | - Update code base for Rust 2018 21 | - Visualise maximum ammo clips 22 | 23 | ## v0.3.9 24 | - Ammunition sprite removed after picking after player picks it up 25 | 26 | ## v0.3.8 27 | - Pickable ammunition and weapon reloading logic 28 | 29 | ## v0.3.7 30 | - Limited ammo capacity visualized 31 | 32 | ## v0.3.6 33 | - Better zombie AI, zombies wander around and engage player only when close enough 34 | 35 | ## v0.3.5 36 | - Fix aiming bug, which occurred in some monitor configurations 37 | - Zombie attack only when player is close enough 38 | 39 | ## v0.3.4 40 | - Fix bullet flying direction bug 41 | 42 | ## v0.3.3 43 | - Character animation speed adjusted 44 | - Adjusted bullet size and collision logic 45 | - Replaced mpsc with crossbeam_channel crate 46 | 47 | ## v0.3.2 48 | - Fixed pathfinding bug 49 | 50 | ## v0.3.1 51 | - Render loop optimized 52 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hinterland" 3 | version = "0.3.12" 4 | authors = ["Mikko Kaistinen "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | cgmath = "0.17.0" 9 | crossbeam-channel = "0.4.4" 10 | getopts = "0.2.21" 11 | gfx_core = "0.9.2" 12 | gfx_device_gl = "0.16.2" 13 | gfx = "0.18.2" 14 | glutin = "0.21.2" 15 | genmesh = "0.6.2" 16 | image = "0.22.3" 17 | json = "0.12.0" 18 | num = "0.2.0" 19 | pathfinding = "2.0.0" 20 | rand = "0.7.2" 21 | rodio = "0.10.0" 22 | rusttype = "0.8.2" 23 | specs = "0.15.1" 24 | tiled = "0.8.1" 25 | 26 | [features] 27 | godmode = [] 28 | framerate = [] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Mikko Kaistinen 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hinterland 2 | 3 | [![Build Status](https://travis-ci.org/Laastine/hinterland.svg?branch=master)](https://travis-ci.org/Laastine/hinterland) 4 | [![Build status](https://ci.appveyor.com/api/projects/status/q30iw99u5f3ua237?svg=true&branch=master)](https://ci.appveyor.com/project/Laastine/hinterland) 5 | 6 | Isometric shooter game written in Rust. Shoot zombies, collect ammo and survive.
Works on Linux, macOS and Windows.
7 | Download precompiled binary from [Github releases](https://github.com/Laastine/hinterland/releases) page. 8 | 9 | ## Preview 10 | 11 | 12 | 13 | ## Build & Run 14 | 15 | `cargo run -- --windowed_mode` 16 | 17 | 18 | ## Command line arguments 19 | 20 | ``` 21 | USAGE: 22 | hinterland [FLAGS] 23 | 24 | FLAGS: 25 | -h, --help Prints help information 26 | -v, --version Prints version information 27 | -w, --windowed_mode Run game in windowed mode 28 | ``` 29 | 30 | ## Controls 31 | 32 | `w,a,s,d` - Character move
33 | `Ctrl + Mouse left` - Fire
34 | `r` - Reload weapon (10 bullets per mag)
35 | `z` - zoom in
36 | `x` - zoom out
37 | `Esc` - exit 38 | 39 | ## Development 40 | 41 | Run windowed mode with `cargo run --features "godmode framerate -- -w` 42 | 43 | `cargo test` - run unit tests 44 | 45 | ## External asset licence list 46 | 47 | * Character: [graphics](http://opengameart.org/content/tmim-heroine-bleeds-game-art) Creative Commons V3 48 | * Zombie [zombie](http://opengameart.org/content/zombie-sprites) Creative Commons V3 49 | * Audio: [pistol](http://opengameart.org/content/chaingun-pistol-rifle-shotgun-shots) Creative Commons V3 50 | * Map: [graphics](http://opengameart.org/content/tiled-terrains) GPL + Creative Commons V3 51 | 52 | ## Source code license 53 | 54 | [Apache License 2.0](https://github.com/Laastine/hinterland/blob/master/LICENSE) 55 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.3.12-{build} 2 | 3 | clone_depth: 5 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | environment: 10 | RUST_BACKTRACE: full 11 | global: 12 | PROJECT_NAME: hinterland 13 | matrix: 14 | - channel: stable 15 | target: x86_64-pc-windows-msvc 16 | - channel: stable 17 | target: x86_64-pc-windows-gnu 18 | 19 | matrix: 20 | allow_failures: 21 | - channel: nightly 22 | 23 | install: 24 | - set PATH=C:\msys64\usr\bin;%PATH% 25 | - set PATH=C:\msys64\mingw32\bin;%PATH% 26 | - if "%TARGET%" == "x86_64-pc-windows-gnu" set PATH=C:\msys64\mingw64\bin;%PATH% 27 | - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 28 | - rustup-init.exe --default-host %TARGET% --default-toolchain stable -y 29 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 30 | - rustc -Vv 31 | 32 | build: false 33 | 34 | test_script: 35 | - cmd: 'cargo test' 36 | - cmd: 'cargo build --release' 37 | 38 | cache: 39 | - target 40 | - C:\Users\appveyor\.cargo 41 | -------------------------------------------------------------------------------- /assets/DejaVuSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/DejaVuSans.ttf -------------------------------------------------------------------------------- /assets/audio/pistol.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/audio/pistol.ogg -------------------------------------------------------------------------------- /assets/character.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/character.png -------------------------------------------------------------------------------- /assets/maps/ammo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/maps/ammo.png -------------------------------------------------------------------------------- /assets/maps/house.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/maps/house.png -------------------------------------------------------------------------------- /assets/maps/shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/maps/shape.png -------------------------------------------------------------------------------- /assets/maps/terrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/maps/terrain.png -------------------------------------------------------------------------------- /assets/maps/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/maps/tree.png -------------------------------------------------------------------------------- /assets/preview-gifs/hinterland-gl-2018-02-26.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/preview-gifs/hinterland-gl-2018-02-26.gif -------------------------------------------------------------------------------- /assets/preview-gifs/hinterland-gl-2018-04-08.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/preview-gifs/hinterland-gl-2018-04-08.gif -------------------------------------------------------------------------------- /assets/preview-gifs/hinterland-gl-2018-04-11.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/preview-gifs/hinterland-gl-2018-04-11.gif -------------------------------------------------------------------------------- /assets/preview-gifs/hinterland-gl-2018-06-17.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/preview-gifs/hinterland-gl-2018-06-17.gif -------------------------------------------------------------------------------- /assets/preview-gifs/hinterland-gl-2018-07-30.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/preview-gifs/hinterland-gl-2018-07-30.gif -------------------------------------------------------------------------------- /assets/preview-gifs/hinterland-gl-2018-09-21.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/preview-gifs/hinterland-gl-2018-09-21.gif -------------------------------------------------------------------------------- /assets/preview-gifs/hinterland-gl-2019-08-28.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/preview-gifs/hinterland-gl-2019-08-28.gif -------------------------------------------------------------------------------- /assets/preview-gifs/hinterland-gl-2019-10-31.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/preview-gifs/hinterland-gl-2019-10-31.gif -------------------------------------------------------------------------------- /assets/preview-gifs/zombie-shooter-gl-2018-01-13.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/preview-gifs/zombie-shooter-gl-2018-01-13.gif -------------------------------------------------------------------------------- /assets/preview-gifs/zombie-shooter-gl-2018-01-24.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/preview-gifs/zombie-shooter-gl-2018-01-24.gif -------------------------------------------------------------------------------- /assets/zombie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Laastine/hinterland/39a2c957cdb93c09725a401bec957e254a65f368/assets/zombie.png -------------------------------------------------------------------------------- /ci/build_artifact.ps1: -------------------------------------------------------------------------------- 1 | mkdir hinterland 2 | mkdir hinterland\assets 3 | copy assets\*.png hinterland\assets 4 | copy assets\*.json hinterland\assets 5 | copy -Recurse assets\maps hinterland\assets\maps 6 | copy -Recurse assets\audio hinterland\assets\audio 7 | copy target\release\hinterland.exe hinterland\hinterland.exe 8 | 7z a -tzip hinterland-windows.zip hinterland 9 | -------------------------------------------------------------------------------- /ci/build_artifact.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | echo "Building $PACKAGE_NAME" 6 | 7 | mkdir -p hinterland/assets 8 | cp assets/*.png hinterland/assets 9 | cp assets/*.json hinterland/assets 10 | cp -r assets/maps hinterland/assets 11 | cp -r assets/audio hinterland/assets 12 | cp target/release/hinterland hinterland 13 | tar zcf $PACKAGE_NAME hinterland 14 | rm -rf hinterland 15 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | type-complexity-threshold = 16384 2 | literal-representation-threshold = 0 3 | too-many-arguments-threshold = 10 4 | -------------------------------------------------------------------------------- /src/audio/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::BufReader}; 2 | 3 | use crossbeam_channel as channel; 4 | use rodio::Sink; 5 | use specs::prelude::ReadStorage; 6 | 7 | use crate::character::{CharacterDrawable, controls::CharacterInputState}; 8 | use crate::game::constants::PISTOL_AUDIO_PATH; 9 | 10 | #[derive(Clone, Copy, PartialEq)] 11 | pub enum Effects { 12 | PistolFire, 13 | None, 14 | } 15 | 16 | pub struct AudioSystem { 17 | effects: Effects, 18 | sink: Sink, 19 | queue: channel::Receiver, 20 | } 21 | 22 | impl AudioSystem { 23 | pub fn new() -> (AudioSystem, channel::Sender) { 24 | #[allow(deprecated)] 25 | let (tx, rx) = channel::unbounded(); 26 | let endpoint = rodio::default_output_device().unwrap(); 27 | 28 | (AudioSystem { 29 | effects: Effects::None, 30 | sink: Sink::new(&endpoint), 31 | queue: rx, 32 | }, tx) 33 | } 34 | 35 | fn play_effect(&mut self) { 36 | let file = File::open(PISTOL_AUDIO_PATH).unwrap(); 37 | let pistol_data = rodio::Decoder::new(BufReader::new(file)).unwrap(); 38 | if self.sink.empty() { 39 | self.sink.append(pistol_data); 40 | } 41 | } 42 | } 43 | 44 | impl<'a> specs::prelude::System<'a> for AudioSystem { 45 | type SystemData = (ReadStorage<'a, CharacterInputState>, 46 | ReadStorage<'a, CharacterDrawable>); 47 | 48 | fn run(&mut self, (character_input, character_drawable): Self::SystemData) { 49 | use specs::join::Join; 50 | 51 | while let Ok(effect) = self.queue.try_recv() { 52 | match effect { 53 | Effects::PistolFire => self.effects = Effects::PistolFire, 54 | _ => self.effects = Effects::None, 55 | } 56 | } 57 | 58 | for (ci, cd) in (&character_input, &character_drawable).join() { 59 | if let Effects::PistolFire = self.effects { 60 | if ci.is_shooting && cd.stats.ammunition > 0 { self.play_effect() } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/bullet/bullets.rs: -------------------------------------------------------------------------------- 1 | 2 | use crate::bullet::{BulletDrawable, collision::Collision}; 3 | use crate::graphics::direction_movement; 4 | use crate::shaders::Position; 5 | 6 | pub struct Bullets { 7 | pub bullets: Vec, 8 | } 9 | 10 | impl Bullets { 11 | pub fn new() -> Bullets { 12 | Bullets { 13 | bullets: Vec::new() 14 | } 15 | } 16 | 17 | pub fn add_bullet(&mut self, position: Position, direction: f32) { 18 | let movement_direction = direction_movement(direction); 19 | self.bullets.push(BulletDrawable::new(position, movement_direction, direction)); 20 | } 21 | 22 | pub fn remove_old_bullets(&mut self) { 23 | self.bullets.retain(|ref mut b| b.status == Collision::Flying); 24 | } 25 | } 26 | 27 | impl specs::prelude::Component for Bullets { 28 | type Storage = specs::storage::VecStorage; 29 | } 30 | -------------------------------------------------------------------------------- /src/bullet/collision.rs: -------------------------------------------------------------------------------- 1 | use specs::prelude::WriteStorage; 2 | 3 | use crate::bullet::bullets::Bullets; 4 | 5 | pub struct CollisionSystem; 6 | 7 | #[derive(Clone, PartialEq)] 8 | pub enum Collision { 9 | Flying, 10 | Hit, 11 | OutOfBounds, 12 | } 13 | 14 | impl<'a> specs::prelude::System<'a> for CollisionSystem { 15 | type SystemData = WriteStorage<'a, Bullets>; 16 | 17 | fn run(&mut self, mut bullets: Self::SystemData) { 18 | use specs::join::Join; 19 | 20 | for bs in (&mut bullets).join() { 21 | Bullets::remove_old_bullets(bs); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/bullet/mod.rs: -------------------------------------------------------------------------------- 1 | use std::f32; 2 | use std::f32::consts::PI; 3 | 4 | use cgmath::Point2; 5 | use specs::prelude::{Read, ReadStorage, WriteStorage}; 6 | 7 | use crate::bullet::bullets::Bullets; 8 | use crate::bullet::collision::Collision; 9 | use crate::character::controls::CharacterInputState; 10 | use crate::game::constants::{ASPECT_RATIO, BULLET_SPEED, VIEW_DISTANCE}; 11 | use crate::gfx_app::{ColorFormat, DepthFormat}; 12 | use crate::graphics::{camera::CameraInputState, can_move, dimensions::{Dimensions, get_projection, get_view_matrix}}; 13 | use crate::graphics::can_move_to_tile; 14 | use crate::graphics::mesh::PlainMesh; 15 | use crate::shaders::{bullet_pipeline, Position, Projection, Rotation}; 16 | 17 | pub mod bullets; 18 | pub mod collision; 19 | 20 | const SHADER_VERT: &[u8] = include_bytes!("../shaders/bullet.v.glsl"); 21 | const SHADER_FRAG: &[u8] = include_bytes!("../shaders/bullet.f.glsl"); 22 | 23 | const SCALING_FACTOR: f32 = 5.0 / 3.0; 24 | 25 | #[derive(PartialEq)] 26 | pub struct BulletDrawable { 27 | projection: Projection, 28 | pub position: Position, 29 | pub rotation: Rotation, 30 | previous_position: Position, 31 | offset_delta: Position, 32 | pub movement_direction: Point2, 33 | pub status: collision::Collision, 34 | } 35 | 36 | impl BulletDrawable { 37 | pub fn new(position: Position, movement_direction: Point2, direction: f32) -> BulletDrawable { 38 | let view = get_view_matrix(VIEW_DISTANCE); 39 | let projection = get_projection(view, ASPECT_RATIO); 40 | let rotation = Rotation::new(direction * PI / 180.0); 41 | BulletDrawable { 42 | projection, 43 | position, 44 | rotation, 45 | previous_position: Position::origin(), 46 | offset_delta: Position::origin(), 47 | movement_direction, 48 | status: Collision::Flying, 49 | } 50 | } 51 | 52 | pub fn update(&mut self, world_to_clip: &Projection, ci: &CharacterInputState) { 53 | self.projection = *world_to_clip; 54 | 55 | self.offset_delta = 56 | if (ci.movement.x() - self.previous_position.x()).abs() > f32::EPSILON || 57 | (ci.movement.y() - self.previous_position.y()).abs() > f32::EPSILON { 58 | ci.movement - self.previous_position 59 | } else { 60 | self.offset_delta 61 | }; 62 | 63 | self.previous_position = Position::new( 64 | ci.movement.x() - (self.movement_direction.x * BULLET_SPEED / SCALING_FACTOR), 65 | ci.movement.y() + (self.movement_direction.y * BULLET_SPEED)); 66 | 67 | self.position = self.position + self.offset_delta + 68 | Position::new(self.movement_direction.x * BULLET_SPEED / SCALING_FACTOR, -self.movement_direction.y * BULLET_SPEED); 69 | 70 | let tile_pos = ci.movement - self.position; 71 | 72 | self.status = if !can_move(self.position) { 73 | Collision::OutOfBounds 74 | } else if !can_move_to_tile(tile_pos) { 75 | Collision::Hit 76 | } else { 77 | Collision::Flying 78 | } 79 | } 80 | } 81 | 82 | pub struct BulletDrawSystem { 83 | bundle: gfx::pso::bundle::Bundle>, 84 | } 85 | 86 | impl BulletDrawSystem { 87 | pub fn new(factory: &mut F, 88 | rtv: gfx::handle::RenderTargetView, 89 | dsv: gfx::handle::DepthStencilView) -> BulletDrawSystem 90 | where F: gfx::Factory { 91 | use gfx::traits::FactoryExt; 92 | 93 | let mesh = PlainMesh::new_with_data(factory, Point2::new(2.4, 0.8), None, None, None); 94 | 95 | let pso = factory.create_pipeline_simple(SHADER_VERT, SHADER_FRAG, bullet_pipeline::new()) 96 | .expect("Bullet shader loading error"); 97 | 98 | let pipeline_data = bullet_pipeline::Data { 99 | vbuf: mesh.vertex_buffer, 100 | projection_cb: factory.create_constant_buffer(1), 101 | position_cb: factory.create_constant_buffer(1), 102 | rotation_cb: factory.create_constant_buffer(1), 103 | out_color: rtv, 104 | out_depth: dsv, 105 | }; 106 | 107 | BulletDrawSystem { 108 | bundle: gfx::Bundle::new(mesh.slice, pso, pipeline_data), 109 | } 110 | } 111 | 112 | pub fn draw(&mut self, 113 | drawable: &BulletDrawable, 114 | encoder: &mut gfx::Encoder) 115 | where C: gfx::CommandBuffer { 116 | encoder.update_constant_buffer(&self.bundle.data.projection_cb, &drawable.projection); 117 | encoder.update_constant_buffer(&self.bundle.data.position_cb, &drawable.position); 118 | encoder.update_constant_buffer(&self.bundle.data.rotation_cb, &drawable.rotation); 119 | self.bundle.encode(encoder); 120 | } 121 | } 122 | 123 | pub struct PreDrawSystem; 124 | 125 | impl<'a> specs::prelude::System<'a> for PreDrawSystem { 126 | type SystemData = (ReadStorage<'a, CameraInputState>, 127 | WriteStorage<'a, Bullets>, 128 | ReadStorage<'a, CharacterInputState>, 129 | Read<'a, Dimensions>); 130 | 131 | fn run(&mut self, (camera_input, mut bullets, character_input, dim): Self::SystemData) { 132 | use specs::join::Join; 133 | 134 | for (camera, bs, ci) in (&camera_input, &mut bullets, &character_input).join() { 135 | let world_to_clip = dim.world_to_projection(camera); 136 | 137 | for b in &mut bs.bullets { 138 | b.update(&world_to_clip, ci); 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/character/character_stats.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Default)] 2 | pub struct CharacterStats { 3 | pub ammunition: usize, 4 | pub magazines: usize, 5 | } 6 | 7 | impl CharacterStats { 8 | pub fn new() -> CharacterStats { 9 | CharacterStats { 10 | ammunition: 10, 11 | magazines: 1, 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/character/controls.rs: -------------------------------------------------------------------------------- 1 | use crossbeam_channel as channel; 2 | use specs::prelude::{Read, WriteStorage}; 3 | 4 | use crate::character::CharacterDrawable; 5 | use crate::game::constants::{CHARACTER_X_SPEED, CHARACTER_Y_SPEED}; 6 | use crate::graphics::{camera::CameraInputState, can_move_to_tile, DeltaTime, orientation::{Orientation, Stance}}; 7 | use crate::shaders::Position; 8 | 9 | pub struct CharacterInputState { 10 | pub movement: Position, 11 | pub orientation: Orientation, 12 | pub is_colliding: bool, 13 | pub is_shooting: bool, 14 | } 15 | 16 | impl CharacterInputState { 17 | pub fn new() -> CharacterInputState { 18 | CharacterInputState { 19 | movement: Position::origin(), 20 | orientation: Orientation::Normal, 21 | is_colliding: false, 22 | is_shooting: false, 23 | } 24 | } 25 | 26 | pub fn update(&mut self, camera: &mut CameraInputState, css: &CharacterControlSystem) { 27 | if css.y_move.is_none() && css.x_move.is_none() { 28 | self.orientation = Orientation::Normal; 29 | } else if css.x_move.is_none() { // Horizontal/vertical movement 30 | if let Some(y) = css.y_move { 31 | let vertical_movement = self.movement + Position::new(0.0, y); 32 | if !self.is_colliding || can_move_to_tile(vertical_movement) { 33 | self.movement = vertical_movement; 34 | camera.movement = camera.movement - Position::new(0.0, y); 35 | self.orientation = match y { 36 | y if y < 0.0 => Orientation::Up, 37 | y if y > 0.0 => Orientation::Down, 38 | _ => Orientation::Normal, 39 | }; 40 | } 41 | } 42 | } else if let Some(x) = css.x_move { // Diagonal movement 43 | let horizontal_move = self.movement + Position::new(x, 0.0); 44 | if let Some(y) = css.y_move { 45 | let horizontal_movement = Position::new(x / 1.5, 0.0); 46 | let vertical_movement = Position::new(0.0, y / 1.666); 47 | if !self.is_colliding || can_move_to_tile(self.movement + horizontal_movement + vertical_movement) { 48 | self.movement = self.movement + horizontal_movement + vertical_movement; 49 | camera.movement = camera.movement + horizontal_movement - vertical_movement; 50 | 51 | self.orientation = match (x, y) { 52 | (x, y) if x > 0.0 && y > 0.0 => Orientation::DownLeft, 53 | (x, y) if x > 0.0 && y < 0.0 => Orientation::UpLeft, 54 | (x, y) if x < 0.0 && y > 0.0 => Orientation::DownRight, 55 | (x, y) if x < 0.0 && y < 0.0 => Orientation::UpRight, 56 | _ => Orientation::Normal, 57 | }; 58 | } 59 | } else if css.y_move.is_none() && !self.is_colliding || can_move_to_tile(horizontal_move) { 60 | let horizontal_movement = Position::new(x, 0.0); 61 | self.movement = self.movement + horizontal_movement; 62 | camera.movement = camera.movement + horizontal_movement; 63 | self.orientation = match x { 64 | x if x < 0.0 => Orientation::Right, 65 | x if x > 0.0 => Orientation::Left, 66 | _ => Orientation::Normal, 67 | }; 68 | } 69 | } 70 | self.is_shooting = css.is_ctrl_pressed; 71 | } 72 | } 73 | 74 | impl Default for CharacterInputState { 75 | fn default() -> Self { 76 | CharacterInputState::new() 77 | } 78 | } 79 | 80 | impl specs::prelude::Component for CharacterInputState { 81 | type Storage = specs::storage::VecStorage; 82 | } 83 | 84 | pub enum CharacterControl { 85 | Left, 86 | Right, 87 | Up, 88 | Down, 89 | XMoveStop, 90 | YMoveStop, 91 | CtrlPressed, 92 | CtrlReleased, 93 | ReloadPressed, 94 | ReloadReleased, 95 | } 96 | 97 | pub struct CharacterControlSystem { 98 | queue: channel::Receiver, 99 | x_move: Option, 100 | y_move: Option, 101 | cool_down: f64, 102 | is_ctrl_pressed: bool, 103 | is_reloading: bool, 104 | } 105 | 106 | impl CharacterControlSystem { 107 | pub fn new() -> (CharacterControlSystem, channel::Sender) { 108 | let (tx, rx) = channel::unbounded(); 109 | (CharacterControlSystem { 110 | queue: rx, 111 | x_move: None, 112 | y_move: None, 113 | cool_down: 1.0, 114 | is_ctrl_pressed: false, 115 | is_reloading: false, 116 | }, tx) 117 | } 118 | } 119 | 120 | impl<'a> specs::prelude::System<'a> for CharacterControlSystem { 121 | type SystemData = (WriteStorage<'a, CharacterInputState>, 122 | WriteStorage<'a, CharacterDrawable>, 123 | WriteStorage<'a, CameraInputState>, 124 | Read<'a, DeltaTime>); 125 | 126 | fn run(&mut self, (mut character_input, mut character, mut camera_input, d): Self::SystemData) { 127 | use specs::join::Join; 128 | 129 | let delta = d.0; 130 | 131 | if self.cool_down == 0.0 { 132 | self.cool_down += 0.1; 133 | } else { 134 | self.cool_down = (self.cool_down - delta).max(0.0); 135 | while let Ok(control) = self.queue.try_recv() { 136 | match control { 137 | CharacterControl::Up => self.y_move = Some(-CHARACTER_Y_SPEED), 138 | CharacterControl::Down => self.y_move = Some(CHARACTER_Y_SPEED), 139 | CharacterControl::YMoveStop => self.y_move = None, 140 | CharacterControl::Right => self.x_move = Some(-CHARACTER_X_SPEED), 141 | CharacterControl::Left => self.x_move = Some(CHARACTER_X_SPEED), 142 | CharacterControl::XMoveStop => self.x_move = None, 143 | CharacterControl::CtrlPressed => self.is_ctrl_pressed = true, 144 | CharacterControl::CtrlReleased => self.is_ctrl_pressed = false, 145 | CharacterControl::ReloadPressed => self.is_reloading = true, 146 | CharacterControl::ReloadReleased => self.is_reloading = false, 147 | } 148 | } 149 | 150 | for (ci, c, camera) in (&mut character_input, &mut character, &mut camera_input).join() { 151 | if c.stance != Stance::NormalDeath { 152 | ci.update(camera, self); 153 | } 154 | if self.is_reloading && c.stats.magazines > 0 && c.stats.ammunition < 10 { 155 | c.stats.ammunition = 10; 156 | c.stats.magazines -= 1; 157 | } 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/character/mod.rs: -------------------------------------------------------------------------------- 1 | use cgmath::Point2; 2 | use specs::prelude::{Read, ReadStorage, WriteStorage}; 3 | 4 | use crate::character::{character_stats::CharacterStats, controls::CharacterInputState}; 5 | use crate::critter::{CharacterSprite, CritterData}; 6 | use crate::data; 7 | use crate::game::constants::{AMMO_POSITIONS, ASPECT_RATIO, CHARACTER_SHEET_TOTAL_WIDTH, RUN_SPRITE_OFFSET, SPRITE_OFFSET, VIEW_DISTANCE, SMALL_HILLS}; 8 | use crate::gfx_app::{ColorFormat, DepthFormat}; 9 | use crate::gfx_app::mouse_controls::MouseInputState; 10 | use crate::graphics::{camera::CameraInputState, dimensions::{Dimensions, get_projection, get_view_matrix}, get_orientation_from_center, orientation::{Orientation, Stance}, overlaps, texture::load_texture, check_terrain_elevation}; 11 | use crate::graphics::mesh::{RectangularTexturedMesh, Geometry}; 12 | use crate::graphics::texture::Texture; 13 | use crate::shaders::{CharacterSheet, critter_pipeline, Position, Projection}; 14 | use crate::terrain_object::{terrain_objects::TerrainObjects, TerrainObjectDrawable, TerrainTexture}; 15 | use crate::zombie::{ZombieDrawable, zombies::Zombies}; 16 | 17 | pub mod controls; 18 | mod character_stats; 19 | 20 | const SHADER_VERT: &[u8] = include_bytes!("../shaders/character.v.glsl"); 21 | const SHADER_FRAG: &[u8] = include_bytes!("../shaders/character.f.glsl"); 22 | 23 | #[derive(Clone)] 24 | pub struct CharacterDrawable { 25 | pub stats: CharacterStats, 26 | projection: Projection, 27 | pub position: Position, 28 | orientation: Orientation, 29 | pub stance: Stance, 30 | direction: Orientation, 31 | } 32 | 33 | impl CharacterDrawable { 34 | pub fn new() -> CharacterDrawable { 35 | let view = get_view_matrix(VIEW_DISTANCE); 36 | let projection = get_projection(view, ASPECT_RATIO); 37 | let stats = CharacterStats::new(); 38 | CharacterDrawable { 39 | stats, 40 | projection, 41 | position: Position::origin(), 42 | orientation: Orientation::Right, 43 | stance: Stance::Walking, 44 | direction: Orientation::Right, 45 | } 46 | } 47 | 48 | pub fn update(&mut self, world_to_clip: &Projection, ci: &CharacterInputState, mouse_input: &MouseInputState, 49 | dimensions: &Dimensions, objs: &mut Vec, zombies: &[ZombieDrawable]) { 50 | self.projection = *world_to_clip; 51 | 52 | self.position.position[1] = check_terrain_elevation(ci.movement - self.position, &SMALL_HILLS); 53 | 54 | fn zombie_not_dead(z: &ZombieDrawable) -> bool { 55 | z.stance != Stance::NormalDeath && 56 | z.stance != Stance::CriticalDeath 57 | } 58 | 59 | for idx in 0..AMMO_POSITIONS.len() { 60 | self.ammo_pick_up(ci.movement, objs, idx); 61 | } 62 | 63 | if !cfg!(feature = "godmode") && 64 | zombies.iter() 65 | .any(|z| 66 | zombie_not_dead(z) && 67 | overlaps(ci.movement, 68 | ci.movement - z.position, 69 | 15.0, 70 | 30.0)) { 71 | self.stance = Stance::NormalDeath; 72 | println!("Player died"); 73 | std::process::exit(0); 74 | } 75 | 76 | if ci.is_shooting && mouse_input.left_click_point.is_some() && !ci.is_colliding { 77 | self.stance = Stance::Firing; 78 | self.orientation = get_orientation_from_center(mouse_input, dimensions); 79 | } else if ci.is_colliding { 80 | self.stance = Stance::Still; 81 | } else { 82 | self.stance = Stance::Walking; 83 | self.orientation = ci.orientation; 84 | } 85 | } 86 | 87 | fn ammo_pick_up(&mut self, movement: Position, objs: &mut Vec, idx: usize) { 88 | if objs.len() > idx && objs[idx].object_type == TerrainTexture::Ammo && overlaps(movement, movement - objs[idx].position, 20.0, 20.0) { 89 | self.stats.magazines = 2; 90 | objs.remove(idx); 91 | } 92 | } 93 | } 94 | 95 | impl Default for CharacterDrawable { 96 | fn default() -> Self { 97 | CharacterDrawable::new() 98 | } 99 | } 100 | 101 | impl specs::prelude::Component for CharacterDrawable { 102 | type Storage = specs::storage::VecStorage; 103 | } 104 | 105 | pub struct CharacterDrawSystem { 106 | bundle: gfx::pso::bundle::Bundle>, 107 | data: Vec, 108 | } 109 | 110 | impl CharacterDrawSystem { 111 | pub fn new(factory: &mut F, 112 | rtv: gfx::handle::RenderTargetView, 113 | dsv: gfx::handle::DepthStencilView) -> CharacterDrawSystem 114 | where F: gfx::Factory { 115 | use gfx::traits::FactoryExt; 116 | 117 | let charter_bytes = &include_bytes!("../../assets/character.png")[..]; 118 | let char_texture = load_texture(factory, charter_bytes); 119 | 120 | let rect_mesh = 121 | RectangularTexturedMesh::new(factory, Texture::new(char_texture, None), Geometry::Rectangle, Point2::new(20.0, 28.0), None, None, None); 122 | 123 | let pso = factory.create_pipeline_simple(SHADER_VERT, SHADER_FRAG, critter_pipeline::new()) 124 | .expect("Character shader loading error"); 125 | 126 | let pipeline_data = critter_pipeline::Data { 127 | vbuf: rect_mesh.mesh.vertex_buffer, 128 | projection_cb: factory.create_constant_buffer(1), 129 | position_cb: factory.create_constant_buffer(1), 130 | character_sprite_cb: factory.create_constant_buffer(1), 131 | charactersheet: (rect_mesh.mesh.texture.raw, factory.create_sampler_linear()), 132 | out_color: rtv, 133 | out_depth: dsv, 134 | }; 135 | 136 | let data = data::load_character(); 137 | 138 | CharacterDrawSystem { 139 | bundle: gfx::Bundle::new(rect_mesh.mesh.slice, pso, pipeline_data), 140 | data, 141 | } 142 | } 143 | 144 | fn get_next_sprite(&self, character_idx: usize, character_fire_idx: usize, drawable: &mut CharacterDrawable) -> CharacterSheet { 145 | let sprite_idx = 146 | if drawable.orientation == Orientation::Normal && drawable.stance == Stance::Walking { 147 | drawable.direction as usize * 28 + RUN_SPRITE_OFFSET 148 | } else if drawable.stance == Stance::Walking { 149 | drawable.direction = drawable.orientation; 150 | drawable.orientation as usize * 28 + character_idx + RUN_SPRITE_OFFSET 151 | } else { 152 | drawable.orientation as usize * 8 + character_fire_idx 153 | } as usize; 154 | 155 | let elements_x = CHARACTER_SHEET_TOTAL_WIDTH / (self.data[sprite_idx].data[2] + SPRITE_OFFSET); 156 | CharacterSheet { 157 | x_div: elements_x, 158 | y_div: 0.0, 159 | row_idx: 0, 160 | index: sprite_idx as f32, 161 | } 162 | } 163 | 164 | pub fn draw(&mut self, 165 | mut drawable: &mut CharacterDrawable, 166 | character: &CharacterSprite, 167 | encoder: &mut gfx::Encoder) 168 | where C: gfx::CommandBuffer { 169 | encoder.update_constant_buffer(&self.bundle.data.projection_cb, &drawable.projection); 170 | encoder.update_constant_buffer(&self.bundle.data.position_cb, &drawable.position); 171 | encoder.update_constant_buffer(&self.bundle.data.character_sprite_cb, 172 | &self.get_next_sprite(character.character_idx, 173 | character.character_fire_idx, 174 | &mut drawable)); 175 | self.bundle.encode(encoder); 176 | } 177 | } 178 | 179 | pub struct PreDrawSystem; 180 | 181 | impl<'a> specs::prelude::System<'a> for PreDrawSystem { 182 | type SystemData = (WriteStorage<'a, CharacterDrawable>, 183 | ReadStorage<'a, CameraInputState>, 184 | ReadStorage<'a, CharacterInputState>, 185 | ReadStorage<'a, MouseInputState>, 186 | WriteStorage<'a, TerrainObjects>, 187 | ReadStorage<'a, Zombies>, 188 | Read<'a, Dimensions>); 189 | 190 | fn run(&mut self, (mut character, camera_input, character_input, mouse_input, mut terrain_objects, zombies, dim): Self::SystemData) { 191 | use specs::join::Join; 192 | 193 | for (c, camera, ci, mi, to, zs) in 194 | (&mut character, &camera_input, &character_input, &mouse_input, &mut terrain_objects, &zombies).join() { 195 | let world_to_clip = dim.world_to_projection(camera); 196 | c.update(&world_to_clip, ci, mi, &dim, &mut to.objects, &zs.zombies); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/critter/mod.rs: -------------------------------------------------------------------------------- 1 | pub struct CharacterSprite { 2 | pub character_idx: usize, 3 | pub character_fire_idx: usize, 4 | } 5 | 6 | impl CharacterSprite { 7 | pub fn new() -> CharacterSprite { 8 | CharacterSprite { 9 | character_idx: 0, 10 | character_fire_idx: 0, 11 | } 12 | } 13 | 14 | pub fn update_run(&mut self) { 15 | if self.character_idx < 12 { 16 | self.character_idx += 1; 17 | } else { 18 | self.character_idx = 0; 19 | } 20 | self.character_fire_idx = 0; 21 | } 22 | 23 | pub fn update_fire(&mut self) { 24 | if self.character_fire_idx < 3 { 25 | self.character_fire_idx += 1; 26 | } else { 27 | self.character_fire_idx = 0; 28 | } 29 | } 30 | } 31 | 32 | impl specs::prelude::Component for CharacterSprite { 33 | type Storage = specs::storage::VecStorage; 34 | } 35 | 36 | pub struct CritterData { 37 | pub data: [f32; 4] 38 | } 39 | 40 | impl CritterData { 41 | pub fn new(data: [f32; 4]) -> CritterData { 42 | CritterData { data } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/data/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::BufReader, io::prelude::*, path::Path, string::String, vec::Vec}; 2 | 3 | use json::JsonValue; 4 | use tiled::Map; 5 | 6 | use crate::critter::CritterData; 7 | use crate::game::constants::{CHARACTER_BUF_LENGTH, CHARACTER_JSON_PATH, ZOMBIE_JSON_PATH}; 8 | 9 | pub fn load_map_file(filename: &str) -> Map { 10 | let file = match File::open(&Path::new(&filename)) { 11 | Ok(f) => f, 12 | Err(e) => panic!("File {} not found: {}", filename, e), 13 | }; 14 | let reader = BufReader::new(file); 15 | match tiled::parse(reader) { 16 | Ok(m) => m, 17 | Err(e) => panic!("Map parse error {:?}", e) 18 | } 19 | } 20 | 21 | pub fn get_map_tile(map: &Map, layer_index: usize, x: usize, y: usize) -> u32 { 22 | let layer = match map.layers.get(layer_index) { 23 | None => panic!("Layer_index value out of index {:?}", map.layers), 24 | Some(l) => l 25 | }; 26 | let y_index = match layer.tiles.iter().rev().nth(y) { 27 | None => panic!("X value out of index {:?}", map.layers[0]), 28 | Some(x) => x 29 | }; 30 | match y_index.get(x) { 31 | None => panic!("Y value out of index {:?}", layer.tiles[y]), 32 | Some(val) => *val 33 | } 34 | } 35 | 36 | fn read_sprite_file(filename: &str) -> String { 37 | let path = Path::new(&filename); 38 | let mut file = match File::open(&path) { 39 | Ok(f) => f, 40 | Err(e) => panic!("File {} not found: {}", filename, e), 41 | }; 42 | let mut buf = String::new(); 43 | match file.read_to_string(&mut buf) { 44 | Ok(_) => buf, 45 | Err(e) => panic!("read file {} error {}", filename, e), 46 | } 47 | } 48 | 49 | fn get_frame_data(character: &JsonValue, key: &str) -> CritterData { 50 | CritterData::new([ 51 | character["frames"][key]["frame"]["x"].as_f32().unwrap(), 52 | character["frames"][key]["frame"]["y"].as_f32().unwrap(), 53 | character["frames"][key]["frame"]["w"].as_f32().unwrap(), 54 | character["frames"][key]["frame"]["h"].as_f32().unwrap(), 55 | ]) 56 | } 57 | 58 | pub fn load_character() -> Vec { 59 | let mut sprites = Vec::with_capacity(CHARACTER_BUF_LENGTH + 64); 60 | let character_json = read_sprite_file(CHARACTER_JSON_PATH); 61 | let character = match json::parse(&character_json) { 62 | Ok(res) => res, 63 | Err(e) => panic!("Character {} parse error {:?}", CHARACTER_JSON_PATH, e), 64 | }; 65 | 66 | for x in 0..16 { 67 | for y in 0..14 { 68 | let key = &format!("run_{}_{}", x, y); 69 | sprites.push(get_frame_data(&character, key)); 70 | } 71 | } 72 | 73 | for x in 0..15 { 74 | for y in 0..4 { 75 | let key = &format!("fire_{}_{}", x, y); 76 | sprites.push(get_frame_data(&character, key)); 77 | } 78 | } 79 | 80 | sprites 81 | } 82 | 83 | pub fn load_zombie() -> Vec { 84 | let mut sprites = Vec::with_capacity(256); 85 | let zombie_json = read_sprite_file(ZOMBIE_JSON_PATH); 86 | let zombie = match json::parse(&zombie_json) { 87 | Ok(res) => res, 88 | Err(e) => panic!("Zombie {} parse error {:?}", ZOMBIE_JSON_PATH, e), 89 | }; 90 | 91 | for x in 0..7 { 92 | for y in 0..7 { 93 | let key = &format!("critical_{}_{}", x, y); 94 | sprites.push(get_frame_data(&zombie, key)); 95 | } 96 | } 97 | for x in 0..7 { 98 | for y in 0..5 { 99 | let key = &format!("normal_{}_{}", x, y); 100 | sprites.push(get_frame_data(&zombie, key)); 101 | } 102 | } 103 | for x in 0..7 { 104 | for y in 0..4 { 105 | let key = &format!("still_{}_{}", x, y); 106 | sprites.push(get_frame_data(&zombie, key)); 107 | } 108 | } 109 | 110 | for x in 0..7 { 111 | for y in 0..7 { 112 | let key = &format!("walk_{}_{}", x, y); 113 | sprites.push(get_frame_data(&zombie, key)); 114 | } 115 | } 116 | sprites 117 | } 118 | -------------------------------------------------------------------------------- /src/game/constants.rs: -------------------------------------------------------------------------------- 1 | pub const TILES_PCS_W: usize = 128; 2 | pub const TILES_PCS_H: usize = 128; 3 | 4 | pub const TILE_SIZE: f32 = 48.0; 5 | pub const TILE_WIDTH: f32 = TILE_SIZE * 2.0; 6 | 7 | pub const Y_OFFSET: f32 = TILES_PCS_W as f32 / 2.0 * TILE_WIDTH; 8 | 9 | pub const CHARACTER_BUF_LENGTH: usize = 224; 10 | 11 | pub const RESOLUTION_X: u32 = 1600; 12 | pub const RESOLUTION_Y: u32 = 900; 13 | 14 | pub const ASPECT_RATIO: f32 = (RESOLUTION_X / RESOLUTION_Y) as f32; 15 | 16 | pub const VIEW_DISTANCE: f32 = 300.0; 17 | 18 | pub const CHARACTER_SHEET_TOTAL_WIDTH: f32 = 16_128f32; 19 | pub const SPRITE_OFFSET: f32 = 2.0; 20 | 21 | pub const ZOMBIE_SHEET_TOTAL_WIDTH: f32 = 9_184f32; 22 | 23 | pub const BULLET_SPEED: f32 = 15.0; 24 | pub const CHARACTER_X_SPEED: f32 = 3.0; 25 | pub const CHARACTER_Y_SPEED: f32 = 3.0; 26 | 27 | pub const GAME_TITLE: &str = "Hinterland"; 28 | 29 | //Assets 30 | pub const ZOMBIE_JSON_PATH: &str = "assets/zombie.json"; 31 | pub const CHARACTER_JSON_PATH: &str = "assets/character.json"; 32 | pub const PISTOL_AUDIO_PATH: &str = "assets/audio/pistol.ogg"; 33 | pub const MAP_FILE_PATH: &str = "assets/maps/tilemap.tmx"; 34 | 35 | pub const RUN_SPRITE_OFFSET: usize = 64; 36 | pub const ZOMBIE_STILL_SPRITE_OFFSET: usize = 32; 37 | pub const NORMAL_DEATH_SPRITE_OFFSET: usize = 64; 38 | 39 | // Object positions 40 | pub const AMMO_POSITIONS: [[i32; 2]; 4] = [ [ -13, -12 ], [ -15, 8 ], [ 16, -8 ], [ 1, 14 ] ]; 41 | pub const HOUSE_POSITIONS: [[i32; 2]; 2] = [[1, 17], [10, 5]]; 42 | pub const TREE_POSITIONS: [[i32; 2]; 5] = [[-11, -5], [8, -8], [-14, -11], [-18, -2], [-14, 3]]; 43 | 44 | pub const TERRAIN_OBJECTS: [[i32; 2]; 13] = [ 45 | [ 55, 54 ], [ 56, 54 ], // House A 46 | [ 55, 55 ], [ 56, 55 ], // House A 47 | [ 66, 57 ], [ 67, 57 ], // House B 48 | [ 66, 56 ], [ 67, 56 ], // House B 49 | [ 72, 65 ], [ 61, 73 ], [ 63, 77 ], [ 56, 70 ], [ 56, 74 ] // Trees 50 | ]; 51 | 52 | pub const SMALL_HILLS: [[i32; 2]; 3] = [[4, 2], [20, -2], [-14, -6]]; 53 | 54 | pub const GAME_VERSION: &str = "v0.3.12"; 55 | 56 | pub const HUD_TEXTS: [&str; 15] = [GAME_VERSION, "Ammo 0", "Ammo 1", "Ammo 2", "Ammo 3", 57 | "Ammo 4", "Ammo 5", "Ammo 6", 58 | "Ammo 7", "Ammo 8", "Ammo 9", "Ammo 10", 59 | "Magazines 0/2", "Magazines 1/2", "Magazines 2/2"]; 60 | 61 | pub const CURRENT_AMMO_TEXT: &str = "Ammo 10"; 62 | pub const CURRENT_MAGAZINE_TEXT: &str = "Magazines 2/2"; 63 | -------------------------------------------------------------------------------- /src/game/mod.rs: -------------------------------------------------------------------------------- 1 | use num::Integer; 2 | use rand::distributions::uniform::SampleUniform; 3 | use rand::Rng; 4 | 5 | pub mod constants; 6 | 7 | pub fn get_random_bool() -> bool { 8 | let mut rnd = rand::thread_rng(); 9 | rnd.gen() 10 | } 11 | 12 | pub fn get_rand_from_range(min: T, max: T) -> T 13 | where T: Integer + SampleUniform { 14 | let mut rnd = rand::thread_rng(); 15 | rnd.gen_range(min, max) 16 | } 17 | 18 | #[allow(dead_code)] 19 | pub fn get_weighted_random(weight: f32) -> bool { 20 | let mut rnd = rand::thread_rng(); 21 | rnd.gen::() < weight 22 | } 23 | -------------------------------------------------------------------------------- /src/gfx_app/controls.rs: -------------------------------------------------------------------------------- 1 | use crossbeam_channel as channel; 2 | 3 | use crate::audio::Effects; 4 | use crate::character::controls::CharacterControl; 5 | use crate::gfx_app::mouse_controls::MouseControl; 6 | use crate::graphics::camera::CameraControl; 7 | 8 | pub enum Control { 9 | Plus, 10 | Negative, 11 | Released, 12 | } 13 | 14 | pub struct TilemapControls { 15 | audio_control: channel::Sender, 16 | terrain_control: channel::Sender, 17 | character_control: channel::Sender, 18 | mouse_control: channel::Sender<(MouseControl, Option<(f64, f64)>)>, 19 | } 20 | 21 | impl TilemapControls { 22 | pub fn new(atc: channel::Sender, 23 | ttc: channel::Sender, 24 | ctc: channel::Sender, 25 | mtc: channel::Sender<(MouseControl, Option<(f64, f64)>)>) -> TilemapControls { 26 | TilemapControls { 27 | audio_control: atc, 28 | terrain_control: ttc, 29 | character_control: ctc, 30 | mouse_control: mtc, 31 | } 32 | } 33 | 34 | pub fn zoom(&mut self, control: &Control) { 35 | match control { 36 | Control::Plus => self.terrain_control.send(CameraControl::ZoomIn), 37 | Control::Negative => self.terrain_control.send(CameraControl::ZoomOut), 38 | Control::Released => self.terrain_control.send(CameraControl::ZoomStop), 39 | }.expect("Terrain control update error"); 40 | } 41 | 42 | pub fn ctrl_pressed(&mut self, is_ctrl: bool) { 43 | if is_ctrl { 44 | self.character_control.send(CharacterControl::CtrlPressed) 45 | } else { 46 | self.character_control.send(CharacterControl::CtrlReleased) 47 | }.expect("Character Ctrl control update error"); 48 | } 49 | 50 | pub fn move_character(&mut self, character_control: CharacterControl) { 51 | self.character_control.send(character_control).expect("Character move control update error"); 52 | } 53 | 54 | pub fn reload_weapon(&mut self, is_reloading: bool) { 55 | if is_reloading { 56 | self.character_control.send(CharacterControl::ReloadPressed) 57 | } else { 58 | self.character_control.send(CharacterControl::ReloadReleased) 59 | }.expect("Character reload weapon control update error"); 60 | } 61 | 62 | pub fn mouse_left_click(&mut self, mouse_pos: Option<(f64, f64)>) { 63 | self.mouse_control.send((MouseControl::LeftClick, mouse_pos)).expect("Mouse control shoot update error"); 64 | match mouse_pos { 65 | Some(_) => self.audio_control.send(Effects::PistolFire), 66 | _ => self.audio_control.send(Effects::None), 67 | }.expect("Audio control update error"); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/gfx_app/init.rs: -------------------------------------------------------------------------------- 1 | use std::time; 2 | 3 | use specs::{Builder, prelude::DispatcherBuilder, shred::World, world::WorldExt}; 4 | 5 | use crate::{bullet, terrain_shape}; 6 | use crate::audio::AudioSystem; 7 | use crate::bullet::bullets::Bullets; 8 | use crate::bullet::collision::CollisionSystem; 9 | use crate::character; 10 | use crate::character::controls::CharacterControlSystem; 11 | use crate::critter::CharacterSprite; 12 | use crate::gfx_app::{Window, WindowStatus}; 13 | use crate::gfx_app::controls::TilemapControls; 14 | use crate::gfx_app::mouse_controls::{MouseControlSystem, MouseInputState}; 15 | use crate::gfx_app::renderer::DeviceRenderer; 16 | use crate::gfx_app::system::DrawSystem; 17 | use crate::graphics; 18 | use crate::graphics::{DeltaTime, dimensions::Dimensions, GameTime}; 19 | use crate::graphics::camera::CameraControlSystem; 20 | use crate::hud; 21 | use crate::terrain; 22 | use crate::terrain_object; 23 | use crate::zombie; 24 | use crate::zombie::zombies::Zombies; 25 | use crate::game::constants::SMALL_HILLS; 26 | 27 | pub fn run(window: &mut W) 28 | where W: Window, 29 | D: gfx::Device + 'static, 30 | F: gfx::Factory, 31 | D::CommandBuffer: Send { 32 | 33 | let mut w = WorldExt::new(); 34 | let viewport_size = window.get_viewport_size(); 35 | let dimensions = Dimensions::new(viewport_size.0, 36 | viewport_size.1, 37 | window.get_hidpi_factor(), 38 | window.is_windowed()); 39 | setup_world(&mut w, dimensions); 40 | dispatch_loop(window, &mut w); 41 | } 42 | 43 | fn setup_world(world: &mut World, dimensions: Dimensions) { 44 | world.register::(); 45 | world.register::(); 46 | world.register::(); 47 | world.register::(); 48 | world.register::(); 49 | world.register::(); 50 | world.register::(); 51 | world.register::(); 52 | world.register::(); 53 | world.register::(); 54 | world.register::(); 55 | 56 | world.insert(dimensions); 57 | world.insert(character::controls::CharacterInputState::new()); 58 | world.insert(MouseInputState::new()); 59 | world.insert(DeltaTime(0.0)); 60 | world.insert(GameTime(0)); 61 | 62 | let mut hills = terrain_shape::terrain_shape_objects::TerrainShapeObjects::new(); 63 | 64 | for hill in SMALL_HILLS.iter() { 65 | hills.small_hill(hill[0], hill[1]); 66 | } 67 | 68 | world.create_entity() 69 | .with(terrain::TerrainDrawable::new()) 70 | .with(character::CharacterDrawable::new()) 71 | .with(hud::hud_objects::HudObjects::new()) 72 | .with(terrain_object::terrain_objects::TerrainObjects::new()) 73 | .with(hills) 74 | .with(Zombies::new()) 75 | .with(Bullets::new()) 76 | .with(CharacterSprite::new()) 77 | .with(graphics::camera::CameraInputState::new()) 78 | .with(character::controls::CharacterInputState::new()) 79 | .with(MouseInputState::new()).build(); 80 | } 81 | 82 | fn dispatch_loop(window: &mut W, 83 | w: &mut World) 84 | where W: Window, 85 | D: gfx::Device + 'static, 86 | F: gfx::Factory, 87 | D::CommandBuffer: Send { 88 | let (mut device_renderer, encoder_queue) = DeviceRenderer::new(window.create_buffers(2)); 89 | let draw = { 90 | let rtv = window.get_render_target_view(); 91 | let dsv = window.get_depth_stencil_view(); 92 | DrawSystem::new(window.get_factory(), &rtv, &dsv, encoder_queue) 93 | }; 94 | 95 | let (audio_system, audio_control) = AudioSystem::new(); 96 | let (terrain_system, terrain_control) = CameraControlSystem::new(); 97 | let (character_system, character_control) = CharacterControlSystem::new(); 98 | let (mouse_system, mouse_control) = MouseControlSystem::new(); 99 | let controls = TilemapControls::new(audio_control, terrain_control, character_control, mouse_control); 100 | 101 | let mut dispatcher = DispatcherBuilder::new() 102 | .with(draw, "drawing", &[]) 103 | .with(terrain::PreDrawSystem, "draw-prep-terrain", &["drawing"]) 104 | .with(character::PreDrawSystem, "draw-prep-character", &["drawing"]) 105 | .with(zombie::PreDrawSystem, "draw-prep-zombie", &["drawing"]) 106 | .with(bullet::PreDrawSystem, "draw-prep-bullet", &["drawing"]) 107 | .with(hud::PreDrawSystem, "draw-prep-hud", &[]) 108 | .with(terrain_system, "terrain-system", &[]) 109 | .with(terrain_object::PreDrawSystem, "draw-prep-terrain_object", &["terrain-system"]) 110 | .with(terrain_shape::PreDrawSystem, "draw-prep-terrain_shape_object", &["terrain-system"]) 111 | .with(character_system, "character-system", &[]) 112 | .with(mouse_system, "mouse-system", &[]) 113 | .with(audio_system, "audio-system", &[]) 114 | .with(CollisionSystem, "collision-system", &["mouse-system"]) 115 | .build(); 116 | 117 | window.set_controls(controls); 118 | 119 | let start_time = time::Instant::now(); 120 | let mut last_time = time::Instant::now(); 121 | loop { 122 | let elapsed = last_time.elapsed(); 123 | let delta = f64::from(elapsed.subsec_nanos()) / 1e9 + elapsed.as_secs() as f64; 124 | // Throttle update speed 125 | if delta >= 0.0083 { 126 | last_time = time::Instant::now(); 127 | dispatcher.dispatch(&w); 128 | w.maintain(); 129 | 130 | *w.write_resource::() = DeltaTime(delta); 131 | *w.write_resource::() = GameTime(start_time.elapsed().as_secs()); 132 | 133 | device_renderer.draw(window.get_device()); 134 | 135 | window.swap_window(); 136 | } 137 | 138 | if let WindowStatus::Close = window.poll_events() { 139 | break; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/gfx_app/mod.rs: -------------------------------------------------------------------------------- 1 | use gfx::format::SurfaceType; 2 | use gfx::handle::{DepthStencilView, RenderTargetView}; 3 | use gfx::memory::Typed; 4 | use glutin::{KeyboardInput, MouseButton, PossiblyCurrent, WindowedContext}; 5 | use glutin::dpi::LogicalSize; 6 | use glutin::ElementState::{Pressed, Released}; 7 | use glutin::VirtualKeyCode::{A, D, Escape, R, S, W, X, Z}; 8 | use std::fmt::{Display, Formatter, Result}; 9 | 10 | use crate::character::controls::CharacterControl; 11 | use crate::game::constants::{GAME_TITLE, RESOLUTION_X, RESOLUTION_Y}; 12 | use crate::gfx_app::controls::{Control, TilemapControls}; 13 | 14 | pub mod init; 15 | pub mod renderer; 16 | pub mod system; 17 | pub mod controls; 18 | pub mod mouse_controls; 19 | 20 | pub type ColorFormat = gfx::format::Rgba8; 21 | pub type DepthFormat = gfx::format::DepthStencil; 22 | 23 | pub const COLOR_FORMAT_VALUE: SurfaceType = SurfaceType::R8_G8_B8_A8; 24 | pub const DEPTH_FORMAT_VALUE: SurfaceType = SurfaceType::D24_S8; 25 | 26 | #[derive(Debug)] 27 | pub struct GameOptions { 28 | windowed_mode: bool, 29 | } 30 | 31 | impl Display for GameOptions { 32 | fn fmt(&self, f: &mut Formatter) -> Result { 33 | write!(f, "{}", format!("windowed_mode={}", self.windowed_mode)) 34 | } 35 | } 36 | 37 | impl GameOptions { 38 | pub fn new(windowed_mode: bool) -> GameOptions { 39 | GameOptions { 40 | windowed_mode, 41 | } 42 | } 43 | } 44 | 45 | pub struct WindowContext { 46 | window_context: WindowedContext, 47 | controls: Option, 48 | events_loop: glutin::EventsLoop, 49 | device: gfx_device_gl::Device, 50 | factory: gfx_device_gl::Factory, 51 | render_target_view: RenderTargetView, 52 | depth_stencil_view: DepthStencilView, 53 | mouse_pos: (f64, f64), 54 | game_options: GameOptions 55 | } 56 | 57 | impl WindowContext { 58 | pub fn new(game_options: GameOptions) -> WindowContext { 59 | let events_loop = glutin::EventsLoop::new(); 60 | 61 | let window_title = glutin::WindowBuilder::new() 62 | .with_title(GAME_TITLE); 63 | 64 | println!("{}", game_options); 65 | 66 | let builder = if game_options.windowed_mode { 67 | let logical_size = LogicalSize::new(RESOLUTION_X.into(), RESOLUTION_Y.into()); 68 | window_title 69 | .with_dimensions(logical_size) 70 | .with_decorations(false) 71 | } else { 72 | let monitor = { 73 | events_loop.get_available_monitors().next().expect("No monitor found") 74 | }; 75 | let monitor_resolution = monitor.get_dimensions(); 76 | 77 | let resolution = ((monitor_resolution.width as f32 * 16.0 / 9.0) as u32, monitor_resolution.height); 78 | 79 | let logical_size = LogicalSize::new(resolution.0.into(), resolution.1); 80 | window_title.with_fullscreen(Some(monitor)) 81 | .with_decorations(false) 82 | .with_dimensions(logical_size) 83 | }; 84 | 85 | let window_context = glutin::ContextBuilder::new() 86 | .with_vsync(true) 87 | .with_double_buffer(Some(true)) 88 | .with_pixel_format(24, 8) 89 | .with_srgb(true) 90 | .build_windowed(builder, &events_loop) 91 | .expect("Window context creation failed"); 92 | 93 | let window_context = unsafe { 94 | window_context 95 | .make_current() 96 | .expect("Window focus failed") 97 | }; 98 | 99 | let (width, height) = { 100 | let inner_size = window_context.window().get_inner_size().expect("get_inner_size failed"); 101 | let size = inner_size.to_physical(window_context.window().get_hidpi_factor()); 102 | (size.width as _, size.height as _) 103 | }; 104 | 105 | let aa = window_context 106 | .get_pixel_format().multisampling 107 | .unwrap_or(0) as u8; 108 | 109 | let window_dimensions = (width, height, 1, aa.into()); 110 | 111 | let (device, factory) = gfx_device_gl::create(|s| 112 | window_context.get_proc_address(s) as *const std::os::raw::c_void); 113 | 114 | let (rtv, dsv) = 115 | gfx_device_gl::create_main_targets_raw(window_dimensions, 116 | COLOR_FORMAT_VALUE, 117 | DEPTH_FORMAT_VALUE); 118 | 119 | WindowContext { 120 | window_context, 121 | controls: None, 122 | events_loop, 123 | device, 124 | factory, 125 | render_target_view: RenderTargetView::new(rtv), 126 | depth_stencil_view: DepthStencilView::new(dsv), 127 | mouse_pos: (0.0, 0.0), 128 | game_options, 129 | } 130 | } 131 | } 132 | 133 | #[derive(PartialEq, Eq)] 134 | pub enum WindowStatus { 135 | Open, 136 | Close, 137 | } 138 | 139 | pub trait Window> { 140 | fn swap_window(&mut self); 141 | fn create_buffers(&mut self, count: usize) -> Vec; 142 | fn set_controls(&mut self, controls: controls::TilemapControls); 143 | fn get_viewport_size(&mut self) -> (f32, f32); 144 | fn get_device(&mut self) -> &mut D; 145 | fn get_factory(&mut self) -> &mut F; 146 | fn get_hidpi_factor(&mut self) -> f32; 147 | fn get_render_target_view(&mut self) -> RenderTargetView; 148 | fn get_depth_stencil_view(&mut self) -> DepthStencilView; 149 | fn poll_events(&mut self) -> WindowStatus; 150 | fn is_windowed(&self) -> bool; 151 | } 152 | 153 | impl Window for WindowContext { 154 | fn swap_window(&mut self) { 155 | use gfx::Device; 156 | self.window_context 157 | .swap_buffers() 158 | .expect("Unable to swap buffers"); 159 | self.device.cleanup(); 160 | } 161 | 162 | fn create_buffers(&mut self, count: usize) -> Vec { 163 | let mut bufs = Vec::new(); 164 | for _ in 0..count { 165 | bufs.push(self.factory.create_command_buffer()); 166 | } 167 | bufs 168 | } 169 | 170 | fn set_controls(&mut self, controls: controls::TilemapControls) { 171 | self.controls = Some(controls); 172 | } 173 | 174 | fn get_viewport_size(&mut self) -> (f32, f32) { 175 | if self.game_options.windowed_mode { 176 | (RESOLUTION_X as f32, RESOLUTION_Y as f32) 177 | } else { 178 | let monitor = self.events_loop.get_available_monitors().next().expect("No monitor found"); 179 | let monitor_resolution = monitor.get_dimensions(); 180 | (monitor_resolution.width as f32, monitor_resolution.height as f32) 181 | } 182 | } 183 | 184 | fn get_device(&mut self) -> &mut gfx_device_gl::Device { 185 | &mut self.device 186 | } 187 | 188 | fn get_factory(&mut self) -> &mut gfx_device_gl::Factory { 189 | &mut self.factory 190 | } 191 | 192 | fn get_hidpi_factor(&mut self) -> f32 { 193 | if self.game_options.windowed_mode { 194 | 1.0 195 | } else { 196 | self.window_context.window().get_hidpi_factor() as f32 197 | } 198 | } 199 | 200 | fn get_render_target_view(&mut self) -> RenderTargetView { 201 | self.render_target_view.clone() 202 | } 203 | 204 | fn get_depth_stencil_view(&mut self) -> DepthStencilView { 205 | self.depth_stencil_view.clone() 206 | } 207 | 208 | fn poll_events(&mut self) -> WindowStatus { 209 | use glutin::WindowEvent::{CursorMoved, CloseRequested, MouseInput}; 210 | 211 | let controls = match self.controls { 212 | Some(ref mut c) => c, 213 | None => panic!("Terrain controls have not been initialized"), 214 | }; 215 | 216 | let m_pos = &mut self.mouse_pos; 217 | let mut game_status = WindowStatus::Open; 218 | 219 | self.events_loop.poll_events(|event| { 220 | game_status = if let glutin::Event::WindowEvent { event, .. } = event { 221 | match event { 222 | glutin::WindowEvent::KeyboardInput { input, .. } => { process_keyboard_input(input, controls) } 223 | MouseInput { state: Pressed, button: MouseButton::Left, .. } => { 224 | controls.mouse_left_click(Some(*m_pos)); 225 | WindowStatus::Open 226 | } 227 | MouseInput { state: Released, button: MouseButton::Left, .. } => { 228 | controls.mouse_left_click(None); 229 | WindowStatus::Open 230 | } 231 | CursorMoved { position, .. } => { 232 | *m_pos = ((position.x as f32).into(), (position.y as f32).into()); 233 | WindowStatus::Open 234 | } 235 | CloseRequested => WindowStatus::Close, 236 | _ => WindowStatus::Open, 237 | } 238 | } else { 239 | WindowStatus::Open 240 | }; 241 | }); 242 | game_status 243 | } 244 | 245 | fn is_windowed(&self) -> bool { 246 | self.game_options.windowed_mode 247 | } 248 | } 249 | 250 | fn process_keyboard_input(input: glutin::KeyboardInput, controls: &mut TilemapControls) -> WindowStatus { 251 | match input { 252 | KeyboardInput { state: Pressed, virtual_keycode: Some(Z), .. } => { 253 | controls.zoom(&Control::Negative); 254 | } 255 | KeyboardInput { state: Pressed, virtual_keycode: Some(X), .. } => { 256 | controls.zoom(&Control::Plus); 257 | } 258 | KeyboardInput { state: Released, virtual_keycode: Some(Z), .. } | 259 | KeyboardInput { state: Released, virtual_keycode: Some(X), .. } => { 260 | controls.zoom(&Control::Released); 261 | } 262 | KeyboardInput { state: Pressed, virtual_keycode: Some(W), .. } => { 263 | controls.move_character(CharacterControl::Up); 264 | } 265 | KeyboardInput { state: Pressed, virtual_keycode: Some(S), .. } => { 266 | controls.move_character(CharacterControl::Down); 267 | } 268 | KeyboardInput { state: Released, virtual_keycode: Some(W), .. } | 269 | KeyboardInput { state: Released, virtual_keycode: Some(S), .. } => { 270 | controls.move_character(CharacterControl::YMoveStop); 271 | } 272 | KeyboardInput { state: Pressed, virtual_keycode: Some(A), .. } => { 273 | controls.move_character(CharacterControl::Left); 274 | } 275 | KeyboardInput { state: Pressed, virtual_keycode: Some(D), .. } => { 276 | controls.move_character(CharacterControl::Right); 277 | } 278 | KeyboardInput { state: Released, virtual_keycode: Some(A), .. } | 279 | KeyboardInput { state: Released, virtual_keycode: Some(D), .. } => { 280 | controls.move_character(CharacterControl::XMoveStop); 281 | } 282 | KeyboardInput { state: Pressed, virtual_keycode: Some(R), .. } => { 283 | controls.reload_weapon(true); 284 | } 285 | KeyboardInput { state: Released, virtual_keycode: Some(R), .. } => { 286 | controls.reload_weapon(false); 287 | } 288 | KeyboardInput { state: Pressed, modifiers, .. } => { 289 | if modifiers.ctrl { 290 | controls.ctrl_pressed(true); 291 | } 292 | } 293 | KeyboardInput { state: Released, modifiers, .. } => { 294 | if !modifiers.ctrl { 295 | controls.ctrl_pressed(false); 296 | } 297 | } 298 | } 299 | if let Some(Escape) = input.virtual_keycode { 300 | WindowStatus::Close 301 | } else { 302 | WindowStatus::Open 303 | } 304 | } 305 | 306 | -------------------------------------------------------------------------------- /src/gfx_app/mouse_controls.rs: -------------------------------------------------------------------------------- 1 | use cgmath::Point2; 2 | use crossbeam_channel as channel; 3 | use specs::prelude::{Read, ReadStorage, WriteStorage}; 4 | 5 | use crate::bullet::bullets::Bullets; 6 | use crate::character::{CharacterDrawable, controls::CharacterInputState}; 7 | use crate::game::constants::SMALL_HILLS; 8 | use crate::graphics::{camera::CameraInputState, check_terrain_elevation, dimensions::Dimensions, direction}; 9 | use crate::shaders::Position; 10 | 11 | type MouseEvent = channel::Sender<(MouseControl, Option<(f64, f64)>)>; 12 | 13 | #[derive(Clone)] 14 | pub struct MouseInputState { 15 | pub mouse_left: Option>, 16 | pub mouse_right: Option>, 17 | pub left_click_point: Option>, 18 | } 19 | 20 | impl MouseInputState { 21 | pub fn new() -> MouseInputState { 22 | MouseInputState { 23 | mouse_left: None, 24 | mouse_right: None, 25 | left_click_point: None, 26 | } 27 | } 28 | } 29 | 30 | impl Default for MouseInputState { 31 | fn default() -> MouseInputState { 32 | MouseInputState::new() 33 | } 34 | } 35 | 36 | impl specs::prelude::Component for MouseInputState { 37 | type Storage = specs::storage::VecStorage; 38 | } 39 | 40 | pub enum MouseControl { 41 | LeftClick, 42 | } 43 | 44 | pub struct MouseControlSystem { 45 | queue: channel::Receiver<(MouseControl, Option<(f64, f64)>)>, 46 | } 47 | 48 | impl MouseControlSystem { 49 | pub fn new() -> (MouseControlSystem, MouseEvent) { 50 | let (tx, rx) = channel::unbounded(); 51 | (MouseControlSystem { 52 | queue: rx, 53 | }, tx) 54 | } 55 | } 56 | 57 | impl<'a> specs::prelude::System<'a> for MouseControlSystem { 58 | type SystemData = (WriteStorage<'a, MouseInputState>, 59 | WriteStorage<'a, CharacterDrawable>, 60 | ReadStorage<'a, CameraInputState>, 61 | ReadStorage<'a, CharacterInputState>, 62 | WriteStorage<'a, Bullets>, 63 | Read<'a, Dimensions>); 64 | 65 | fn run(&mut self, (mut mouse_input, mut character_drawable, camera, character_input, mut bullets, dim): Self::SystemData) { 66 | use specs::join::Join; 67 | 68 | while let Ok((control_value, value)) = self.queue.try_recv() { 69 | match control_value { 70 | MouseControl::LeftClick => { 71 | for (mut mi, cd, bs, ca, ci) in (&mut mouse_input, &mut character_drawable, &mut bullets, &camera, &character_input).join() { 72 | if let Some(val) = value { 73 | if ci.is_shooting && cd.stats.ammunition > 0 { 74 | cd.stats.ammunition -= 1; 75 | let start_point = Point2::new(dim.window_width / 2.0 * dim.hidpi_factor, dim.window_height / 2.0 * dim.hidpi_factor); 76 | let end_point = Point2::new(val.0 as f32 * dim.hidpi_factor, val.1 as f32 * dim.hidpi_factor); 77 | mi.left_click_point = Some(end_point); 78 | let dir = direction(start_point, end_point); 79 | let elevated_pos_y = check_terrain_elevation(ci.movement, &SMALL_HILLS); 80 | Bullets::add_bullet(bs, Position::new(-ca.movement.x(), ca.movement.y() + elevated_pos_y), dir); 81 | } 82 | } else { 83 | mi.left_click_point = None; 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/gfx_app/renderer.rs: -------------------------------------------------------------------------------- 1 | use crossbeam_channel as channel; 2 | 3 | pub struct EncoderQueue { 4 | pub sender: channel::Sender>, 5 | pub receiver: channel::Receiver>, 6 | } 7 | 8 | pub struct DeviceRenderer { 9 | queue: EncoderQueue, 10 | } 11 | 12 | impl DeviceRenderer { 13 | pub fn new(buffers: Vec) -> (DeviceRenderer, EncoderQueue) { 14 | let (a_send, b_recv) = channel::unbounded(); 15 | let (b_send, a_recv) = channel::unbounded(); 16 | 17 | for cb in buffers { 18 | let encoder = gfx::Encoder::from(cb); 19 | a_send.send(encoder).expect("Device renderer error"); 20 | } 21 | 22 | (DeviceRenderer { 23 | queue: EncoderQueue { 24 | sender: a_send, 25 | receiver: a_recv, 26 | }, 27 | }, 28 | EncoderQueue { 29 | sender: b_send, 30 | receiver: b_recv, 31 | }) 32 | } 33 | 34 | pub fn draw(&mut self, device: &mut D) { 35 | self.queue.receiver.recv() 36 | .map(|mut encoder| { 37 | encoder.flush(device); 38 | self.queue.sender.send(encoder).expect("Error while sending new encoder to channel"); 39 | }) 40 | .expect("Device renderer queue read error"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/gfx_app/system.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use specs::prelude::{Read, WriteStorage}; 4 | 5 | use crate::{bullet, terrain_shape}; 6 | use crate::character; 7 | use crate::critter::CharacterSprite; 8 | use crate::game::constants::{CURRENT_AMMO_TEXT, GAME_VERSION, HUD_TEXTS}; 9 | use crate::gfx_app::{ColorFormat, DepthFormat}; 10 | use crate::gfx_app::renderer::EncoderQueue; 11 | use crate::graphics::{DeltaTime, orientation::{Orientation, Stance}}; 12 | use crate::graphics::Drawables; 13 | use crate::hud; 14 | use crate::terrain; 15 | use crate::terrain_object; 16 | use crate::terrain_object::TerrainTexture; 17 | use crate::zombie; 18 | 19 | pub struct DrawSystem { 20 | render_target_view: gfx::handle::RenderTargetView, 21 | depth_stencil_view: gfx::handle::DepthStencilView, 22 | terrain_system: terrain::TerrainDrawSystem, 23 | character_system: character::CharacterDrawSystem, 24 | zombie_system: zombie::ZombieDrawSystem, 25 | bullet_system: bullet::BulletDrawSystem, 26 | terrain_object_system: [terrain_object::TerrainObjectDrawSystem; 3], 27 | terrain_shape_system: [terrain_shape::TerrainShapeDrawSystem; 9], 28 | text_system: [hud::TextDrawSystem; 3], 29 | encoder_queue: EncoderQueue, 30 | game_time: Instant, 31 | frames: u32, 32 | cool_down: f64, 33 | run_cool_down: f64, 34 | fire_cool_down: f64, 35 | } 36 | 37 | impl DrawSystem { 38 | pub fn new(factory: &mut F, 39 | rtv: &gfx::handle::RenderTargetView, 40 | dsv: &gfx::handle::DepthStencilView, 41 | encoder_queue: EncoderQueue) 42 | -> DrawSystem 43 | where F: gfx::Factory { 44 | DrawSystem { 45 | render_target_view: rtv.clone(), 46 | depth_stencil_view: dsv.clone(), 47 | terrain_system: terrain::TerrainDrawSystem::new(factory, rtv.clone(), dsv.clone()), 48 | character_system: character::CharacterDrawSystem::new(factory, rtv.clone(), dsv.clone()), 49 | zombie_system: zombie::ZombieDrawSystem::new(factory, rtv.clone(), dsv.clone()), 50 | bullet_system: bullet::BulletDrawSystem::new(factory, rtv.clone(), dsv.clone()), 51 | terrain_object_system: [ 52 | terrain_object::TerrainObjectDrawSystem::new(factory, rtv.clone(), dsv.clone(), TerrainTexture::Ammo), 53 | terrain_object::TerrainObjectDrawSystem::new(factory, rtv.clone(), dsv.clone(), TerrainTexture::House), 54 | terrain_object::TerrainObjectDrawSystem::new(factory, rtv.clone(), dsv.clone(), TerrainTexture::Tree) 55 | ], 56 | terrain_shape_system: [ 57 | terrain_shape::TerrainShapeDrawSystem::new(factory, rtv.clone(), dsv.clone(), Orientation::Right), 58 | terrain_shape::TerrainShapeDrawSystem::new(factory, rtv.clone(), dsv.clone(), Orientation::DownRight), 59 | terrain_shape::TerrainShapeDrawSystem::new(factory, rtv.clone(), dsv.clone(), Orientation::Down), 60 | terrain_shape::TerrainShapeDrawSystem::new(factory, rtv.clone(), dsv.clone(), Orientation::DownLeft), 61 | terrain_shape::TerrainShapeDrawSystem::new(factory, rtv.clone(), dsv.clone(), Orientation::Left), 62 | terrain_shape::TerrainShapeDrawSystem::new(factory, rtv.clone(), dsv.clone(), Orientation::UpLeft), 63 | terrain_shape::TerrainShapeDrawSystem::new(factory, rtv.clone(), dsv.clone(), Orientation::UpRight), 64 | terrain_shape::TerrainShapeDrawSystem::new(factory, rtv.clone(), dsv.clone(), Orientation::Normal), 65 | terrain_shape::TerrainShapeDrawSystem::new(factory, rtv.clone(), dsv.clone(), Orientation::Up), 66 | ], 67 | text_system: [ 68 | hud::TextDrawSystem::new(factory, &HUD_TEXTS, GAME_VERSION, rtv.clone(), dsv.clone()), 69 | hud::TextDrawSystem::new(factory, &HUD_TEXTS, CURRENT_AMMO_TEXT, rtv.clone(), dsv.clone()), 70 | hud::TextDrawSystem::new(factory, &HUD_TEXTS, CURRENT_AMMO_TEXT, rtv.clone(), dsv.clone()) 71 | ], 72 | encoder_queue, 73 | game_time: Instant::now(), 74 | frames: 0, 75 | cool_down: 1.0, 76 | run_cool_down: 1.0, 77 | fire_cool_down: 1.0, 78 | } 79 | } 80 | 81 | fn update_cooldowns(&mut self, delta: f64) { 82 | if self.cool_down == 0.0 { 83 | self.cool_down += 0.05; 84 | } 85 | if self.fire_cool_down == 0.0 { 86 | self.fire_cool_down += 0.2; 87 | } 88 | if self.run_cool_down == 0.0 { 89 | self.run_cool_down += 0.02; 90 | } 91 | self.cool_down = (self.cool_down - delta).max(0.0); 92 | self.run_cool_down = (self.run_cool_down - delta).max(0.0); 93 | self.fire_cool_down = (self.fire_cool_down - delta).max(0.0); 94 | } 95 | } 96 | 97 | impl<'a, D> specs::prelude::System<'a> for DrawSystem 98 | where D: gfx::Device, 99 | D::CommandBuffer: Send { 100 | type SystemData = (WriteStorage<'a, terrain::TerrainDrawable>, 101 | WriteStorage<'a, terrain_shape::terrain_shape_objects::TerrainShapeObjects>, 102 | WriteStorage<'a, character::CharacterDrawable>, 103 | WriteStorage<'a, CharacterSprite>, 104 | WriteStorage<'a, hud::hud_objects::HudObjects>, 105 | WriteStorage<'a, zombie::zombies::Zombies>, 106 | WriteStorage<'a, bullet::bullets::Bullets>, 107 | WriteStorage<'a, terrain_object::terrain_objects::TerrainObjects>, 108 | Read<'a, DeltaTime>); 109 | 110 | fn run(&mut self, (mut terrain, mut terrain_shape, mut character, mut character_sprite, mut hud_objects, mut zombies, mut bullets, mut terrain_objects, dt): Self::SystemData) { 111 | use specs::join::Join; 112 | let mut encoder = self.encoder_queue.receiver 113 | .recv() 114 | .expect("Encoder error"); 115 | 116 | self.update_cooldowns(dt.0); 117 | 118 | let current_time = Instant::now(); 119 | self.frames += 1; 120 | 121 | let time_passed = current_time.duration_since(self.game_time).as_secs(); 122 | 123 | if cfg!(feature = "framerate") && time_passed >= 1 { 124 | println!("{:?} ms/frames", 1000.0 / f64::from(self.frames)); 125 | self.frames = 0; 126 | self.game_time = Instant::now(); 127 | } 128 | 129 | encoder.clear(&self.render_target_view, [16.0 / 256.0, 16.0 / 256.0, 20.0 / 256.0, 1.0]); 130 | encoder.clear_depth(&self.depth_stencil_view, 1.0); 131 | 132 | for (t, t_shape, c, cs, hds, zs, bs, obj) in (&mut terrain, &mut terrain_shape, &mut character, &mut character_sprite, &mut hud_objects, 133 | &mut zombies, &mut bullets, &mut terrain_objects).join() { 134 | self.terrain_system.draw(t, time_passed, &mut encoder); 135 | 136 | for hud in &mut hds.objects { 137 | self.text_system[0].draw(hud, &mut encoder); 138 | self.text_system[1].draw(hud, &mut encoder); 139 | } 140 | 141 | if self.cool_down == 0.0 { 142 | if c.stance == Stance::Walking { 143 | cs.update_run(); 144 | } 145 | for z in &mut zs.zombies { 146 | match z.stance { 147 | Stance::NormalDeath => z.update_death_idx(5), 148 | Stance::CriticalDeath => z.update_death_idx(7), 149 | Stance::Walking => z.update_alive_idx(7), 150 | Stance::Still => z.update_alive_idx(3), 151 | _ => () 152 | }; 153 | } 154 | } else if self.fire_cool_down == 0.0 && c.stance == Stance::Firing { 155 | cs.update_fire(); 156 | } 157 | 158 | if self.run_cool_down == 0.0 { 159 | for z in &mut zs.zombies { 160 | if let Stance::Running = z.stance { 161 | z.update_alive_idx(7) 162 | } 163 | } 164 | } 165 | 166 | let mut drawables: Vec = vec![]; 167 | drawables.append(&mut bs.bullets.iter().map(|b| Drawables::Bullet(b)).collect()); 168 | drawables.append(&mut zs.zombies.iter_mut().map(|z| Drawables::Zombie(z)).collect()); 169 | 170 | for o in &obj.objects { 171 | match o.object_type { 172 | TerrainTexture::Ammo => drawables.push(Drawables::TerrainAmmo(o)), 173 | TerrainTexture::House => drawables.push(Drawables::TerrainHouse(o)), 174 | TerrainTexture::Tree => drawables.push(Drawables::TerrainTree(o)), 175 | }; 176 | } 177 | 178 | drawables.push(Drawables::Character(c)); 179 | 180 | drawables.sort_by(|a, b| { 181 | Drawables::get_vertical_pos(b) 182 | .partial_cmp(&Drawables::get_vertical_pos(a)) 183 | .expect("Z-axis sorting failed") 184 | }); 185 | 186 | for ts in &t_shape.objects { 187 | match ts.get_shape() { 188 | Orientation::Right => self.terrain_shape_system[0].draw(ts, time_passed, &mut encoder), 189 | Orientation::DownRight => self.terrain_shape_system[1].draw(ts, time_passed, &mut encoder), 190 | Orientation::Down => self.terrain_shape_system[2].draw(ts, time_passed, &mut encoder), 191 | Orientation::DownLeft => self.terrain_shape_system[3].draw(ts, time_passed, &mut encoder), 192 | Orientation::Left => self.terrain_shape_system[4].draw(ts, time_passed, &mut encoder), 193 | Orientation::UpLeft => self.terrain_shape_system[5].draw(ts, time_passed, &mut encoder), 194 | Orientation::UpRight => self.terrain_shape_system[6].draw(ts, time_passed, &mut encoder), 195 | Orientation::Normal => self.terrain_shape_system[7].draw(ts, time_passed, &mut encoder), 196 | Orientation::Up => self.terrain_shape_system[8].draw(ts, time_passed, &mut encoder), 197 | } 198 | } 199 | 200 | for e in &mut drawables { 201 | match *e { 202 | Drawables::Bullet(ref e) => { self.bullet_system.draw(e, &mut encoder) } 203 | Drawables::Zombie(ref mut e) => { self.zombie_system.draw(e, &mut encoder) } 204 | Drawables::TerrainAmmo(ref mut e) => { self.terrain_object_system[0].draw(e, time_passed, &mut encoder) } 205 | Drawables::TerrainHouse(ref mut e) => { self.terrain_object_system[1].draw(e, time_passed, &mut encoder) } 206 | Drawables::TerrainTree(ref mut e) => { self.terrain_object_system[2].draw(e, time_passed, &mut encoder) } 207 | Drawables::Character(ref mut e) => { self.character_system.draw(e, cs, &mut encoder) } 208 | } 209 | } 210 | } 211 | 212 | self.encoder_queue.sender.send(encoder).expect("Encoder queue update error"); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/graphics/camera.rs: -------------------------------------------------------------------------------- 1 | use crossbeam_channel as channel; 2 | use specs::prelude::WriteStorage; 3 | 4 | use crate::game::constants::VIEW_DISTANCE; 5 | use crate::shaders::Position; 6 | 7 | #[derive(Clone)] 8 | pub struct CameraInputState { 9 | pub distance: f32, 10 | pub movement: Position, 11 | } 12 | 13 | impl CameraInputState { 14 | pub fn new() -> CameraInputState { 15 | CameraInputState { 16 | distance: VIEW_DISTANCE, 17 | movement: Position::origin(), 18 | } 19 | } 20 | } 21 | 22 | impl Default for CameraInputState { 23 | fn default() -> CameraInputState { 24 | CameraInputState::new() 25 | } 26 | } 27 | 28 | impl specs::prelude::Component for CameraInputState { 29 | type Storage = specs::storage::HashMapStorage; 30 | } 31 | 32 | pub enum CameraControl { 33 | ZoomOut, 34 | ZoomIn, 35 | ZoomStop, 36 | Left, 37 | Right, 38 | Up, 39 | Down, 40 | XMoveStop, 41 | YMoveStop, 42 | } 43 | 44 | pub struct CameraControlSystem { 45 | queue: channel::Receiver, 46 | zoom_level: Option, 47 | } 48 | 49 | impl CameraControlSystem { 50 | pub fn new() -> (CameraControlSystem, channel::Sender) { 51 | let (tx, rx) = channel::unbounded(); 52 | (CameraControlSystem { 53 | queue: rx, 54 | zoom_level: None, 55 | }, tx) 56 | } 57 | } 58 | 59 | impl<'a> specs::prelude::System<'a> for CameraControlSystem { 60 | type SystemData = WriteStorage<'a, CameraInputState>; 61 | fn run(&mut self, mut map_input: Self::SystemData) { 62 | use specs::join::Join; 63 | 64 | while let Ok(control) = self.queue.try_recv() { 65 | match control { 66 | CameraControl::ZoomIn => self.zoom_level = Some(2.0), 67 | CameraControl::ZoomOut => self.zoom_level = Some(-2.0), 68 | CameraControl::ZoomStop => self.zoom_level = None, 69 | _ => (), 70 | } 71 | } 72 | if let Some(zoom) = self.zoom_level { 73 | for m in (&mut map_input).join() { 74 | if m.distance > 200.0 && zoom < 0.0 || m.distance < 600.0 && zoom > 0.0 { 75 | m.distance += zoom; 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/graphics/dimensions.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Matrix4, Point3, Vector3}; 2 | 3 | use crate::graphics::camera::CameraInputState; 4 | use crate::shaders::Projection; 5 | 6 | #[derive(Clone, Default)] 7 | pub struct Dimensions { 8 | pub window_width: f32, 9 | pub window_height: f32, 10 | pub hidpi_factor: f32, 11 | } 12 | 13 | impl Dimensions { 14 | pub fn new(window_width: f32, window_height: f32, hidpi_val: f32, is_windowed: bool) -> Dimensions { 15 | let hidpi_factor = if is_windowed { 1.0 } else { hidpi_val }; 16 | Dimensions { 17 | window_width, 18 | window_height, 19 | hidpi_factor, 20 | } 21 | } 22 | 23 | pub fn world_to_projection(&self, input: &CameraInputState) -> Projection { 24 | let view: Matrix4 = get_view_matrix(input.distance); 25 | let aspect_ratio = self.window_width / self.window_height; 26 | get_projection(view, aspect_ratio) 27 | } 28 | } 29 | 30 | pub fn get_projection(view: Matrix4, aspect_ratio: f32) -> Projection { 31 | Projection { 32 | model: view.into(), 33 | view: view.into(), 34 | proj: cgmath::perspective(cgmath::Deg(75.0f32), aspect_ratio, 0.1, 4000.0).into(), 35 | } 36 | } 37 | 38 | pub fn get_view_matrix(view: f32) -> Matrix4 { 39 | Matrix4::look_at( 40 | Point3::new(0.0, 0.0, view), 41 | Point3::new(0.0, 0.0, 0.0), 42 | Vector3::unit_y(), 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/graphics/graphics_test.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn direction_test() { 3 | use cgmath::Point2; 4 | use crate::graphics; 5 | 6 | assert_eq!(0.0, graphics::direction(Point2 { 7 | x: 1.0, 8 | y: 0.0, 9 | }, Point2 { 10 | x: 2.0, 11 | y: 0.0, 12 | }), "(1,0) to (2,0) should be 0deg"); 13 | 14 | 15 | assert_eq!(90.0, graphics::direction(Point2 { 16 | x: 0.0, 17 | y: 1.0, 18 | }, Point2 { 19 | x: 0.0, 20 | y: 2.0, 21 | }), "(0,1) to (0,2) should be 90deg"); 22 | 23 | assert_eq!(26.565052, graphics::direction(Point2 { 24 | x: -2.0, 25 | y: 1.0, 26 | }, Point2 { 27 | x: 2.0, 28 | y: 3.0, 29 | }), "(-2,1) to (2,3) should be 26deg"); 30 | 31 | assert_eq!(45.0, graphics::direction(Point2 { 32 | x: -2.0, 33 | y: -2.0, 34 | }, Point2 { 35 | x: -1.0, 36 | y: -1.0, 37 | }), "(-2,-2) to (-1,-1) should be 45deg"); 38 | 39 | assert_eq!(225.0, graphics::direction(Point2 { 40 | x: -1.0, 41 | y: -2.0, 42 | }, Point2 { 43 | x: -3.0, 44 | y: -4.0, 45 | }), "(-1,-2) to (-3,-4) should be 225deg"); 46 | 47 | assert_eq!(315.0, graphics::direction(Point2 { 48 | x: -1.0, 49 | y: -2.0, 50 | }, Point2 { 51 | x: 1.0, 52 | y: -4.0, 53 | }), "(-1,-2) to (1,-4) should be 315deg"); 54 | } 55 | 56 | #[test] 57 | fn direction_movement_test() { 58 | use cgmath::Point2; 59 | use crate::graphics; 60 | 61 | assert_eq!(Point2 { x: 1.0, y: 0.0 }, 62 | graphics::direction_movement( 63 | graphics::direction(Point2 { 64 | x: 1.0, 65 | y: 0.0, 66 | }, Point2 { 67 | x: 2.0, 68 | y: 0.0, 69 | }) 70 | ), "(1,0) to (2,0) should be (1,0)"); 71 | 72 | assert_eq!(Point2 { x: -0.00000004371139, y: 1.0 }, 73 | graphics::direction_movement( 74 | graphics::direction(Point2 { 75 | x: 0.0, 76 | y: 1.0, 77 | }, Point2 { 78 | x: 0.0, 79 | y: 2.0, 80 | }) 81 | ), "(0,1) to (0,2) should be (0,1)"); 82 | 83 | assert_eq!(Point2 { x: 0.70710677, y: 0.70710677 }, // 0.71 = sqrt(2) / 2.0 84 | graphics::direction_movement( 85 | graphics::direction(Point2 { 86 | x: -2.0, 87 | y: -2.0, 88 | }, Point2 { 89 | x: -1.0, 90 | y: -1.0, 91 | }) 92 | ), "(-2,-2) to (-1,-1) should be 45deg"); 93 | 94 | assert_eq!(Point2 { x: -0.7071068, y: -0.7071067 }, // 0.71 = sqrt(2) / 2.0 95 | graphics::direction_movement( 96 | graphics::direction(Point2 { 97 | x: -1.0, 98 | y: -1.0, 99 | }, Point2 { 100 | x: -2.0, 101 | y: -2.0, 102 | }) 103 | ), "(-1,-1) to (-2,-2) should be 225deg"); 104 | } 105 | 106 | #[test] 107 | fn tile_to_coords_test() { 108 | use cgmath::Point2; 109 | use crate::graphics::coords_to_tile; 110 | use crate::shaders::Position; 111 | 112 | let up = Position::new(0.0, -5385.0); 113 | let down = Position::new(0.0, 5385.0); 114 | let right = Position::new(-5995.0, 0.0); 115 | let left = Position::new(5995.0, 0.0); 116 | 117 | assert_eq!(coords_to_tile(up), Point2::new(1, 1), "Up corner"); 118 | 119 | assert_eq!(coords_to_tile(down), Point2::new(126, 126), "Down corner"); 120 | 121 | assert_eq!(coords_to_tile(right), Point2::new(126, 1), "Right corner"); 122 | 123 | assert_eq!(coords_to_tile(left), Point2::new(1, 126), "Left corner"); 124 | } 125 | -------------------------------------------------------------------------------- /src/graphics/mesh.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Matrix2, Point2, Vector2}; 2 | use cgmath::Angle; 3 | use cgmath::Deg; 4 | use gfx::Resources; 5 | use gfx::traits::FactoryExt; 6 | 7 | use crate::graphics::orientation::Orientation; 8 | use crate::graphics::texture::Texture; 9 | use crate::shaders::VertexData; 10 | 11 | #[derive(Clone, Copy)] 12 | pub enum Geometry { 13 | Triangle, 14 | Rectangle, 15 | } 16 | 17 | fn triangle_mesh(w: f32, h: f32) -> [VertexData; 3] { 18 | [ 19 | VertexData::new([0.0, 0.0], [0.5, 1.0]), 20 | VertexData::new([w * 2.0, -h], [1.0, 0.0]), 21 | VertexData::new([-w * 2.0, -h], [0.0, 0.0]), 22 | ] 23 | } 24 | 25 | fn rectangle_mesh(w: f32, h: f32) -> [VertexData; 4] { 26 | [ 27 | VertexData::new([-w, -h], [0.0, 1.0]), 28 | VertexData::new([w, -h], [1.0, 1.0]), 29 | VertexData::new([w, h], [1.0, 0.0]), 30 | VertexData::new([-w, h], [0.0, 0.0]), 31 | ] 32 | } 33 | 34 | fn edit_vertices(w: f32, 35 | h: f32, 36 | geometry: Geometry, 37 | scale: Option>, 38 | rotation: Option, 39 | orientation: Option) -> Vec { 40 | let mesh = match geometry { 41 | Geometry::Rectangle => rectangle_mesh(w, h).to_vec(), 42 | Geometry::Triangle => triangle_mesh(w, h).to_vec(), 43 | }; 44 | 45 | let scale_matrix = scale.unwrap_or_else(|| Matrix2::new(1.0, 0.0, 0.0, 1.0)); 46 | 47 | let rot = rotation.unwrap_or(0.0); 48 | 49 | mesh.iter() 50 | .map(|el| { 51 | let cos = Angle::cos(Deg(rot)); 52 | let sin = Angle::sin(Deg(rot)); 53 | 54 | let x_skew = match orientation { 55 | Some(Orientation::UpLeft) => Angle::tan(Deg(50.0)), 56 | Some(Orientation::UpRight) => Angle::tan(Deg(-50.0)), 57 | _ => 0.0, 58 | }; 59 | 60 | let y_skew = match orientation { 61 | Some(Orientation::DownRight) => Angle::tan(Deg(-22.0)), 62 | Some(Orientation::DownLeft) => Angle::tan(Deg(20.0)), 63 | Some(Orientation::Left) => Angle::tan(Deg(63.0)), 64 | Some(Orientation::Right) => Angle::tan(Deg(-61.0)), 65 | _ => 0.0, 66 | }; 67 | let skew_matrix = Matrix2::::new(1.0, y_skew, x_skew, 1.0); 68 | let rotation_matrix = Matrix2::::new(cos, -sin, sin, cos); 69 | let translate = match orientation { 70 | Some(Orientation::UpLeft) => Vector2::::new(0.0, -20.0), 71 | Some(Orientation::UpRight) => Vector2::::new(-2.0, -22.0), 72 | Some(Orientation::DownLeft) => Vector2::::new(0.0, -14.0), 73 | Some(Orientation::DownRight) => Vector2::::new(-4.0, -14.0), 74 | Some(Orientation::Normal) => Vector2::::new(-4.0, 4.0), 75 | Some(Orientation::Left) => Vector2::::new(55.0, -5.0), 76 | Some(Orientation::Right) => Vector2::::new(-58.0, -7.0), 77 | Some(Orientation::Down) => Vector2::::new(-1.0, 7.0), 78 | Some(Orientation::Up) => Vector2::::new(-2.0, -8.0), 79 | None => Vector2::::new(0.0, 0.0), 80 | }; 81 | let transformation_vec = 82 | scale_matrix * 83 | (skew_matrix * 84 | (rotation_matrix)); 85 | 86 | let edited_vertex_data = 87 | translate + transformation_vec * Vector2::::new(el.pos[0] as f32, el.pos[1] as f32); 88 | 89 | VertexData { pos: [edited_vertex_data.x, edited_vertex_data.y], uv: el.uv } 90 | }) 91 | .collect::>() 92 | } 93 | 94 | #[derive(Clone)] 95 | pub struct PlainMesh where R: Resources { 96 | pub slice: gfx::Slice, 97 | pub vertex_buffer: gfx::handle::Buffer, 98 | } 99 | 100 | impl PlainMesh where R: gfx::Resources { 101 | pub fn new(factory: &mut F, vertices: &[VertexData], indices: &[u16]) -> PlainMesh where F: gfx::Factory { 102 | let (vertex_buffer, slice) = factory.create_vertex_buffer_with_slice(vertices, indices); 103 | PlainMesh { 104 | slice, 105 | vertex_buffer, 106 | } 107 | } 108 | 109 | pub fn new_with_data(factory: &mut F, size: Point2, scale: Option>, rotation: Option, orientation: Option) -> PlainMesh where F: gfx::Factory { 110 | let w = size.x; 111 | let h = size.y; 112 | 113 | let vertex_data = edit_vertices(w, h, Geometry::Rectangle, scale, rotation, orientation); 114 | 115 | let indices: &[u16] = &[0, 1, 2, 2, 3, 0]; 116 | 117 | let (vertex_buffer, slice) = factory.create_vertex_buffer_with_slice(&vertex_data[..], indices); 118 | PlainMesh { 119 | slice, 120 | vertex_buffer, 121 | } 122 | } 123 | } 124 | 125 | #[derive(Clone)] 126 | pub struct TexturedMesh where R: Resources { 127 | pub slice: gfx::Slice, 128 | pub vertex_buffer: gfx::handle::Buffer, 129 | pub texture: Texture, 130 | } 131 | 132 | impl TexturedMesh where R: gfx::Resources { 133 | pub fn new(factory: &mut F, vertices: &[VertexData], indices: &[u16], texture: Texture) -> TexturedMesh where F: gfx::Factory { 134 | let mesh = PlainMesh::new(factory, vertices, indices); 135 | TexturedMesh { 136 | slice: mesh.slice, 137 | vertex_buffer: mesh.vertex_buffer, 138 | texture, 139 | } 140 | } 141 | } 142 | 143 | #[derive(Clone)] 144 | pub struct RectangularTexturedMesh where R: Resources { 145 | pub mesh: TexturedMesh, 146 | pub size: Point2, 147 | } 148 | 149 | impl RectangularTexturedMesh where R: gfx::Resources { 150 | pub fn new(factory: &mut F, 151 | texture: Texture, 152 | geometry: Geometry, 153 | size: Point2, 154 | scale: Option>, 155 | rotation: Option, 156 | orientation: Option) -> RectangularTexturedMesh where F: gfx::Factory { 157 | let vertex_data = edit_vertices(size.x, size.y, geometry, scale, rotation, orientation); 158 | 159 | let indices = match geometry { 160 | Geometry::Rectangle => vec![0, 1, 2, 2, 3, 0], 161 | Geometry::Triangle => vec![0, 1, 2, 0], 162 | }; 163 | 164 | let mesh = TexturedMesh::new(factory, &vertex_data, &indices.as_slice(), texture); 165 | RectangularTexturedMesh { 166 | mesh, 167 | size, 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/graphics/mod.rs: -------------------------------------------------------------------------------- 1 | use std::f32; 2 | 3 | use cgmath::{Angle, Deg, Point2}; 4 | use num::{Num, NumCast}; 5 | 6 | use crate::bullet::BulletDrawable; 7 | use crate::character::CharacterDrawable; 8 | use crate::game::{constants::{RESOLUTION_Y, TERRAIN_OBJECTS, TILE_SIZE, TILES_PCS_H, TILES_PCS_W, Y_OFFSET}, get_rand_from_range}; 9 | use crate::game::constants::TILE_WIDTH; 10 | use crate::gfx_app::{mouse_controls::MouseInputState}; 11 | use crate::graphics::{dimensions::Dimensions, orientation::Orientation}; 12 | use crate::shaders::Position; 13 | use crate::terrain_object::TerrainObjectDrawable; 14 | use crate::zombie::ZombieDrawable; 15 | 16 | pub mod camera; 17 | pub mod dimensions; 18 | mod graphics_test; 19 | pub mod mesh; 20 | pub mod orientation; 21 | pub mod texture; 22 | 23 | const Y_MODIFIER: f32 = 0.9; 24 | 25 | #[derive(Default)] 26 | pub struct DeltaTime(pub f64); 27 | 28 | #[derive(Default)] 29 | pub struct GameTime(pub u64); 30 | 31 | pub fn flip_y_axel(point: Point2) -> Point2 { 32 | Point2::new(point.x, RESOLUTION_Y as f32 - point.y) 33 | } 34 | 35 | pub fn direction(start_point: Point2, end_point: Point2) -> f32 { 36 | let theta = Angle::atan2(end_point.y - start_point.y, end_point.x - start_point.x); 37 | let Deg(angle) = theta; 38 | if angle < 0.0 { 360.0 + angle } else { angle } 39 | } 40 | 41 | pub fn direction_movement(direction: f32) -> Point2 { 42 | let angle = Deg(direction); 43 | Point2::new(Angle::cos(angle), Angle::sin(angle)) 44 | } 45 | 46 | pub fn direction_movement_180(movement_direction: Point2) -> Point2 { 47 | let angle = Deg(direction(Point2::new(0.0, 0.0), movement_direction) + 180.0); 48 | Point2::new(Angle::cos(angle), Angle::sin(angle)) 49 | } 50 | 51 | pub fn orientation_to_direction(angle_in_degrees: f32) -> Orientation { 52 | match angle_in_degrees as u32 { 53 | 345..=360 | 0..=22 => Orientation::Right, 54 | 23..=68 => Orientation::UpRight, 55 | 69..=114 => Orientation::Up, 56 | 115..=160 => Orientation::UpLeft, 57 | 161..=206 => Orientation::Left, 58 | 207..=252 => Orientation::DownLeft, 59 | 253..=298 => Orientation::Down, 60 | 299..=344 => Orientation::DownRight, 61 | _ => unreachable!("Invalid orientation") 62 | } 63 | } 64 | 65 | pub fn get_orientation_from_center(mouse_input: &MouseInputState, dim: &Dimensions) -> Orientation { 66 | if let Some(end_point_gl) = mouse_input.left_click_point { 67 | let start_point = Point2::new(dim.window_width / 2.0 * dim.hidpi_factor, dim.window_height / 2.0 * dim.hidpi_factor); 68 | let dir = direction(start_point, flip_y_axel(end_point_gl)); 69 | orientation_to_direction(dir) 70 | } else { 71 | Orientation::Right 72 | } 73 | } 74 | 75 | pub fn overlaps(area: Position, el: Position, width: f32, height: f32) -> bool { 76 | area.x() - width < el.x() && 77 | area.x() + width > el.x() && 78 | area.y() - height < el.y() && 79 | area.y() + height > el.y() 80 | } 81 | 82 | pub fn is_within_map_borders(point: Point2) -> bool { 83 | point.x < (TILES_PCS_W - 1) && point.y < (TILES_PCS_H - 1) 84 | } 85 | 86 | pub fn can_move(screen_pos: Position) -> bool { 87 | let point = coords_to_tile(screen_pos); 88 | is_within_map_borders(Point2::new(point.x as usize, point.y as usize)) 89 | } 90 | 91 | fn is_not_terrain_object(pos: Point2) -> bool 92 | where T: NumCast + Num, i32: std::cmp::PartialEq { 93 | !TERRAIN_OBJECTS.iter().any(|e| (e[0] == pos.x) && (e[1] == pos.y)) 94 | } 95 | 96 | fn is_map_tile(pos: Point2) -> bool { 97 | pos.x > 0 && pos.y > 0 && pos.x < (TILES_PCS_W - 2) as i32 && pos.y < (TILES_PCS_H - 2) as i32 98 | } 99 | 100 | pub fn can_move_to_tile(screen_pos: Position) -> bool { 101 | let tile_pos = coords_to_tile(screen_pos); 102 | is_not_terrain_object(tile_pos) && is_map_tile(tile_pos) 103 | } 104 | 105 | pub fn check_terrain_elevation(critter_pos: Position, objects: &[[i32; 2]]) -> f32 { 106 | let offset = TILE_SIZE / 2.0; 107 | let nearest_hill = objects.iter() 108 | .map(|x| { 109 | position_distance( 110 | critter_pos, 111 | Position::new(TILE_SIZE * -x[0] as f32, TILE_SIZE * -x[1] as f32).tile_center(0.0, offset) 112 | ) 113 | }) 114 | .fold(100_000_000f32, |mut min, val| { 115 | if val < min { 116 | min = val; 117 | } 118 | min 119 | }); 120 | 121 | if nearest_hill < TILE_SIZE * 2.0 { 122 | (nearest_hill - TILE_SIZE * 2.0).abs() 123 | } else { 124 | 0.0 125 | } 126 | } 127 | 128 | pub fn set_position(x: i32, y: i32) -> Position { 129 | let x_val = x as f32; 130 | let y_val = y as f32; 131 | Position::new( 132 | TILE_SIZE * x_val, 133 | TILE_SIZE * Y_MODIFIER * y_val, 134 | ) 135 | } 136 | 137 | pub fn coords_to_tile(position: Position) -> Point2 { 138 | let pos = Point2::new(-position.x(), position.y() / Y_MODIFIER + Y_OFFSET); 139 | Point2::new(((pos.x + pos.y) / TILE_WIDTH) as i32, ((pos.y - pos.x) / TILE_WIDTH) as i32) 140 | } 141 | 142 | fn round(number: f32, precision: usize) -> f32 { 143 | let ten: f32 = 10.0; 144 | let divider = ten.powf(precision as f32); 145 | (number * divider).round() / divider 146 | } 147 | 148 | pub fn get_nearest_random_tile_position(pos: Position) -> Position { 149 | fn iter(pos: Position) -> Position { 150 | let offset = Position::new(get_rand_from_range(-2, 2) as f32, get_rand_from_range(-2, 2) as f32); 151 | let offset_point = Position::new( 152 | round(offset.x() * TILE_WIDTH - offset.y() / TILE_WIDTH * Y_MODIFIER, 3), 153 | round(offset.y() * TILE_WIDTH - offset.y() / TILE_WIDTH * Y_MODIFIER, 3), 154 | ); 155 | pos + offset_point 156 | } 157 | loop { 158 | let res = iter(pos); 159 | if can_move_to_tile(res) { 160 | return res; 161 | } 162 | } 163 | } 164 | 165 | pub fn distance(a: f32, b: f32) -> f32 { 166 | (a.powf(2.0) + b.powf(2.0)).sqrt() 167 | } 168 | 169 | fn position_distance(a: Position, b: Position) -> f32 { 170 | let d = a - b; 171 | distance(d.x(), d.y()) 172 | } 173 | 174 | pub enum Drawables<'b> { 175 | Bullet(&'b BulletDrawable), 176 | Character(&'b mut CharacterDrawable), 177 | TerrainAmmo(&'b TerrainObjectDrawable), 178 | TerrainHouse(&'b TerrainObjectDrawable), 179 | TerrainTree(&'b TerrainObjectDrawable), 180 | Zombie(&'b mut ZombieDrawable), 181 | } 182 | 183 | impl<'b> Drawables<'b> { 184 | pub fn get_vertical_pos(drawable: &Drawables) -> f32 { 185 | match drawable { 186 | Drawables::Bullet(e) => e.position.y(), 187 | Drawables::Zombie(e) => e.position.y(), 188 | Drawables::TerrainAmmo(e) => e.position.y(), 189 | Drawables::TerrainHouse(e) => e.position.y(), 190 | Drawables::TerrainTree(e) => e.position.y(), 191 | Drawables::Character(e) => e.position.y(), 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/graphics/orientation.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result}; 2 | 3 | #[derive(Clone, Copy, PartialEq)] 4 | pub enum Orientation { 5 | Right, 6 | UpRight, 7 | Up, 8 | UpLeft, 9 | Left, 10 | DownLeft, 11 | Down, 12 | DownRight, 13 | Normal, 14 | } 15 | 16 | impl Display for Orientation { 17 | fn fmt(&self, f: &mut Formatter) -> Result { 18 | match *self { 19 | Orientation::Right => write!(f, "Right"), 20 | Orientation::UpRight => write!(f, "UpRight"), 21 | Orientation::Up => write!(f, "Up"), 22 | Orientation::UpLeft => write!(f, "UpLeft"), 23 | Orientation::Left => write!(f, "Left"), 24 | Orientation::DownLeft => write!(f, "DownLeft"), 25 | Orientation::Down => write!(f, "Down"), 26 | Orientation::DownRight => write!(f, "DownRight"), 27 | Orientation::Normal => write!(f, "Normal"), 28 | } 29 | } 30 | } 31 | 32 | #[derive(Clone, PartialEq)] 33 | pub enum Stance { 34 | Walking, 35 | Running, 36 | Firing, 37 | Still, 38 | NormalDeath, 39 | CriticalDeath, 40 | } 41 | 42 | impl Display for Stance { 43 | fn fmt(&self, f: &mut Formatter) -> Result { 44 | match *self { 45 | Stance::Walking => write!(f, "Walking"), 46 | Stance::Running => write!(f, "Running"), 47 | Stance::Firing => write!(f, "Firing"), 48 | Stance::Still => write!(f, "Still"), 49 | Stance::NormalDeath => write!(f, "NormalDeath"), 50 | Stance::CriticalDeath => write!(f, "CriticalDeath"), 51 | } 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/graphics/texture.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::hash::BuildHasher; 3 | use std::io::Cursor; 4 | 5 | use cgmath::Point2; 6 | use gfx::{Factory, format::Rgba8, handle::ShaderResourceView, Resources, texture::{AaMode, Kind, Mipmap, Size}}; 7 | use rusttype::Font; 8 | 9 | use crate::gfx_app::ColorFormat; 10 | use crate::hud::font::draw_text; 11 | 12 | #[derive(Clone)] 13 | pub struct Texture where R: Resources { 14 | pub raw: ShaderResourceView, 15 | pub size: Point2, 16 | } 17 | 18 | impl Texture where R: Resources { 19 | pub fn new(raw: ShaderResourceView, size: Option>) -> Texture { 20 | Texture { 21 | raw, 22 | size: size.map_or(Point2::new(1, 1), |e| e), 23 | } 24 | } 25 | } 26 | 27 | pub fn load_texture(factory: &mut F, data: &[u8]) -> ShaderResourceView where R: Resources, F: Factory { 28 | let img = image::load(Cursor::new(data), image::PNG).unwrap().to_rgba(); 29 | let (width, height) = img.dimensions(); 30 | let kind = Kind::D2(width as Size, height as Size, AaMode::Single); 31 | match factory.create_texture_immutable_u8::(kind, Mipmap::Provided, &[&img]) { 32 | Ok(val) => val.1, 33 | Err(e) => panic!("Couldn't load texture {:?}", e) 34 | } 35 | } 36 | 37 | pub fn load_raw_texture(factory: &mut F, data: &[u8], size: Point2) -> ShaderResourceView 38 | where R: Resources, F: Factory { 39 | let kind = Kind::D2(size.x as Size, size.y as Size, AaMode::Single); 40 | let mipmap = Mipmap::Provided; 41 | match factory 42 | .create_texture_immutable_u8::(kind, mipmap, &[data]) { 43 | Ok(val) => val.1, 44 | Err(e) => panic!("Couldn't load raw texture {:?}", e) 45 | } 46 | } 47 | 48 | pub fn text_texture<'a, R, F, S: BuildHasher>(factory: &mut F, 49 | font: &Font, 50 | texts: &[&str], 51 | texture_cache: &'a mut HashMap, S>) 52 | -> &'a mut HashMap, S> 53 | where R: Resources, F: Factory { 54 | let text_texture_height = 100.0; 55 | texts.iter().for_each(|text| { 56 | let (texture_size, texture_data) = draw_text(&font, text_texture_height, text); 57 | let texture = load_raw_texture(factory, &texture_data.as_slice(), texture_size); 58 | texture_cache.insert((*text).to_string(), Texture::new(texture, None)); 59 | }); 60 | texture_cache 61 | } 62 | -------------------------------------------------------------------------------- /src/hud/font.rs: -------------------------------------------------------------------------------- 1 | use cgmath::Point2; 2 | use rusttype::{Font, point, Scale}; 3 | 4 | pub fn draw_text(font: &Font, font_size: f32, text: &str) -> (Point2, Vec) { 5 | let scale = Scale { 6 | x: font_size / 2.0, 7 | y: font_size, 8 | }; 9 | let v_metrics = font.v_metrics(scale); 10 | let offset = point(0.0, v_metrics.ascent); 11 | let glyphs: Vec<_> = font.layout(text, scale, offset).collect(); 12 | let pixel_height = font_size.ceil() as usize; 13 | let width = glyphs 14 | .iter() 15 | .rev() 16 | .map(|g| g.position().x as f32 + g.unpositioned().h_metrics().advance_width) 17 | .next() 18 | .unwrap_or(0.0) 19 | .ceil() as usize; 20 | 21 | let mut pixel_data = vec![0_u8; 4 * width * pixel_height]; 22 | let mapping_scale = 255.0; 23 | for g in glyphs { 24 | if let Some(bb) = g.pixel_bounding_box() { 25 | g.draw(|x, y, v| { 26 | let v = (v * mapping_scale + 0.5) as u8; 27 | let x = x as i32 + bb.min.x; 28 | let y = y as i32 + bb.min.y; 29 | if v > 0 && x >= 0 && x < width as i32 && y >= 0 && y < pixel_height as i32 { 30 | let i = (x as usize + y as usize * width) * 4; 31 | pixel_data[i] = 255; 32 | pixel_data[i + 1] = 255; 33 | pixel_data[i + 2] = 255; 34 | pixel_data[i + 3] = v; 35 | } 36 | }); 37 | } 38 | } 39 | let size = Point2 { 40 | x: width as i32, 41 | y: pixel_height as i32, 42 | }; 43 | (size, pixel_data) 44 | } 45 | -------------------------------------------------------------------------------- /src/hud/hud_objects.rs: -------------------------------------------------------------------------------- 1 | 2 | use crate::game::constants::{CURRENT_AMMO_TEXT, CURRENT_MAGAZINE_TEXT, GAME_VERSION}; 3 | use crate::hud::TextDrawable; 4 | use crate::shaders::Position; 5 | 6 | pub struct HudObjects { 7 | pub objects: Vec, 8 | } 9 | 10 | impl HudObjects { 11 | pub fn new() -> HudObjects { 12 | HudObjects { 13 | objects: vec![ 14 | TextDrawable::new(GAME_VERSION, Position::origin()), 15 | TextDrawable::new(CURRENT_AMMO_TEXT, Position::new(1.9, -1.9)), 16 | TextDrawable::new(CURRENT_MAGAZINE_TEXT, Position::new(1.9, -1.94)), 17 | ] 18 | } 19 | } 20 | } 21 | 22 | impl specs::prelude::Component for HudObjects { 23 | type Storage = specs::storage::VecStorage; 24 | } 25 | -------------------------------------------------------------------------------- /src/hud/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use cgmath::Point2; 4 | use rusttype::FontCollection; 5 | use specs::{ReadStorage, WriteStorage}; 6 | 7 | use crate::character::CharacterDrawable; 8 | use crate::gfx_app::ColorFormat; 9 | use crate::gfx_app::DepthFormat; 10 | use crate::graphics::{mesh::RectangularTexturedMesh}; 11 | use crate::graphics::texture::{text_texture, Texture}; 12 | use crate::shaders::{Position, text_pipeline}; 13 | use crate::graphics::mesh::Geometry; 14 | 15 | pub mod font; 16 | pub mod hud_objects; 17 | 18 | const SHADER_VERT: &[u8] = include_bytes!("../shaders/text.v.glsl"); 19 | const SHADER_FRAG: &[u8] = include_bytes!("../shaders/text.f.glsl"); 20 | 21 | pub struct TextDrawable { 22 | text: String, 23 | position: Position, 24 | } 25 | 26 | impl<'a> TextDrawable { 27 | pub fn new(text: &str, position: Position) -> TextDrawable { 28 | TextDrawable { 29 | text: text.to_string(), 30 | position, 31 | } 32 | } 33 | 34 | pub fn update(&mut self, new_text: String) { 35 | self.text = new_text; 36 | } 37 | } 38 | 39 | impl specs::prelude::Component for TextDrawable { 40 | type Storage = specs::storage::HashMapStorage; 41 | } 42 | 43 | pub struct TextDrawSystem { 44 | bundle: gfx::pso::bundle::Bundle>, 45 | texture_cache: HashMap>, 46 | pub current_text: String, 47 | } 48 | 49 | impl TextDrawSystem { 50 | pub fn new(factory: &mut F, 51 | texts: &[&str], 52 | current_text: &str, 53 | rtv: gfx::handle::RenderTargetView, 54 | dsv: gfx::handle::DepthStencilView) -> TextDrawSystem 55 | where F: gfx::Factory { 56 | use gfx::traits::FactoryExt; 57 | 58 | let font_bytes = &include_bytes!("../../assets/DejaVuSans.ttf")[..]; 59 | let font = FontCollection::from_bytes(font_bytes as &[u8]) 60 | .unwrap_or_else(|e| panic!("Font loading error: {}", e)) 61 | .into_font().unwrap_or_else(|e| panic!("into_font error: {}", e)); 62 | 63 | let mut texture_cache: HashMap> = HashMap::new(); 64 | 65 | text_texture(factory, &font, texts, &mut texture_cache); 66 | 67 | let pso = factory.create_pipeline_simple(SHADER_VERT, SHADER_FRAG, text_pipeline::new()) 68 | .expect("HUD shader loading error"); 69 | 70 | let texture = texture_cache[current_text].clone(); 71 | 72 | let rect_mesh = RectangularTexturedMesh::new(factory, texture, Geometry::Rectangle, Point2::new(1.0, 1.0), None, None, None); 73 | 74 | let pipeline_data = text_pipeline::Data { 75 | vbuf: rect_mesh.mesh.vertex_buffer, 76 | position_cb: factory.create_constant_buffer(1), 77 | text_sheet: (rect_mesh.mesh.texture.raw, factory.create_sampler_linear()), 78 | out_color: rtv, 79 | out_depth: dsv, 80 | }; 81 | 82 | TextDrawSystem { 83 | bundle: gfx::Bundle::new(rect_mesh.mesh.slice, pso, pipeline_data), 84 | texture_cache, 85 | current_text: current_text.to_string(), 86 | } 87 | } 88 | 89 | pub fn draw(&mut self, 90 | drawable: &TextDrawable, 91 | encoder: &mut gfx::Encoder) 92 | where C: gfx::CommandBuffer { 93 | encoder.update_constant_buffer(&self.bundle.data.position_cb, &drawable.position); 94 | if self.current_text.trim() != drawable.text.trim() { 95 | self.current_text = drawable.text.to_owned(); 96 | self.bundle.data.text_sheet.0 = self.texture_cache[&drawable.text].raw.clone(); 97 | } 98 | self.bundle.encode(encoder); 99 | } 100 | } 101 | 102 | pub struct PreDrawSystem; 103 | 104 | impl<'a> specs::prelude::System<'a> for PreDrawSystem { 105 | type SystemData = (ReadStorage<'a, CharacterDrawable>, 106 | WriteStorage<'a, hud_objects::HudObjects>); 107 | 108 | fn run(&mut self, (character_drawable, mut hud_objects): Self::SystemData) { 109 | use specs::join::Join; 110 | 111 | for (cd, huds) in (&character_drawable, &mut hud_objects).join() { 112 | let new_ammo_text = format!("Ammo {}", cd.stats.ammunition); 113 | let new_mag_text = format!("Magazines {}/2", cd.stats.magazines); 114 | huds.objects[1].update(new_ammo_text); 115 | huds.objects[2].update(new_mag_text); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate getopts; 2 | #[macro_use] 3 | extern crate gfx; 4 | 5 | use getopts::Options; 6 | 7 | use crate::game::constants::{GAME_TITLE, GAME_VERSION}; 8 | use crate::gfx_app::GameOptions; 9 | 10 | mod audio; 11 | mod bullet; 12 | mod gfx_app; 13 | mod game; 14 | mod data; 15 | mod critter; 16 | pub mod graphics; 17 | mod hud; 18 | mod terrain_object; 19 | mod terrain_shape; 20 | mod terrain; 21 | mod character; 22 | mod shaders; 23 | mod zombie; 24 | 25 | fn print_usage() { 26 | println!("USAGE:\nhinterland [FLAGS]\n\nFLAGS:\n-h, --help\t\t\tPrints help information\n-v, --version\t\t\tPrints version information\n-w, --windowed_mode\t\tRun game in windowed mode"); 27 | } 28 | 29 | fn print_version() { 30 | println!("{} - {}", GAME_TITLE, GAME_VERSION) 31 | } 32 | 33 | pub fn main() { 34 | let args = std::env::args().collect::>(); 35 | let mut opts = Options::new(); 36 | opts.optflag("w", "windowed_mode", "Run game in windowed mode"); 37 | opts.optflag("h", "help", "Prints help information"); 38 | opts.optflag("v", "version", "Prints version information"); 39 | 40 | let matches = match opts.parse(&args[1..]) { 41 | Ok(matching_args) => { matching_args } 42 | Err(err) => { panic!("{}", err.to_string()) } 43 | }; 44 | 45 | if matches.opt_present("help") { 46 | print_usage(); 47 | return; 48 | } 49 | 50 | if matches.opt_present("version") { 51 | print_version(); 52 | return; 53 | } 54 | 55 | let game_opt = GameOptions::new(matches.opt_present("windowed_mode")); 56 | let mut window = gfx_app::WindowContext::new(game_opt); 57 | gfx_app::init::run(&mut window); 58 | } 59 | -------------------------------------------------------------------------------- /src/shaders/bullet.f.glsl: -------------------------------------------------------------------------------- 1 | #version 150 core 2 | 3 | out vec4 Target0; 4 | 5 | void main() { 6 | Target0 = vec4(0.5, 0.2, 0.2, 0.8); 7 | } 8 | -------------------------------------------------------------------------------- /src/shaders/bullet.v.glsl: -------------------------------------------------------------------------------- 1 | #version 150 core 2 | 3 | in vec3 a_Pos; 4 | 5 | uniform b_VsLocals { 6 | mat4 u_Model; 7 | mat4 u_View; 8 | mat4 u_Proj; 9 | }; 10 | 11 | uniform b_BulletPosition { 12 | vec2 a_position; 13 | }; 14 | 15 | uniform b_BulletRotation { 16 | float a_rotation; 17 | }; 18 | 19 | void main() { 20 | vec3 rot_pos = mat3(cos(a_rotation), -sin(a_rotation), 0.0, 21 | sin(a_rotation), cos(a_rotation), 0.0, 22 | 0.0, 0.0, 1.0) * a_Pos; 23 | 24 | gl_Position = vec4(a_position, 0.0, 0.0) + vec4(rot_pos, 1.0) * u_Proj * u_View * u_Model; 25 | } 26 | -------------------------------------------------------------------------------- /src/shaders/character.f.glsl: -------------------------------------------------------------------------------- 1 | #version 150 core 2 | 3 | in vec2 v_BufPos; 4 | out vec4 Target0; 5 | 6 | uniform sampler2D t_CharacterSheet; 7 | 8 | void main() { 9 | vec4 tex = texture(t_CharacterSheet, v_BufPos).rgba; 10 | if(tex.a < 0.1) { 11 | discard; 12 | } 13 | tex.r = smoothstep(0.1, 1.0, tex.r); 14 | tex.g = smoothstep(0.1, 1.0, tex.g); 15 | tex.b = smoothstep(0.1, 1.0, tex.b); 16 | Target0 = tex; 17 | } 18 | -------------------------------------------------------------------------------- /src/shaders/character.v.glsl: -------------------------------------------------------------------------------- 1 | #version 150 core 2 | 3 | in vec3 a_Pos; 4 | in vec2 a_BufPos; 5 | out vec2 v_BufPos; 6 | 7 | uniform b_VsLocals { 8 | mat4 u_Model; 9 | mat4 u_View; 10 | mat4 u_Proj; 11 | }; 12 | 13 | layout (std140) uniform b_CharacterSprite { 14 | float x_div; 15 | float y_div; 16 | int a_row; 17 | float a_index; 18 | }; 19 | 20 | uniform b_CharacterPosition { 21 | vec2 a_position; 22 | }; 23 | 24 | void main() { 25 | v_BufPos = vec2(a_BufPos); 26 | 27 | v_BufPos.y += y_div; 28 | if (a_row > 1) { 29 | v_BufPos.y /= 2.0; 30 | } 31 | v_BufPos.x /= x_div; 32 | v_BufPos.x += a_index / x_div; 33 | 34 | gl_Position = vec4(a_position, 0.0, 0.0) + u_Proj * u_View * u_Model * vec4(a_Pos, 1.0); 35 | } 36 | -------------------------------------------------------------------------------- /src/shaders/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::{Display, Formatter, Result}, ops::{Add, Sub}}; 2 | 3 | use cgmath::BaseFloat; 4 | #[allow(clippy::useless_attribute)] 5 | #[allow(clippy::single_component_path_imports)] 6 | use gfx; 7 | 8 | gfx_defines! { 9 | constant TileMapData { 10 | data: [f32; 4] = "data", 11 | } 12 | 13 | constant Position { 14 | position: [f32; 2] = "a_position", 15 | } 16 | 17 | constant Rotation { 18 | rotation: f32 = "a_rotation", 19 | } 20 | 21 | constant Time { 22 | time_modulo: f32 = "a_time", 23 | } 24 | 25 | constant TilemapSettings { 26 | world_size: [f32; 2] = "u_WorldSize", 27 | tilesheet_size: [f32; 2] = "u_TilesheetSize", 28 | } 29 | 30 | vertex VertexData { 31 | pos: [f32; 2] = "a_Pos", 32 | uv: [f32; 2] = "a_BufPos", 33 | } 34 | 35 | constant CharacterSheet { 36 | x_div: f32 = "x_div", 37 | y_div: f32 = "y_div", 38 | row_idx: u32 = "a_row", 39 | index: f32 = "a_index", 40 | } 41 | 42 | pipeline bullet_pipeline { 43 | vbuf: gfx::VertexBuffer = (), 44 | projection_cb: gfx::ConstantBuffer = "b_VsLocals", 45 | position_cb: gfx::ConstantBuffer = "b_BulletPosition", 46 | rotation_cb: gfx::ConstantBuffer = "b_BulletRotation", 47 | out_color: gfx::RenderTarget = "Target0", 48 | out_depth: gfx::DepthTarget = gfx::preset::depth::LESS_EQUAL_WRITE, 49 | } 50 | 51 | pipeline critter_pipeline { 52 | vbuf: gfx::VertexBuffer = (), 53 | projection_cb: gfx::ConstantBuffer = "b_VsLocals", 54 | position_cb: gfx::ConstantBuffer = "b_CharacterPosition", 55 | character_sprite_cb: gfx::ConstantBuffer = "b_CharacterSprite", 56 | charactersheet: gfx::TextureSampler<[f32; 4]> = "t_CharacterSheet", 57 | out_color: gfx::RenderTarget = "Target0", 58 | out_depth: gfx::DepthTarget = gfx::preset::depth::LESS_EQUAL_WRITE, 59 | } 60 | 61 | pipeline tilemap_pipeline { 62 | vbuf: gfx::VertexBuffer = (), 63 | position_cb: gfx::ConstantBuffer = "b_TileMapPosition", 64 | time_passed_cb: gfx::ConstantBuffer