├── .github ├── FUNDING.yml └── workflows │ └── test.js.yml ├── .gitignore ├── .vscode ├── commandbar.json ├── launch.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── _base.hxml ├── build.dev.hxml ├── build.directx.hxml ├── build.js.hxml ├── build.opengl.hxml ├── res ├── atlas │ └── tiles.aseprite ├── const.json ├── data.cdb ├── fonts │ ├── pixel_unicode_regular_12.png │ ├── pixel_unicode_regular_12.xml │ ├── pixica_mono_regular_16.png │ └── pixica_mono_regular_16.xml ├── lang │ ├── en.po │ └── sourceTexts.pot └── levels │ ├── sampleWorld.ldtk │ └── sampleWorldTiles.aseprite ├── run_js.html ├── setup.hxml ├── src ├── game │ ├── App.hx │ ├── Boot.hx │ ├── Camera.hx │ ├── Const.hx │ ├── Entity.hx │ ├── Fx.hx │ ├── Game.hx │ ├── Level.hx │ ├── Types.hx │ ├── assets │ │ ├── Assets.hx │ │ ├── AssetsDictionaries.hx │ │ ├── CastleDb.hx │ │ ├── ConstDbBuilder.hx │ │ ├── Lang.hx │ │ └── World.hx │ ├── en │ │ └── DebugDrone.hx │ ├── import.hx │ ├── sample │ │ ├── SampleGame.hx │ │ └── SamplePlayer.hx │ ├── tools │ │ ├── AppChildProcess.hx │ │ ├── ChargedAction.hx │ │ ├── GameChildProcess.hx │ │ ├── LPoint.hx │ │ ├── LRect.hx │ │ └── script │ │ │ ├── Api.hx │ │ │ └── Script.hx │ └── ui │ │ ├── Bar.hx │ │ ├── Console.hx │ │ ├── Hud.hx │ │ ├── IconBar.hx │ │ ├── UiComponent.hx │ │ ├── UiGroupController.hx │ │ ├── Window.hx │ │ ├── component │ │ ├── Button.hx │ │ ├── CheckBox.hx │ │ ├── ControlsHelp.hx │ │ └── Text.hx │ │ └── win │ │ ├── DebugWindow.hx │ │ └── SimpleMenu.hx └── langParser │ └── LangParser.hx └── tools.langParser.hxml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: deepnight -------------------------------------------------------------------------------- /.github/workflows/test.js.yml: -------------------------------------------------------------------------------- 1 | name: Test JS build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | build: 13 | 14 | strategy: 15 | matrix: 16 | os: [windows-latest] 17 | haxe: [4.3.3] 18 | fail-fast: true 19 | runs-on: windows-latest 20 | 21 | steps: 22 | # Checkout & install haxe 23 | - uses: actions/checkout@v2 24 | - uses: krdlab/setup-haxe@v1 25 | with: 26 | haxe-version: ${{ matrix.haxe }} 27 | - run: haxe -version 28 | 29 | # Install libs 30 | - run: haxe setup.hxml 31 | - run: haxelib list 32 | 33 | # Try to build 34 | - run: haxe build.js.hxml 35 | 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | redist/ 2 | bin/ 3 | dump/ 4 | res/.tmp/ 5 | res/**/backups/ 6 | 7 | # VScode 8 | .vscode/settings.json 9 | -------------------------------------------------------------------------------- /.vscode/commandbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "skipTerminateQuickPick": true, 3 | "skipSwitchToOutput": false, 4 | "skipErrorMessage": true, 5 | "commands": [ 6 | { 7 | "text": "🍊 DX", 8 | "color": "orange", 9 | "commandType":"exec", 10 | "command": "haxe build.directx.hxml", 11 | "alignment": "right", 12 | "skipTerminateQuickPick": false, 13 | "priority": -9 14 | }, 15 | { 16 | "text": "🍊 OpenGL", 17 | "color": "orange", 18 | "commandType":"exec", 19 | "command": "haxe build.opengl.hxml", 20 | "alignment": "right", 21 | "skipTerminateQuickPick": false, 22 | "priority": -10 23 | }, 24 | { 25 | "text": "Run HL", 26 | "color": "orange", 27 | "command": "hl bin/client.hl", 28 | "alignment": "right", 29 | "skipTerminateQuickPick": false, 30 | "priority": -11 31 | }, 32 | { 33 | "text": "☕ JS", 34 | "color": "yellow", 35 | "commandType":"exec", 36 | "command": "haxe build.js.hxml", 37 | "alignment": "right", 38 | "skipTerminateQuickPick": false, 39 | "priority": -20 40 | }, 41 | { 42 | "text": "Run JS", 43 | "color": "yellow", 44 | "command": "start run_js.html", 45 | "alignment": "right", 46 | "skipTerminateQuickPick": false, 47 | "priority": -21 48 | }, 49 | { 50 | "text": "🅰️ Lang", 51 | "color": "white", 52 | "command": "haxe tools.langParser.hxml", 53 | "alignment": "right", 54 | "skipTerminateQuickPick": false, 55 | "priority": -40 56 | }, 57 | { 58 | "text": "📦 Redist", 59 | "color": "lightgreen", 60 | "command": "haxelib run redistHelper build.directx.hxml build.opengl.hxml build.js.hxml -o redist -p GameBase -zip", 61 | "alignment": "right", 62 | "skipTerminateQuickPick": false, 63 | "priority": -50 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "HL debug", 9 | "request": "launch", 10 | "type": "hl", 11 | "cwd": "${workspaceRoot}", 12 | "preLaunchTask": "HaxeActiveConf" 13 | }, 14 | { 15 | "name": "Chrome JS debug", 16 | "request": "launch", 17 | "type": "chrome", 18 | "file": "${workspaceFolder}/run_js.html", 19 | "preLaunchTask": "HaxeActiveConf" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "HaxeActiveConf", 8 | "type": "haxe", 9 | "args": "active configuration", 10 | "problemMatcher": [ 11 | "$haxe-absolute", 12 | "$haxe", 13 | "$haxe-error", 14 | "$haxe-trace" 15 | ], 16 | "presentation": { 17 | "reveal": "never", 18 | }, 19 | "group": { 20 | "kind": "build", 21 | "isDefault": true 22 | }, 23 | }, 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # GameBase changelog 2 | 3 | ## 2.0 4 | 5 | - General: 6 | - **The official gameBase version is now the former "advanced" branch version. The previous, more minimalistic version, is still available in the `legacy` branch.** 7 | - **Debug Drone**: press `CTRL-SHIFT-D` to spawn a debug Drone. Use arrows to fly around and quickly explore your current level. You can also type `/drone` in console. 8 | - Full **Controller** rework, to provide much better Gamepad support, bindings, combos, etc. 9 | - Full **LDtk** integration (https://ldtk.io), with hot-reloading support. 10 | - Added many comments everywhere. 11 | - Moved all source code to various `src/` subfolders 12 | - Moved all assets related classes to the `assets.*` package 13 | - Added various debug commands to console. Open it up by typing `/`. Enter `/help` to list all available commands. 14 | - Added `/fps` command to console to display FPS chart over time. 15 | - Added `/ctrl` command to visualize and debug controller (keyboard or gamepad). 16 | - Fixed various FPS values issues 17 | - Better "active" level management, through `startLevel()` method 18 | - Cleaned up Main class 19 | - Renamed Main class to App 20 | - Renamed Data class to CastleDb 21 | - Replaced pixel perfect filters with a more optimized one (now using `Nothing` filter) 22 | - Added many comments and docs everywhere 23 | - Added XML doc generation for future proper doc 24 | - Removed SWF target (see you space cowboy) 25 | - Added this CHANGELOG ;) 26 | 27 | - Entity: 28 | - All entities now have a proper width/height and a pivotX/Y factor 29 | - Separated bump X/Y frictions 30 | - Fixed X/Y squash frictions (forgot to use tmod) 31 | - Added a `sightCheck` methods using Bresenham algorithm 32 | - Added `isAlive()` (ie. quick check to both `destroyed` flag and `life>0` check) 33 | - Added `.exists()` to Game and Main 34 | 35 | - Camera: 36 | - Cleanup & rework 37 | - Added zoom support 38 | - Camera slows down when reaching levels bounds 39 | - Camera no longer clamps to level bounds by default 40 | - Added isOnScreen(x,y) 41 | 42 | - UI: 43 | - Added basic notifications to HUD 44 | - Added debug text field to HUD 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sébastien Bénard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | **A lightweight and simple base structure for games, using _[Heaps](https://heaps.io)_ framework and _[Haxe](https://haxe.org)_ language.** 4 | 5 | Latest release notes: [View changelog](CHANGELOG.md). 6 | 7 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/deepnight/gameBase/test.js.yml)](https://github.com/deepnight/gameBase/actions/workflows/test.js.yml) 8 | [![GitHub Repo stars](https://img.shields.io/github/stars/deepnight/gameBase?label=%E2%98%85)](https://github.com/deepnight/gameBase) 9 | 10 | # Install 11 | 12 | ## Legacy or master? 13 | 14 | Two separate branches exist for GameBase: 15 | 16 | - `master`: latest GameBase version, actively maintained. 17 | - `legacy`: the previous Gamebase version. This one is much more minimalistic but it could be useful if you were looking for a very basic framework for Heaps+Haxe. 18 | 19 | The following document will only refer to the `master` branch. 20 | 21 | ## Getting master 22 | 23 | 1. Install **Haxe** and **Hashlink**: [Step-by-step tutorial](https://deepnight.net/tutorial/a-quick-guide-to-installing-haxe/) 24 | 2. Install required libs by running the following command **in the root of the repo**: `haxe setup.hxml` 25 | 26 | # Compile 27 | 28 | From the command line, run either: 29 | 30 | - For **DirectX**: `haxe build.directx.hxml` 31 | - For **OpenGL**: `haxe build.opengl.hxml` 32 | - For **Javascript/WebGL**: `haxe build.js.hxml` 33 | 34 | The `build.dev.hxml` is just a shortcut to one of the previous ones, with added `-debug` flag. 35 | 36 | Run the result with either: 37 | 38 | - For **DirectX/OpenGL**: `hl bin\client.hl` 39 | - For **Javascript**: `start run_js.html` 40 | 41 | # Full guide 42 | 43 | An in-depth tutorial is available here: [Using gamebase to create a game](https://deepnight.net/tutorial/using-my-gamebase-to-create-a-heaps-game/). Please note that this tutorial still refers to the `legacy` branch, even though the general idea is the same in `master` branch. 44 | 45 | ## Sample examples 46 | 47 | The samples are the recommended places to start for the latest `GameBase` version (`main`). 48 | 49 | They should give a pretty hands-on understanding of how entities work and how to integrate `ldtk` to development. 50 | 51 | `SamplePlayer.hx`[SamplePlayer.hx] 52 | 53 | SamplePlayer is an Entity with some extra functionalities: 54 | 55 | - user controlled (using gamepad or keyboard) 56 | - falls with gravity 57 | - has basic level collisions 58 | - some squash animations, because it's cheap and they do the job 59 | 60 | `SampleWorld.hx` 61 | 62 | A small class that just creates a SamplePlayer instance in the sample level. 63 | 64 | ## Localization 65 | 66 | For **localization support** (ie. translating your game texts), you may also check the [following guide](https://deepnight.net/tutorial/part-4-localize-texts-using-po-files/). 67 | 68 | ## Questions 69 | 70 | Any question? Join the [Official Deepnight Games discord](https://deepnight.net/go/discord). 71 | 72 | # Cleanup for your own usage 73 | 74 | You can safely remove the following files/folders from repo root: 75 | 76 | - `.github/` 77 | - `LICENSE` 78 | - `README.md` 79 | - `CHANGELOG.md` 80 | -------------------------------------------------------------------------------- /_base.hxml: -------------------------------------------------------------------------------- 1 | -cp src/game 2 | 3 | -lib castle 4 | -lib heaps 5 | -lib hscript 6 | -lib deepnightLibs 7 | -lib ldtk-haxe-api 8 | -lib heaps-aseprite 9 | 10 | -main Boot 11 | -------------------------------------------------------------------------------- /build.dev.hxml: -------------------------------------------------------------------------------- 1 | build.directx.hxml 2 | -debug -------------------------------------------------------------------------------- /build.directx.hxml: -------------------------------------------------------------------------------- 1 | _base.hxml 2 | -D windowSize=1280x720 3 | -hl bin/client.hl 4 | -lib hldx -------------------------------------------------------------------------------- /build.js.hxml: -------------------------------------------------------------------------------- 1 | _base.hxml 2 | -dce std 3 | -js bin/client.js 4 | -------------------------------------------------------------------------------- /build.opengl.hxml: -------------------------------------------------------------------------------- 1 | _base.hxml 2 | -D windowSize=1280x720 3 | -hl bin/client.hl 4 | -lib hlsdl -------------------------------------------------------------------------------- /res/atlas/tiles.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepnight/gameBase/c5949781eec5a376563d7a7aac678f1669735a86/res/atlas/tiles.aseprite -------------------------------------------------------------------------------- /res/const.json: -------------------------------------------------------------------------------- 1 | { 2 | "myJsonConstant": 2 3 | } -------------------------------------------------------------------------------- /res/data.cdb: -------------------------------------------------------------------------------- 1 | { 2 | "sheets": [ 3 | { 4 | "name": "ConstDb", 5 | "columns": [ 6 | { 7 | "typeStr": "0", 8 | "name": "constId" 9 | }, 10 | { 11 | "typeStr": "8", 12 | "name": "values" 13 | } 14 | ], 15 | "lines": [ 16 | { 17 | "constId": "player", 18 | "values": [ 19 | { 20 | "valueName": "valueA", 21 | "value": 1, 22 | "doc": "This documentation will show up when using your editor auto-completion for this value", 23 | "isInteger": true 24 | }, 25 | { 26 | "valueName": "valueB", 27 | "value": 2.5, 28 | "doc": "Doc for this other value", 29 | "isInteger": false 30 | }, 31 | { 32 | "valueName": "withSubValues", 33 | "value": 0, 34 | "isInteger": false, 35 | "subValues": { 36 | "x": 1.1, 37 | "y": 1.2, 38 | "n": 3 39 | }, 40 | "doc": "You may also define some \"sub values\". Accepted sub value types are: Int, Float, Bool, Text and Color." 41 | } 42 | ] 43 | } 44 | ], 45 | "props": { 46 | "separatorTitles": [ 47 | "This sheet allows to access CastleDB \"constants\" in your code. Just call for example \"Const.db.Player.valueA\" to access float values. These values will be updated on runtime if hot-reloading is enabled. DO NOT MODIFY THE SHEET NAME OR COLUMNS!" 48 | ] 49 | }, 50 | "separatorIds": [ 51 | "player" 52 | ] 53 | }, 54 | { 55 | "name": "ConstDb@values", 56 | "props": { 57 | "hide": true 58 | }, 59 | "separators": [], 60 | "lines": [], 61 | "columns": [ 62 | { 63 | "typeStr": "1", 64 | "name": "valueName", 65 | "display": null 66 | }, 67 | { 68 | "typeStr": "1", 69 | "name": "doc", 70 | "opt": true, 71 | "display": null 72 | }, 73 | { 74 | "typeStr": "4", 75 | "name": "value", 76 | "display": null 77 | }, 78 | { 79 | "typeStr": "2", 80 | "name": "isInteger" 81 | }, 82 | { 83 | "typeStr": "17", 84 | "name": "subValues", 85 | "opt": true, 86 | "display": null 87 | } 88 | ] 89 | }, 90 | { 91 | "name": "ConstDb@values@subValues", 92 | "props": { 93 | "hide": true, 94 | "isProps": true 95 | }, 96 | "separators": [], 97 | "lines": [], 98 | "columns": [ 99 | { 100 | "typeStr": "4", 101 | "name": "x", 102 | "opt": true 103 | }, 104 | { 105 | "typeStr": "4", 106 | "name": "y", 107 | "opt": true, 108 | "display": null 109 | }, 110 | { 111 | "typeStr": "3", 112 | "name": "n", 113 | "opt": true, 114 | "display": null 115 | } 116 | ] 117 | } 118 | ], 119 | "customTypes": [], 120 | "compress": false 121 | } -------------------------------------------------------------------------------- /res/fonts/pixel_unicode_regular_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepnight/gameBase/c5949781eec5a376563d7a7aac678f1669735a86/res/fonts/pixel_unicode_regular_12.png -------------------------------------------------------------------------------- /res/fonts/pixel_unicode_regular_12.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /res/fonts/pixica_mono_regular_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepnight/gameBase/c5949781eec5a376563d7a7aac678f1669735a86/res/fonts/pixica_mono_regular_16.png -------------------------------------------------------------------------------- /res/fonts/pixica_mono_regular_16.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | -------------------------------------------------------------------------------- /res/lang/en.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: \n" 4 | "POT-Creation-Date: \n" 5 | "PO-Revision-Date: 2021-10-01 21:26+0200\n" 6 | "Last-Translator: \n" 7 | "Language-Team: \n" 8 | "Language: en\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 3.0\n" 13 | "X-Poedit-Basepath: .\n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 15 | 16 | #: "src/game/gm/Game.hx" 17 | msgid "Press ESCAPE again to exit." 18 | msgstr "" 19 | -------------------------------------------------------------------------------- /res/lang/sourceTexts.pot: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Content-Type: text/plain; charset=UTF-8\n" 4 | "Content-Transfer-Encoding: 8bit\n" 5 | "MIME-Version: 1.0\n" 6 | 7 | 8 | #: "src/game/Game.hx" 9 | msgid "Press ESCAPE again to exit." 10 | msgstr "" 11 | -------------------------------------------------------------------------------- /res/levels/sampleWorldTiles.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepnight/gameBase/c5949781eec5a376563d7a7aac678f1669735a86/res/levels/sampleWorldTiles.aseprite -------------------------------------------------------------------------------- /run_js.html: -------------------------------------------------------------------------------- 1 | 2 | base2D 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /setup.hxml: -------------------------------------------------------------------------------- 1 | # Misc 2 | --cmd haxelib --always git castle https://github.com/deepnight/castle.git 3 | --cmd haxelib --always git hscript https://github.com/HaxeFoundation/hscript.git 4 | 5 | # Aseprite 6 | --cmd haxelib --always install ase 7 | --cmd haxelib --always git heaps-aseprite https://github.com/deepnight/heaps-aseprite.git 8 | 9 | # Heaps 10 | --cmd haxelib --always git hlsdl https://github.com/HaxeFoundation/hashlink.git master libs/sdl 11 | --cmd haxelib --always git hldx https://github.com/HaxeFoundation/hashlink.git master libs/directx 12 | --cmd haxelib --always git heaps https://github.com/deepnight/heaps.git 13 | 14 | # Deepnight 15 | --cmd haxelib --always install redistHelper 16 | --cmd haxelib --always git ldtk-haxe-api https://github.com/deepnight/ldtk-haxe-api.git 17 | --cmd haxelib --always git deepnightLibs https://github.com/deepnight/deepnightLibs.git 18 | 19 | # Done 20 | --cmd haxelib list 21 | -------------------------------------------------------------------------------- /src/game/App.hx: -------------------------------------------------------------------------------- 1 | /** 2 | "App" class takes care of all the top-level stuff in the whole application. Any other Process, including Game instance, should be a child of App. 3 | **/ 4 | 5 | class App extends dn.Process { 6 | public static var ME : App; 7 | 8 | /** 2D scene **/ 9 | public var scene(default,null) : h2d.Scene; 10 | 11 | /** Used to create "ControllerAccess" instances that will grant controller usage (keyboard or gamepad) **/ 12 | public var controller : Controller; 13 | 14 | /** Controller Access created for Main & Boot **/ 15 | public var ca : ControllerAccess; 16 | 17 | /** If TRUE, game is paused, and a Contrast filter is applied **/ 18 | public var screenshotMode(default,null) = false; 19 | 20 | public function new(s:h2d.Scene) { 21 | super(); 22 | ME = this; 23 | scene = s; 24 | createRoot(scene); 25 | 26 | hxd.Window.getInstance().addEventTarget(onWindowEvent); 27 | 28 | initEngine(); 29 | initAssets(); 30 | initController(); 31 | 32 | // Create console (open with [²] key) 33 | new ui.Console(Assets.fontPixelMono, scene); // init debug console 34 | 35 | // Optional screen that shows a "Click to start/continue" message when the game client looses focus 36 | if( dn.heaps.GameFocusHelper.isUseful() ) 37 | new dn.heaps.GameFocusHelper(scene, Assets.fontPixel); 38 | 39 | #if debug 40 | Console.ME.enableStats(); 41 | #end 42 | 43 | startGame(); 44 | } 45 | 46 | 47 | function onWindowEvent(ev:hxd.Event) { 48 | switch ev.kind { 49 | case EPush: 50 | case ERelease: 51 | case EMove: 52 | case EOver: onMouseEnter(ev); 53 | case EOut: onMouseLeave(ev); 54 | case EWheel: 55 | case EFocus: onWindowFocus(ev); 56 | case EFocusLost: onWindowBlur(ev); 57 | case EKeyDown: 58 | case EKeyUp: 59 | case EReleaseOutside: 60 | case ETextInput: 61 | case ECheck: 62 | } 63 | } 64 | 65 | function onMouseEnter(e:hxd.Event) {} 66 | function onMouseLeave(e:hxd.Event) {} 67 | function onWindowFocus(e:hxd.Event) {} 68 | function onWindowBlur(e:hxd.Event) {} 69 | 70 | 71 | #if hl 72 | public static function onCrash(err:Dynamic) { 73 | var title = L.untranslated("Fatal error"); 74 | var msg = L.untranslated('I\'m really sorry but the game crashed! Error: ${Std.string(err)}'); 75 | var flags : haxe.EnumFlags = new haxe.EnumFlags(); 76 | flags.set(IsError); 77 | 78 | var log = [ Std.string(err) ]; 79 | try { 80 | log.push("BUILD: "+Const.BUILD_INFO); 81 | log.push("EXCEPTION:"); 82 | log.push( haxe.CallStack.toString( haxe.CallStack.exceptionStack() ) ); 83 | 84 | log.push("CALL:"); 85 | log.push( haxe.CallStack.toString( haxe.CallStack.callStack() ) ); 86 | 87 | sys.io.File.saveContent("crash.log", log.join("\n")); 88 | hl.UI.dialog(title, msg, flags); 89 | } 90 | catch(_) { 91 | sys.io.File.saveContent("crash2.log", log.join("\n")); 92 | hl.UI.dialog(title, msg, flags); 93 | } 94 | 95 | hxd.System.exit(); 96 | } 97 | #end 98 | 99 | 100 | /** Start game process **/ 101 | public function startGame() { 102 | if( Game.exists() ) { 103 | // Kill previous game instance first 104 | Game.ME.destroy(); 105 | dn.Process.updateAll(1); // ensure all garbage collection is done 106 | _createGameInstance(); 107 | hxd.Timer.skip(); 108 | } 109 | else { 110 | // Fresh start 111 | delayer.nextFrame( ()->{ 112 | _createGameInstance(); 113 | hxd.Timer.skip(); 114 | }); 115 | } 116 | } 117 | 118 | final function _createGameInstance() { 119 | // new Game(); // <---- Uncomment this to start an empty Game instance 120 | new sample.SampleGame(); // <---- Uncomment this to start the Sample Game instance 121 | } 122 | 123 | 124 | public function anyInputHasFocus() { 125 | return Console.ME.isActive() || cd.has("consoleRecentlyActive") || cd.has("modalClosedRecently"); 126 | } 127 | 128 | 129 | /** 130 | Set "screenshot" mode. 131 | If enabled, the game will be adapted to be more suitable for screenshots: more color contrast, no UI etc. 132 | **/ 133 | public function setScreenshotMode(v:Bool) { 134 | screenshotMode = v; 135 | 136 | Console.ME.runCommand("cls"); 137 | if( screenshotMode ) { 138 | var f = new h2d.filter.ColorMatrix(); 139 | f.matrix.colorContrast(0.2); 140 | root.filter = f; 141 | if( Game.exists() ) { 142 | Game.ME.hud.root.visible = false; 143 | Game.ME.pause(); 144 | } 145 | } 146 | else { 147 | if( Game.exists() ) { 148 | Game.ME.hud.root.visible = true; 149 | Game.ME.resume(); 150 | } 151 | root.filter = null; 152 | } 153 | } 154 | 155 | /** Toggle current game pause state **/ 156 | public inline function toggleGamePause() setGamePause( !isGamePaused() ); 157 | 158 | /** Return TRUE if current game is paused **/ 159 | public inline function isGamePaused() return Game.exists() && Game.ME.isPaused(); 160 | 161 | /** Set current game pause state **/ 162 | public function setGamePause(pauseState:Bool) { 163 | if( Game.exists() ) 164 | if( pauseState ) 165 | Game.ME.pause(); 166 | else 167 | Game.ME.resume(); 168 | } 169 | 170 | 171 | /** 172 | Initialize low-level engine stuff, before anything else 173 | **/ 174 | function initEngine() { 175 | // Engine settings 176 | engine.backgroundColor = 0xff<<24 | 0x111133; 177 | #if( hl && !debug ) 178 | engine.fullScreen = true; 179 | #end 180 | 181 | #if( hl && !debug) 182 | hl.UI.closeConsole(); 183 | hl.Api.setErrorHandler( onCrash ); 184 | #end 185 | 186 | // Heaps resource management 187 | #if( hl && debug ) 188 | hxd.Res.initLocal(); 189 | hxd.res.Resource.LIVE_UPDATE = true; 190 | #else 191 | hxd.Res.initEmbed(); 192 | #end 193 | 194 | // Sound manager (force manager init on startup to avoid a freeze on first sound playback) 195 | hxd.snd.Manager.get(); 196 | hxd.Timer.skip(); // needed to ignore heavy Sound manager init frame 197 | 198 | // Framerate 199 | hxd.Timer.smoothFactor = 0.4; 200 | hxd.Timer.wantedFPS = Const.FPS; 201 | dn.Process.FIXED_UPDATE_FPS = Const.FIXED_UPDATE_FPS; 202 | } 203 | 204 | 205 | /** 206 | Init app assets 207 | **/ 208 | function initAssets() { 209 | // Init game assets 210 | Assets.init(); 211 | 212 | // Init lang data 213 | Lang.init("en"); 214 | 215 | // Bind DB hot-reloading callback 216 | Const.db.onReload = onDbReload; 217 | } 218 | 219 | 220 | /** Init game controller and default key bindings **/ 221 | function initController() { 222 | controller = dn.heaps.input.Controller.createFromAbstractEnum(GameAction); 223 | ca = controller.createAccess(); 224 | ca.lockCondition = ()->return destroyed || anyInputHasFocus(); 225 | 226 | initControllerBindings(); 227 | } 228 | 229 | public function initControllerBindings() { 230 | controller.removeBindings(); 231 | 232 | // Gamepad bindings 233 | controller.bindPadLStick4(MoveLeft, MoveRight, MoveUp, MoveDown); 234 | controller.bindPad(Jump, A); 235 | controller.bindPad(Restart, SELECT); 236 | controller.bindPad(Pause, START); 237 | controller.bindPad(MoveLeft, DPAD_LEFT); 238 | controller.bindPad(MoveRight, DPAD_RIGHT); 239 | controller.bindPad(MoveUp, DPAD_UP); 240 | controller.bindPad(MoveDown, DPAD_DOWN); 241 | 242 | controller.bindPad(MenuUp, [DPAD_UP, LSTICK_UP]); 243 | controller.bindPad(MenuDown, [DPAD_DOWN, LSTICK_DOWN]); 244 | controller.bindPad(MenuLeft, [DPAD_LEFT, LSTICK_LEFT]); 245 | controller.bindPad(MenuRight, [DPAD_RIGHT, LSTICK_RIGHT]); 246 | controller.bindPad(MenuOk, [A, X]); 247 | controller.bindPad(MenuCancel, B); 248 | 249 | // Keyboard bindings 250 | controller.bindKeyboard(MoveLeft, [K.LEFT, K.Q, K.A]); 251 | controller.bindKeyboard(MoveRight, [K.RIGHT, K.D]); 252 | controller.bindKeyboard(MoveUp, [K.UP, K.Z, K.W]); 253 | controller.bindKeyboard(MoveDown, [K.DOWN, K.S]); 254 | controller.bindKeyboard(Jump, [K.SPACE,K.UP]); 255 | controller.bindKeyboard(Restart, K.R); 256 | controller.bindKeyboard(ScreenshotMode, K.F9); 257 | controller.bindKeyboard(Pause, K.P); 258 | controller.bindKeyboard(Pause, K.PAUSE_BREAK); 259 | 260 | controller.bindKeyboard(MenuUp, [K.UP, K.Z, K.W]); 261 | controller.bindKeyboard(MenuDown, [K.DOWN, K.S]); 262 | controller.bindKeyboard(MenuLeft, [K.LEFT, K.Q, K.A]); 263 | controller.bindKeyboard(MenuRight, [K.RIGHT, K.D]); 264 | controller.bindKeyboard(MenuOk, [K.SPACE, K.ENTER, K.F]); 265 | controller.bindKeyboard(MenuCancel, K.ESCAPE); 266 | 267 | // Debug controls 268 | #if debug 269 | controller.bindPad(DebugTurbo, LT); 270 | controller.bindPad(DebugSlowMo, LB); 271 | controller.bindPad(DebugDroneZoomIn, RSTICK_UP); 272 | controller.bindPad(DebugDroneZoomOut, RSTICK_DOWN); 273 | 274 | controller.bindKeyboard(DebugDroneZoomIn, K.PGUP); 275 | controller.bindKeyboard(DebugDroneZoomOut, K.PGDOWN); 276 | controller.bindKeyboard(DebugTurbo, [K.END, K.NUMPAD_ADD]); 277 | controller.bindKeyboard(DebugSlowMo, [K.HOME, K.NUMPAD_SUB]); 278 | controller.bindPadCombo(ToggleDebugDrone, [LSTICK_PUSH, RSTICK_PUSH]); 279 | controller.bindKeyboardCombo(ToggleDebugDrone, [K.CTRL,K.SHIFT, K.D]); 280 | controller.bindKeyboardCombo(OpenConsoleFlags, [[K.QWERTY_TILDE], [K.QWERTY_QUOTE], ["²".code], [K.CTRL,K.SHIFT, K.F]]); 281 | #end 282 | } 283 | 284 | 285 | /** Return TRUE if an App instance exists **/ 286 | public static inline function exists() return ME!=null && !ME.destroyed; 287 | 288 | /** Close & exit the app **/ 289 | public function exit() { 290 | destroy(); 291 | } 292 | 293 | override function onDispose() { 294 | super.onDispose(); 295 | 296 | hxd.Window.getInstance().removeEventTarget( onWindowEvent ); 297 | 298 | #if hl 299 | hxd.System.exit(); 300 | #end 301 | } 302 | 303 | /** Called when Const.db values are hot-reloaded **/ 304 | public function onDbReload() { 305 | if( Game.exists() ) 306 | Game.ME.onDbReload(); 307 | } 308 | 309 | override function update() { 310 | Assets.update(tmod); 311 | 312 | super.update(); 313 | 314 | if( !Window.hasAnyModal() ) { 315 | if( ca.isPressed(ScreenshotMode) ) 316 | setScreenshotMode( !screenshotMode ); 317 | 318 | if( ca.isPressed(Pause) ) 319 | toggleGamePause(); 320 | 321 | if( ca.isPressed(OpenConsoleFlags) ) 322 | Console.ME.runCommand("/flags"); 323 | } 324 | 325 | if( ui.Console.ME.isActive() ) 326 | cd.setF("consoleRecentlyActive",2); 327 | 328 | 329 | // Mem track reporting 330 | #if debug 331 | if( ca.isKeyboardDown(K.SHIFT) && ca.isKeyboardPressed(K.ENTER) ) { 332 | Console.ME.runCommand("/cls"); 333 | dn.debug.MemTrack.report( (v)->Console.ME.log(v,Yellow) ); 334 | } 335 | #end 336 | 337 | } 338 | } -------------------------------------------------------------------------------- /src/game/Boot.hx: -------------------------------------------------------------------------------- 1 | /** 2 | Boot class is the entry point for the app. 3 | It doesn't do much, except creating Main class and taking care of loops. Thus, you shouldn't be doing too much in this class. 4 | **/ 5 | 6 | class Boot extends hxd.App { 7 | #if debug 8 | // Debug controls over game speed 9 | var tmodSpeedMul = 1.0; 10 | 11 | // Shortcut to controller 12 | var ca(get,never) : ControllerAccess; 13 | inline function get_ca() return App.ME.ca; 14 | #end 15 | 16 | 17 | /** 18 | App entry point: everything starts here 19 | **/ 20 | static function main() { 21 | new Boot(); 22 | } 23 | 24 | /** 25 | Called when engine is ready, actual app can start 26 | **/ 27 | override function init() { 28 | new App(s2d); 29 | onResize(); 30 | } 31 | 32 | // Window resized 33 | override function onResize() { 34 | super.onResize(); 35 | dn.Process.resizeAll(); 36 | } 37 | 38 | 39 | /** Main app loop **/ 40 | override function update(deltaTime:Float) { 41 | super.update(deltaTime); 42 | 43 | // Debug controls over app speed 44 | var adjustedTmod = hxd.Timer.tmod; 45 | #if debug 46 | if( App.exists() ) { 47 | // Slow down (toggle) 48 | if( ca.isPressed(DebugSlowMo) ) 49 | tmodSpeedMul = tmodSpeedMul>=1 ? 0.2 : 1; 50 | adjustedTmod *= tmodSpeedMul; 51 | 52 | // Turbo (by holding a key) 53 | adjustedTmod *= ca.isDown(DebugTurbo) ? 5 : 1; 54 | } 55 | #end 56 | 57 | #if( hl && !debug ) 58 | try { 59 | #end 60 | 61 | // Run all dn.Process instances loops 62 | dn.Process.updateAll(adjustedTmod); 63 | 64 | // Update current sprite atlas "tmod" value (for animations) 65 | Assets.update(adjustedTmod); 66 | 67 | #if( hl && !debug ) 68 | } catch(err) { 69 | App.onCrash(err); 70 | } 71 | #end 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /src/game/Camera.hx: -------------------------------------------------------------------------------- 1 | class Camera extends GameChildProcess { 2 | public static var MIN_ZOOM : Float = 1.0; 3 | public static var MAX_ZOOM : Float = 10; 4 | 5 | 6 | /** Camera focus coord in level pixels. This is the raw camera location: the actual camera location might be clamped to level bounds. **/ 7 | public var rawFocus : LPoint; 8 | 9 | /** This is equal to rawFocus if `clampToLevelBounds` is disabled **/ 10 | var clampedFocus : LPoint; 11 | 12 | var target : Null; 13 | public var targetOffX = 0.; 14 | public var targetOffY = 0.; 15 | 16 | /** Width of viewport in level pixels **/ 17 | public var pxWid(get,never) : Int; 18 | 19 | /** Height of viewport in level pixels **/ 20 | public var pxHei(get,never) : Int; 21 | 22 | public var cWid(get,never) : Int; inline function get_cWid() return M.ceil(pxWid/Const.GRID); 23 | public var cHei(get,never) : Int; inline function get_cHei() return M.ceil(pxHei/Const.GRID); 24 | 25 | /** Horizontal camera dead-zone in percentage of viewport width **/ 26 | public var deadZonePctX = 0.04; 27 | 28 | /** Verticakl camera dead-zone in percentage of viewport height **/ 29 | public var deadZonePctY = 0.10; 30 | 31 | var baseFrict = 0.89; 32 | var dx = 0.; 33 | var dy = 0.; 34 | var dz = 0.; 35 | var bumpOffX = 0.; 36 | var bumpOffY = 0.; 37 | var bumpFrict = 0.85; 38 | var bumpZoomFactor = 0.; 39 | 40 | /** Actual zoom value without modifiers **/ 41 | var baseZoom = 1.0; 42 | var zoomSpeed = 0.0014; 43 | var zoomFrict = 0.9; 44 | 45 | /** Current zoom factor, including all modifiers **/ 46 | public var zoom(get,never) : Float; 47 | 48 | /** Target base zoom value **/ 49 | public var targetZoom(default,set) = 1.0; 50 | 51 | /** Speed multiplier when camera is tracking a target **/ 52 | var trackingSpeed = 1.0; 53 | 54 | /** If TRUE (default), the camera will try to stay inside level bounds. It cannot be done if level is smaller than actual viewport. In such case, the camera will be centered. **/ 55 | public var clampToLevelBounds = false; 56 | var brakeDistNearBounds = 0.1; 57 | 58 | 59 | /** Camera bound coords in level pixels **/ 60 | public var pxLeft(get,never) : Int; inline function get_pxLeft() return Std.int( clampedFocus.levelX - pxWid*0.5 ); 61 | public var pxRight(get,never) : Int; inline function get_pxRight() return Std.int( pxLeft + (pxWid - 1) ); 62 | public var pxTop(get,never) : Int; inline function get_pxTop() return Std.int( clampedFocus.levelY-pxHei*0.5 ); 63 | public var pxBottom(get,never) : Int; inline function get_pxBottom() return pxTop + pxHei - 1; 64 | 65 | /** Center X in pixels **/ 66 | public var centerX(get,never) : Int; inline function get_centerX() return Std.int( (pxLeft+pxRight) * 0.5 ); 67 | 68 | /** Center Y in pixels **/ 69 | public var centerY(get,never) : Int; inline function get_centerY() return Std.int( (pxTop+pxBottom) * 0.5 ); 70 | 71 | /** Camera bound coords in grid cells **/ 72 | public var cLeft(get,never) : Int; inline function get_cLeft() return Std.int( pxLeft/Const.GRID ); 73 | public var cRight(get,never) : Int; inline function get_cRight() return M.ceil( pxRight/Const.GRID ); 74 | public var cTop(get,never) : Int; inline function get_cTop() return Std.int( pxTop/Const.GRID ); 75 | public var cBottom(get,never) : Int; inline function get_cBottom() return M.ceil( pxBottom/Const.GRID ); 76 | 77 | // Debugging 78 | var invalidateDebugBounds = false; 79 | var debugBounds : Null; 80 | 81 | 82 | public function new() { 83 | super(); 84 | rawFocus = LPoint.fromCase(0,0); 85 | clampedFocus = LPoint.fromCase(0,0); 86 | dx = dy = 0; 87 | } 88 | 89 | @:keep 90 | override function toString() { 91 | return 'Camera@${Std.int(rawFocus.levelX)},${Std.int(rawFocus.levelY)}'; 92 | } 93 | 94 | inline function get_zoom() { 95 | return baseZoom + bumpZoomFactor; 96 | } 97 | 98 | 99 | inline function set_targetZoom(v) { 100 | return targetZoom = M.fclamp(v, MIN_ZOOM, MAX_ZOOM); 101 | } 102 | 103 | /** Smoothly change zoom within MIN/MAX bounds **/ 104 | public inline function zoomTo(v:Float) { 105 | targetZoom = v; 106 | } 107 | 108 | /** Force zoom immediately to given value **/ 109 | public function forceZoom(v) { 110 | baseZoom = targetZoom = M.fclamp(v, MIN_ZOOM, MAX_ZOOM); 111 | dz = 0; 112 | } 113 | 114 | public inline function bumpZoom(z:Float) { 115 | bumpZoomFactor = z; 116 | } 117 | 118 | function get_pxWid() { 119 | return M.ceil( Game.ME.stageWid / Const.SCALE / zoom ); 120 | } 121 | 122 | function get_pxHei() { 123 | return M.ceil( Game.ME.stageHei / Const.SCALE / zoom ); 124 | } 125 | 126 | 127 | /** 128 | Return TRUE if given coords are in current camera bounds. Padding is *added* to the screen bounds (it can be negative to *shrink* these bounds). 129 | **/ 130 | public inline function isOnScreen(levelX:Float, levelY: Float, padding=0.) { 131 | return levelX>=pxLeft-padding && levelX<=pxRight+padding && levelY>=pxTop-padding && levelY<=pxBottom+padding; 132 | } 133 | 134 | /** 135 | Return TRUE if given rectangle is partially inside current camera bounds. Padding is *added* to the screen bounds (it can be negative to *shrink* these bounds). 136 | **/ 137 | public inline function isOnScreenRect(x:Float, y:Float, wid:Float, hei:Float, padding=0.) { 138 | return Lib.rectangleOverlaps( 139 | pxLeft-padding, pxTop-padding, pxWid+padding*2, pxHei+padding*2, 140 | x, y, wid, hei 141 | ); 142 | } 143 | 144 | /** 145 | Return TRUE if given grid coords are in current camera bounds. Padding is *added* to the screen bounds (it can be negative to *shrink* these bounds). 146 | **/ 147 | public inline function isOnScreenCase(cx:Int, cy:Int, padding=32) { 148 | return cx*Const.GRID>=pxLeft-padding && (cx+1)*Const.GRID<=pxRight+padding 149 | && cy*Const.GRID>=pxTop-padding && (cy+1)*Const.GRID<=pxBottom+padding; 150 | } 151 | 152 | 153 | /** 154 | Enable auto tracking on given Entity. If `immediate` is true, the camera is immediately positioned over the Entity, otherwise it just moves to it. 155 | **/ 156 | public function trackEntity(e:Entity, immediate:Bool, speed=1.0) { 157 | target = e; 158 | setTrackingSpeed(speed); 159 | if( immediate || rawFocus.levelX==0 && rawFocus.levelY==0 ) 160 | centerOnTarget(); 161 | } 162 | 163 | public inline function setTrackingSpeed(spd:Float) { 164 | trackingSpeed = M.fclamp(spd, 0.01, 10); 165 | } 166 | 167 | public inline function stopTracking() { 168 | target = null; 169 | } 170 | 171 | public function centerOnTarget() { 172 | if( target!=null ) { 173 | rawFocus.levelX = target.centerX + targetOffX; 174 | rawFocus.levelY = target.centerY + targetOffY; 175 | } 176 | } 177 | 178 | public inline function levelToGlobalX(v:Float) return v*Const.SCALE + Game.ME.scroller.x; 179 | public inline function levelToGlobalY(v:Float) return v*Const.SCALE + Game.ME.scroller.y; 180 | 181 | var shakePower = 1.0; 182 | public function shakeS(t:Float, pow=1.0) { 183 | cd.setS("shaking", t, false); 184 | shakePower = pow; 185 | } 186 | 187 | public inline function bumpAng(a, dist) { 188 | bumpOffX+=Math.cos(a)*dist; 189 | bumpOffY+=Math.sin(a)*dist; 190 | } 191 | 192 | public inline function bump(x,y) { 193 | bumpOffX+=x; 194 | bumpOffY+=y; 195 | } 196 | 197 | 198 | /** Apply camera values to Game scroller **/ 199 | function apply() { 200 | if( ui.Console.ME.hasFlag(F_CameraScrolling) ) 201 | return; 202 | 203 | var level = Game.ME.level; 204 | var scroller = Game.ME.scroller; 205 | 206 | // Update scroller 207 | scroller.x = -clampedFocus.levelX + pxWid*0.5; 208 | scroller.y = -clampedFocus.levelY + pxHei*0.5; 209 | 210 | // Bumps friction 211 | bumpOffX *= Math.pow(bumpFrict, tmod); 212 | bumpOffY *= Math.pow(bumpFrict, tmod); 213 | 214 | // Bump 215 | scroller.x -= bumpOffX; 216 | scroller.y -= bumpOffY; 217 | 218 | // Shakes 219 | if( cd.has("shaking") ) { 220 | scroller.x += Math.cos(ftime*1.1)*2.5*shakePower * cd.getRatio("shaking"); 221 | scroller.y += Math.sin(0.3+ftime*1.7)*2.5*shakePower * cd.getRatio("shaking"); 222 | } 223 | 224 | // Scaling 225 | scroller.x*=Const.SCALE*zoom; 226 | scroller.y*=Const.SCALE*zoom; 227 | 228 | // Rounding 229 | scroller.x = M.round(scroller.x); 230 | scroller.y = M.round(scroller.y); 231 | 232 | // Zoom 233 | scroller.setScale(Const.SCALE * zoom); 234 | } 235 | 236 | 237 | /** Hide camera debug bounds **/ 238 | public function disableDebugBounds() { 239 | if( debugBounds!=null ) { 240 | debugBounds.remove(); 241 | debugBounds = null; 242 | } 243 | } 244 | 245 | /** Show camera debug bounds **/ 246 | public function enableDebugBounds() { 247 | disableDebugBounds(); 248 | debugBounds = new h2d.Graphics(); 249 | Game.ME.scroller.add(debugBounds, Const.DP_TOP); 250 | invalidateDebugBounds = true; 251 | } 252 | 253 | function renderDebugBounds() { 254 | debugBounds.clear(); 255 | 256 | debugBounds.lineStyle(2,0xff00ff); 257 | debugBounds.drawRect(0,0,pxWid,pxHei); 258 | 259 | debugBounds.moveTo(pxWid*0.5, 0); 260 | debugBounds.lineTo(pxWid*0.5, pxHei); 261 | 262 | debugBounds.moveTo(0, pxHei*0.5); 263 | debugBounds.lineTo(pxWid, pxHei*0.5); 264 | } 265 | 266 | 267 | override function onResize() { 268 | super.onResize(); 269 | invalidateDebugBounds = true; 270 | } 271 | 272 | 273 | override function postUpdate() { 274 | super.postUpdate(); 275 | 276 | apply(); 277 | 278 | // Debug bounds 279 | if( ui.Console.ME.hasFlag(F_Camera) && debugBounds==null ) 280 | enableDebugBounds(); 281 | else if( !ui.Console.ME.hasFlag(F_Camera) && debugBounds!=null ) 282 | disableDebugBounds(); 283 | 284 | if( debugBounds!=null ) { 285 | if( invalidateDebugBounds ) { 286 | renderDebugBounds(); 287 | invalidateDebugBounds = false; 288 | } 289 | debugBounds.setPosition(pxLeft,pxTop); 290 | } 291 | } 292 | 293 | 294 | override function update() { 295 | super.update(); 296 | 297 | final level = Game.ME.level; 298 | 299 | 300 | // Zoom movement 301 | var tz = targetZoom; 302 | 303 | if( tz!=baseZoom ) { 304 | if( tz>baseZoom) 305 | dz+=zoomSpeed; 306 | else 307 | dz-=zoomSpeed; 308 | } 309 | else 310 | dz = 0; 311 | 312 | var prevZoom = baseZoom; 313 | baseZoom+=dz*tmod; 314 | 315 | bumpZoomFactor *= Math.pow(0.9, tmod); 316 | dz*=Math.pow(zoomFrict, tmod); 317 | if( M.fabs(tz-baseZoom)<=0.05*tmod ) 318 | dz*=Math.pow(0.8,tmod); 319 | 320 | // Reached target zoom 321 | if( prevZoom=tz || prevZoom>tz && baseZoom<=tz ) { 322 | baseZoom = tz; 323 | dz = 0; 324 | } 325 | 326 | 327 | // Follow target entity 328 | if( target!=null ) { 329 | var spdX = 0.015*trackingSpeed*zoom; 330 | var spdY = 0.023*trackingSpeed*zoom; 331 | var tx = target.centerX + targetOffX; 332 | var ty = target.centerY + targetOffY; 333 | 334 | var a = rawFocus.angTo(tx,ty); 335 | var distX = M.fabs( tx - rawFocus.levelX ); 336 | if( distX>=deadZonePctX*pxWid ) 337 | dx += Math.cos(a) * (0.8*distX-deadZonePctX*pxWid) * spdX * tmod; 338 | 339 | var distY = M.fabs( ty - rawFocus.levelY ); 340 | if( distY>=deadZonePctY*pxHei) 341 | dy += Math.sin(a) * (0.8*distY-deadZonePctY*pxHei) * spdY * tmod; 342 | } 343 | 344 | // Compute frictions 345 | var frictX = baseFrict - trackingSpeed*zoom*0.027*baseFrict; 346 | var frictY = frictX; 347 | if( clampToLevelBounds ) { 348 | // "Brake" when approaching bounds 349 | final brakeDist = brakeDistNearBounds * pxWid; 350 | if( dx<=0 ) { 351 | final brakeRatio = 1-M.fclamp( ( rawFocus.levelX - pxWid*0.5 ) / brakeDist, 0, 1 ); 352 | frictX *= 1 - 1*brakeRatio; 353 | } 354 | else if( dx>0 ) { 355 | final brakeRatio = 1-M.fclamp( ( (level.pxWid-pxWid*0.5) - rawFocus.levelX ) / brakeDist, 0, 1 ); 356 | frictX *= 1 - 0.9*brakeRatio; 357 | } 358 | 359 | final brakeDist = brakeDistNearBounds * pxHei; 360 | if( dy<0 ) { 361 | final brakeRatio = 1-M.fclamp( ( rawFocus.levelY - pxHei*0.5 ) / brakeDist, 0, 1 ); 362 | frictY *= 1 - 0.9*brakeRatio; 363 | } 364 | else if( dy>0 ) { 365 | final brakeRatio = 1-M.fclamp( ( (level.pxHei-pxHei*0.5) - rawFocus.levelY ) / brakeDist, 0, 1 ); 366 | frictY *= 1 - 0.9*brakeRatio; 367 | } 368 | } 369 | 370 | // Apply velocities 371 | rawFocus.levelX += dx*tmod; 372 | dx *= Math.pow(frictX,tmod); 373 | rawFocus.levelY += dy*tmod; 374 | dy *= Math.pow(frictY,tmod); 375 | 376 | 377 | // Bounds clamping 378 | if( clampToLevelBounds ) { 379 | // X 380 | if( level.pxWid < pxWid) 381 | clampedFocus.levelX = level.pxWid*0.5; // centered small level 382 | else 383 | clampedFocus.levelX = M.fclamp( rawFocus.levelX, pxWid*0.5, level.pxWid-pxWid*0.5 ); 384 | 385 | // Y 386 | if( level.pxHei < pxHei) 387 | clampedFocus.levelY = level.pxHei*0.5; // centered small level 388 | else 389 | clampedFocus.levelY = M.fclamp( rawFocus.levelY, pxHei*0.5, level.pxHei-pxHei*0.5 ); 390 | } 391 | else { 392 | // No clamping 393 | clampedFocus.levelX = rawFocus.levelX; 394 | clampedFocus.levelY = rawFocus.levelY; 395 | } 396 | } 397 | 398 | } -------------------------------------------------------------------------------- /src/game/Const.hx: -------------------------------------------------------------------------------- 1 | /** 2 | The Const class is a place for you to store various values that should be available everywhere in your code. Example: `Const.FPS` 3 | **/ 4 | class Const { 5 | #if !macro 6 | 7 | /** Default engine framerate (60) **/ 8 | public static var FPS(get,never) : Int; 9 | static inline function get_FPS() return Std.int( hxd.System.getDefaultFrameRate() ); 10 | 11 | /** 12 | "Fixed" updates framerate. 30fps is a good value here, as it's almost guaranteed to work on any decent setup, and it's more than enough to run any gameplay related physics. 13 | **/ 14 | public static final FIXED_UPDATE_FPS = 30; 15 | 16 | /** Grid size in pixels **/ 17 | public static final GRID = 16; 18 | 19 | /** "Infinite", sort-of. More like a "big number" **/ 20 | public static final INFINITE : Int = 0xfffFfff; 21 | 22 | static var _nextUniqueId = 0; 23 | /** Unique value generator **/ 24 | public static inline function makeUniqueId() { 25 | return _nextUniqueId++; 26 | } 27 | 28 | /** Viewport scaling **/ 29 | public static var SCALE(get,never) : Int; 30 | static inline function get_SCALE() { 31 | // can be replaced with another way to determine the game scaling 32 | return dn.heaps.Scaler.bestFit_i(200,200); 33 | } 34 | 35 | /** Specific scaling for top UI elements **/ 36 | public static var UI_SCALE(get,never) : Float; 37 | static inline function get_UI_SCALE() { 38 | // can be replaced with another way to determine the UI scaling 39 | return dn.heaps.Scaler.bestFit_i(400,400); 40 | } 41 | 42 | 43 | /** Current build information, including date, time, language & various other things **/ 44 | public static var BUILD_INFO(get,never) : String; 45 | static function get_BUILD_INFO() return dn.MacroTools.getBuildInfo(); 46 | 47 | 48 | /** Game layers indexes **/ 49 | static var _inc = 0; 50 | public static var DP_BG = _inc++; 51 | public static var DP_FX_BG = _inc++; 52 | public static var DP_MAIN = _inc++; 53 | public static var DP_FRONT = _inc++; 54 | public static var DP_FX_FRONT = _inc++; 55 | public static var DP_TOP = _inc++; 56 | public static var DP_UI = _inc++; 57 | 58 | 59 | /** 60 | Simplified "constants database" using CastleDB and JSON files 61 | It will be filled with all values found in both following sources: 62 | 63 | - `res/const.json`, a basic JSON file, 64 | - `res/data.cdb`, the CastleDB file, from the sheet named "ConstDb". 65 | 66 | This allows super easy access to your game constants and settings. Example: 67 | 68 | Having `res/const.json`: 69 | { "myValue":5, "someText":"hello" } 70 | 71 | You may use: 72 | Const.db.myValue; // equals to 5 73 | Const.db.someText; // equals to "hello" 74 | 75 | If the JSON changes on runtime, the `myValue` field is kept up-to-date, allowing testing without recompiling. IMPORTANT: this hot-reloading only works if the project was built using the `-debug` flag. In release builds, all values become constants and are fully embedded. 76 | **/ 77 | public static var db = ConstDbBuilder.buildVar(["data.cdb", "const.json"]); 78 | 79 | #end 80 | } 81 | -------------------------------------------------------------------------------- /src/game/Entity.hx: -------------------------------------------------------------------------------- 1 | class Entity { 2 | public static var ALL : FixedArray = new FixedArray(1024); 3 | public static var GC : FixedArray = new FixedArray(ALL.maxSize); 4 | 5 | // Various getters to access all important stuff easily 6 | public var app(get,never) : App; inline function get_app() return App.ME; 7 | public var game(get,never) : Game; inline function get_game() return Game.ME; 8 | public var fx(get,never) : Fx; inline function get_fx() return Game.ME.fx; 9 | public var level(get,never) : Level; inline function get_level() return Game.ME.level; 10 | public var destroyed(default,null) = false; 11 | public var ftime(get,never) : Float; inline function get_ftime() return game.ftime; 12 | public var camera(get,never) : Camera; inline function get_camera() return game.camera; 13 | 14 | var tmod(get,never) : Float; inline function get_tmod() return Game.ME.tmod; 15 | var utmod(get,never) : Float; inline function get_utmod() return Game.ME.utmod; 16 | public var hud(get,never) : ui.Hud; inline function get_hud() return Game.ME.hud; 17 | 18 | /** Cooldowns **/ 19 | public var cd : dn.Cooldown; 20 | 21 | /** Cooldowns, unaffected by slowmo (ie. always in realtime) **/ 22 | public var ucd : dn.Cooldown; 23 | 24 | /** Temporary gameplay affects **/ 25 | var affects : Map = new Map(); 26 | 27 | /** State machine. Value should only be changed using `startState(v)` **/ 28 | public var state(default,null) : State; 29 | 30 | /** Unique identifier **/ 31 | public var uid(default,null) : Int; 32 | 33 | /** Grid X coordinate **/ 34 | public var cx = 0; 35 | /** Grid Y coordinate **/ 36 | public var cy = 0; 37 | /** Sub-grid X coordinate (from 0.0 to 1.0) **/ 38 | public var xr = 0.5; 39 | /** Sub-grid Y coordinate (from 0.0 to 1.0) **/ 40 | public var yr = 1.0; 41 | 42 | var allVelocities : VelocityArray; 43 | 44 | /** Base X/Y velocity of the Entity **/ 45 | public var vBase : Velocity; 46 | /** "External bump" velocity. It is used to push the Entity in some direction, independently of the "user-controlled" base velocity. **/ 47 | public var vBump : Velocity; 48 | 49 | /** Last known X position of the attach point (in pixels), at the beginning of the latest fixedUpdate **/ 50 | var lastFixedUpdateX = 0.; 51 | /** Last known Y position of the attach point (in pixels), at the beginning of the latest fixedUpdate **/ 52 | var lastFixedUpdateY = 0.; 53 | 54 | /** If TRUE, the sprite display coordinates will be an interpolation between the last known position and the current one. This is useful if the gameplay happens in the `fixedUpdate()` (so at 30 FPS), but you still want the sprite position to move smoothly at 60 FPS or more. **/ 55 | var interpolateSprPos = true; 56 | 57 | /** Total of all X velocities **/ 58 | public var dxTotal(get,never) : Float; inline function get_dxTotal() return allVelocities.getSumX(); 59 | /** Total of all Y velocities **/ 60 | public var dyTotal(get,never) : Float; inline function get_dyTotal() return allVelocities.getSumY(); 61 | 62 | /** Pixel width of entity **/ 63 | public var wid(default,set) : Float = Const.GRID; 64 | inline function set_wid(v) { invalidateDebugBounds=true; return wid=v; } 65 | public var iwid(get,set) : Int; 66 | inline function get_iwid() return M.round(wid); 67 | inline function set_iwid(v:Int) { invalidateDebugBounds=true; wid=v; return iwid; } 68 | 69 | /** Pixel height of entity **/ 70 | public var hei(default,set) : Float = Const.GRID; 71 | inline function set_hei(v) { invalidateDebugBounds=true; return hei=v; } 72 | public var ihei(get,set) : Int; 73 | inline function get_ihei() return M.round(hei); 74 | inline function set_ihei(v:Int) { invalidateDebugBounds=true; hei=v; return ihei; } 75 | 76 | /** Inner radius in pixels (ie. smallest value between width/height, then divided by 2) **/ 77 | public var innerRadius(get,never) : Float; 78 | inline function get_innerRadius() return M.fmin(wid,hei)*0.5; 79 | 80 | /** "Large" radius in pixels (ie. biggest value between width/height, then divided by 2) **/ 81 | public var largeRadius(get,never) : Float; 82 | inline function get_largeRadius() return M.fmax(wid,hei)*0.5; 83 | 84 | /** Horizontal direction, can only be -1 or 1 **/ 85 | public var dir(default,set) = 1; 86 | 87 | /** Current sprite X **/ 88 | public var sprX(get,never) : Float; 89 | inline function get_sprX() { 90 | return interpolateSprPos 91 | ? M.lerp( lastFixedUpdateX, (cx+xr)*Const.GRID, game.getFixedUpdateAccuRatio() ) 92 | : (cx+xr)*Const.GRID; 93 | } 94 | 95 | /** Current sprite Y **/ 96 | public var sprY(get,never) : Float; 97 | inline function get_sprY() { 98 | return interpolateSprPos 99 | ? M.lerp( lastFixedUpdateY, (cy+yr)*Const.GRID, game.getFixedUpdateAccuRatio() ) 100 | : (cy+yr)*Const.GRID; 101 | } 102 | 103 | /** Sprite X scaling **/ 104 | public var sprScaleX = 1.0; 105 | /** Sprite Y scaling **/ 106 | public var sprScaleY = 1.0; 107 | 108 | /** Sprite X squash & stretch scaling, which automatically comes back to 1 after a few frames **/ 109 | var sprSquashX = 1.0; 110 | /** Sprite Y squash & stretch scaling, which automatically comes back to 1 after a few frames **/ 111 | var sprSquashY = 1.0; 112 | 113 | /** Entity visibility **/ 114 | public var entityVisible = true; 115 | 116 | /** Current hit points **/ 117 | public var life(default,null) : dn.struct.Stat; 118 | /** Last source of damage if it was an Entity **/ 119 | public var lastDmgSource(default,null) : Null; 120 | 121 | /** Horizontal direction (left=-1 or right=1): from "last source of damage" to "this" **/ 122 | public var lastHitDirFromSource(get,never) : Int; 123 | inline function get_lastHitDirFromSource() return lastDmgSource==null ? -dir : -dirTo(lastDmgSource); 124 | 125 | /** Horizontal direction (left=-1 or right=1): from "this" to "last source of damage" **/ 126 | public var lastHitDirToSource(get,never) : Int; 127 | inline function get_lastHitDirToSource() return lastDmgSource==null ? dir : dirTo(lastDmgSource); 128 | 129 | /** Main entity HSprite instance **/ 130 | public var spr : HSprite; 131 | 132 | /** Color vector transformation applied to sprite **/ 133 | public var baseColor : h3d.Vector; 134 | 135 | /** Color matrix transformation applied to sprite **/ 136 | public var colorMatrix : h3d.Matrix; 137 | 138 | // Animated blink color on damage hit 139 | var blinkColor : h3d.Vector; 140 | 141 | /** Sprite X shake power **/ 142 | var shakePowX = 0.; 143 | /** Sprite Y shake power **/ 144 | var shakePowY = 0.; 145 | 146 | // Debug stuff 147 | var debugLabel : Null; 148 | var debugBounds : Null; 149 | var invalidateDebugBounds = false; 150 | 151 | /** Defines X alignment of entity at its attach point (0 to 1.0) **/ 152 | public var pivotX(default,set) : Float = 0.5; 153 | /** Defines Y alignment of entity at its attach point (0 to 1.0) **/ 154 | public var pivotY(default,set) : Float = 1; 155 | 156 | /** Entity attach X pixel coordinate **/ 157 | public var attachX(get,never) : Float; inline function get_attachX() return (cx+xr)*Const.GRID; 158 | /** Entity attach Y pixel coordinate **/ 159 | public var attachY(get,never) : Float; inline function get_attachY() return (cy+yr)*Const.GRID; 160 | 161 | // Various coordinates getters, for easier gameplay coding 162 | 163 | /** Left pixel coordinate of the bounding box **/ 164 | public var left(get,never) : Float; inline function get_left() return attachX + (0-pivotX) * wid; 165 | /** Right pixel coordinate of the bounding box **/ 166 | public var right(get,never) : Float; inline function get_right() return attachX + (1-pivotX) * wid; 167 | /** Top pixel coordinate of the bounding box **/ 168 | public var top(get,never) : Float; inline function get_top() return attachY + (0-pivotY) * hei; 169 | /** Bottom pixel coordinate of the bounding box **/ 170 | public var bottom(get,never) : Float; inline function get_bottom() return attachY + (1-pivotY) * hei; 171 | 172 | /** Center X pixel coordinate of the bounding box **/ 173 | public var centerX(get,never) : Float; inline function get_centerX() return attachX + (0.5-pivotX) * wid; 174 | /** Center Y pixel coordinate of the bounding box **/ 175 | public var centerY(get,never) : Float; inline function get_centerY() return attachY + (0.5-pivotY) * hei; 176 | 177 | /** Current X position on screen (ie. absolute)**/ 178 | public var screenAttachX(get,never) : Float; 179 | inline function get_screenAttachX() return game!=null && !game.destroyed ? sprX*Const.SCALE + game.scroller.x : sprX*Const.SCALE; 180 | 181 | /** Current Y position on screen (ie. absolute)**/ 182 | public var screenAttachY(get,never) : Float; 183 | inline function get_screenAttachY() return game!=null && !game.destroyed ? sprY*Const.SCALE + game.scroller.y : sprY*Const.SCALE; 184 | 185 | /** attachX value during last frame **/ 186 | public var prevFrameAttachX(default,null) : Float = -Const.INFINITE; 187 | /** attachY value during last frame **/ 188 | public var prevFrameAttachY(default,null) : Float = -Const.INFINITE; 189 | 190 | var actions : RecyclablePool; 191 | 192 | 193 | /** 194 | Constructor 195 | **/ 196 | public function new(x:Int, y:Int) { 197 | uid = Const.makeUniqueId(); 198 | ALL.push(this); 199 | 200 | cd = new dn.Cooldown(Const.FPS); 201 | ucd = new dn.Cooldown(Const.FPS); 202 | life = new Stat(); 203 | setPosCase(x,y); 204 | initLife(1); 205 | state = Normal; 206 | actions = new RecyclablePool(15, ()->new tools.ChargedAction()); 207 | 208 | allVelocities = new VelocityArray(15); 209 | vBase = registerNewVelocity(0.82); 210 | vBump = registerNewVelocity(0.93); 211 | 212 | spr = new HSprite(Assets.tiles); 213 | Game.ME.scroller.add(spr, Const.DP_MAIN); 214 | spr.colorAdd = new h3d.Vector(); 215 | baseColor = new h3d.Vector(); 216 | blinkColor = new h3d.Vector(); 217 | spr.colorMatrix = colorMatrix = h3d.Matrix.I(); 218 | spr.setCenterRatio(pivotX, pivotY); 219 | 220 | if( ui.Console.ME.hasFlag(F_Bounds) ) 221 | enableDebugBounds(); 222 | } 223 | 224 | 225 | public function registerNewVelocity(frict:Float) : Velocity { 226 | var v = Velocity.createFrict(frict); 227 | allVelocities.push(v); 228 | return v; 229 | } 230 | 231 | 232 | /** Remove sprite from display context. Only do that if you're 100% sure your entity won't need the `spr` instance itself. **/ 233 | function noSprite() { 234 | spr.setEmptyTexture(); 235 | spr.remove(); 236 | } 237 | 238 | 239 | function set_pivotX(v) { 240 | pivotX = M.fclamp(v,0,1); 241 | if( spr!=null ) 242 | spr.setCenterRatio(pivotX, pivotY); 243 | return pivotX; 244 | } 245 | 246 | function set_pivotY(v) { 247 | pivotY = M.fclamp(v,0,1); 248 | if( spr!=null ) 249 | spr.setCenterRatio(pivotX, pivotY); 250 | return pivotY; 251 | } 252 | 253 | /** Initialize current and max hit points **/ 254 | public function initLife(v) { 255 | life.initMaxOnMax(v); 256 | } 257 | 258 | /** Inflict damage **/ 259 | public function hit(dmg:Int, from:Null) { 260 | if( !isAlive() || dmg<=0 ) 261 | return; 262 | 263 | life.v -= dmg; 264 | lastDmgSource = from; 265 | onDamage(dmg, from); 266 | if( life.v<=0 ) 267 | onDie(); 268 | } 269 | 270 | /** Kill instantly **/ 271 | public function kill(by:Null) { 272 | if( isAlive() ) 273 | hit(life.v, by); 274 | } 275 | 276 | function onDamage(dmg:Int, from:Entity) {} 277 | 278 | function onDie() { 279 | destroy(); 280 | } 281 | 282 | inline function set_dir(v) { 283 | return dir = v>0 ? 1 : v<0 ? -1 : dir; 284 | } 285 | 286 | /** Return TRUE if current entity wasn't destroyed or killed **/ 287 | public inline function isAlive() { 288 | return !destroyed && life.v>0; 289 | } 290 | 291 | /** Move entity to grid coordinates **/ 292 | public function setPosCase(x:Int, y:Int) { 293 | cx = x; 294 | cy = y; 295 | xr = 0.5; 296 | yr = 1; 297 | onPosManuallyChangedBoth(); 298 | } 299 | 300 | /** Move entity to pixel coordinates **/ 301 | public function setPosPixel(x:Float, y:Float) { 302 | cx = Std.int(x/Const.GRID); 303 | cy = Std.int(y/Const.GRID); 304 | xr = (x-cx*Const.GRID)/Const.GRID; 305 | yr = (y-cy*Const.GRID)/Const.GRID; 306 | onPosManuallyChangedBoth(); 307 | } 308 | 309 | /** Should be called when you manually (ie. ignoring physics) modify both X & Y entity coordinates **/ 310 | function onPosManuallyChangedBoth() { 311 | if( M.dist(attachX,attachY,prevFrameAttachX,prevFrameAttachY) > Const.GRID*2 ) { 312 | prevFrameAttachX = attachX; 313 | prevFrameAttachY = attachY; 314 | } 315 | updateLastFixedUpdatePos(); 316 | } 317 | 318 | /** Should be called when you manually (ie. ignoring physics) modify entity X coordinate **/ 319 | function onPosManuallyChangedX() { 320 | if( M.fabs(attachX-prevFrameAttachX) > Const.GRID*2 ) 321 | prevFrameAttachX = attachX; 322 | lastFixedUpdateX = attachX; 323 | } 324 | 325 | /** Should be called when you manually (ie. ignoring physics) modify entity Y coordinate **/ 326 | function onPosManuallyChangedY() { 327 | if( M.fabs(attachY-prevFrameAttachY) > Const.GRID*2 ) 328 | prevFrameAttachY = attachY; 329 | lastFixedUpdateY = attachY; 330 | } 331 | 332 | 333 | /** Quickly set X/Y pivots. If Y is omitted, it will be equal to X. **/ 334 | public function setPivots(x:Float, y=-99.) { 335 | pivotX = x; 336 | pivotY = y>=-98 ? y : x; 337 | } 338 | 339 | /** Return TRUE if the Entity *center point* is in screen bounds (default padding is +32px) **/ 340 | public inline function isOnScreenCenter(padding=32) { 341 | return camera.isOnScreen( centerX, centerY, padding + M.fmax(wid*0.5, hei*0.5) ); 342 | } 343 | 344 | /** Return TRUE if the Entity rectangle is in screen bounds (default padding is +32px) **/ 345 | public inline function isOnScreenBounds(padding=32) { 346 | return camera.isOnScreenRect( left,top, wid, hei, padding ); 347 | } 348 | 349 | 350 | /** 351 | Changed the current entity state. 352 | Return TRUE if the state is `s` after the call. 353 | **/ 354 | public function startState(s:State) : Bool { 355 | if( s==state ) 356 | return true; 357 | 358 | if( !canChangeStateTo(state, s) ) 359 | return false; 360 | 361 | var old = state; 362 | state = s; 363 | onStateChange(old,state); 364 | return true; 365 | } 366 | 367 | 368 | /** Return TRUE to allow a change of the state value **/ 369 | function canChangeStateTo(from:State, to:State) { 370 | return true; 371 | } 372 | 373 | /** Called when state is changed to a new value **/ 374 | function onStateChange(old:State, newState:State) {} 375 | 376 | 377 | /** Apply a bump/kick force to entity **/ 378 | public function bump(x:Float,y:Float) { 379 | vBump.addXY(x,y); 380 | } 381 | 382 | /** Reset velocities to zero **/ 383 | public function cancelVelocities() { 384 | allVelocities.clearAll(); 385 | } 386 | 387 | public function is(c:Class) return Std.isOfType(this, c); 388 | public function as(c:Class) : T return Std.downcast(this, c); 389 | 390 | /** Return a random Float value in range [min,max]. If `sign` is TRUE, returned value might be multiplied by -1 randomly. **/ 391 | public inline function rnd(min,max,sign=false) return Lib.rnd(min,max,sign); 392 | /** Return a random Integer value in range [min,max]. If `sign` is TRUE, returned value might be multiplied by -1 randomly. **/ 393 | public inline function irnd(min,max,sign=false) return Lib.irnd(min,max,sign); 394 | 395 | /** Truncate a float value using given `precision` **/ 396 | public inline function pretty(value:Float,precision=1) return M.pretty(value,precision); 397 | 398 | public inline function dirTo(e:Entity) return e.centerXVoid, ?onProgress:ChargedAction->Void) { 536 | if( !isAlive() ) 537 | return; 538 | 539 | if( isChargingAction(id) ) 540 | cancelAction(id); 541 | 542 | var a = actions.alloc(); 543 | a.id = id; 544 | a.onComplete = onComplete; 545 | a.durationS = sec; 546 | if( onProgress!=null ) 547 | a.onProgress = onProgress; 548 | } 549 | 550 | /** If id is null, return TRUE if any action is charging. If id is provided, return TRUE if this specific action is charging nokw. **/ 551 | public function isChargingAction(?id:ChargedActionId) { 552 | if( !isAlive() ) 553 | return false; 554 | 555 | if( id==null ) 556 | return actions.allocated>0; 557 | 558 | for(a in actions) 559 | if( a.id==id ) 560 | return true; 561 | 562 | return false; 563 | } 564 | 565 | public function cancelAction(?onlyId:ChargedActionId) { 566 | if( !isAlive() ) 567 | return; 568 | 569 | if( onlyId==null ) 570 | actions.freeAll(); 571 | else { 572 | var i = 0; 573 | while( i0; 599 | } 600 | 601 | public inline function getAffectDurationS(k:Affect) { 602 | return hasAffect(k) ? affects.get(k) : 0.; 603 | } 604 | 605 | /** Add an Affect. If `allowLower` is TRUE, it is possible to override an existing Affect with a shorter duration. **/ 606 | public function setAffectS(k:Affect, t:Float, allowLower=false) { 607 | if( !isAlive() || affects.exists(k) && affects.get(k)>t && !allowLower ) 608 | return; 609 | 610 | if( t<=0 ) 611 | clearAffect(k); 612 | else { 613 | var isNew = !hasAffect(k); 614 | affects.set(k,t); 615 | if( isNew ) 616 | onAffectStart(k); 617 | } 618 | } 619 | 620 | /** Multiply an Affect duration by a factor `f` **/ 621 | public function mulAffectS(k:Affect, f:Float) { 622 | if( hasAffect(k) ) 623 | setAffectS(k, getAffectDurationS(k)*f, true); 624 | } 625 | 626 | public function clearAffect(k:Affect) { 627 | if( hasAffect(k) ) { 628 | affects.remove(k); 629 | onAffectEnd(k); 630 | } 631 | } 632 | 633 | /** Affects update loop **/ 634 | function updateAffects() { 635 | if( !isAlive() ) 636 | return; 637 | 638 | for(k in affects.keys()) { 639 | var t = affects.get(k); 640 | t-=1/Const.FPS * tmod; 641 | if( t<=0 ) 642 | clearAffect(k); 643 | else 644 | affects.set(k,t); 645 | } 646 | } 647 | 648 | function onAffectStart(k:Affect) {} 649 | function onAffectEnd(k:Affect) {} 650 | 651 | /** Return TRUE if the entity is active and has no status affect that prevents actions. **/ 652 | public function isConscious() { 653 | return !hasAffect(Stun) && isAlive(); 654 | } 655 | 656 | /** Blink `spr` briefly (eg. when damaged by something) **/ 657 | public function blink(c:Col) { 658 | blinkColor.setColor(c); 659 | cd.setS("keepBlink",0.06); 660 | } 661 | 662 | public function shakeS(xPow:Float, yPow:Float, t:Float) { 663 | cd.setS("shaking", t, true); 664 | shakePowX = xPow; 665 | shakePowY = yPow; 666 | } 667 | 668 | /** Briefly squash sprite on X (Y changes accordingly). "1.0" means no distorsion. **/ 669 | public function setSquashX(scaleX:Float) { 670 | sprSquashX = scaleX; 671 | sprSquashY = 2-scaleX; 672 | } 673 | 674 | /** Briefly squash sprite on Y (X changes accordingly). "1.0" means no distorsion. **/ 675 | public function setSquashY(scaleY:Float) { 676 | sprSquashX = 2-scaleY; 677 | sprSquashY = scaleY; 678 | } 679 | 680 | 681 | /** 682 | "Beginning of the frame" loop, called before any other Entity update loop 683 | **/ 684 | public function preUpdate() { 685 | ucd.update(utmod); 686 | cd.update(tmod); 687 | updateAffects(); 688 | updateActions(); 689 | 690 | 691 | #if debug 692 | // Display the list of active "affects" (with `/set affect` in console) 693 | if( ui.Console.ME.hasFlag(F_Affects) ) { 694 | var all = []; 695 | for(k in affects.keys()) 696 | all.push( k+"=>"+M.pretty( getAffectDurationS(k) , 1) ); 697 | debug(all); 698 | } 699 | 700 | // Show bounds (with `/bounds` in console) 701 | if( ui.Console.ME.hasFlag(F_Bounds) && debugBounds==null ) 702 | enableDebugBounds(); 703 | 704 | // Hide bounds 705 | if( !ui.Console.ME.hasFlag(F_Bounds) && debugBounds!=null ) 706 | disableDebugBounds(); 707 | #end 708 | 709 | } 710 | 711 | /** 712 | Post-update loop, which is guaranteed to happen AFTER any preUpdate/update. This is usually where render and display is updated 713 | **/ 714 | public function postUpdate() { 715 | spr.x = sprX; 716 | spr.y = sprY; 717 | spr.scaleX = dir*sprScaleX * sprSquashX; 718 | spr.scaleY = sprScaleY * sprSquashY; 719 | spr.visible = entityVisible; 720 | 721 | sprSquashX += (1-sprSquashX) * M.fmin(1, 0.2*tmod); 722 | sprSquashY += (1-sprSquashY) * M.fmin(1, 0.2*tmod); 723 | 724 | if( cd.has("shaking") ) { 725 | spr.x += Math.cos(ftime*1.1)*shakePowX * cd.getRatio("shaking"); 726 | spr.y += Math.sin(0.3+ftime*1.7)*shakePowY * cd.getRatio("shaking"); 727 | } 728 | 729 | // Blink 730 | if( !cd.has("keepBlink") ) { 731 | blinkColor.r*=Math.pow(0.60, tmod); 732 | blinkColor.g*=Math.pow(0.55, tmod); 733 | blinkColor.b*=Math.pow(0.50, tmod); 734 | } 735 | 736 | // Color adds 737 | spr.colorAdd.load(baseColor); 738 | spr.colorAdd.r += blinkColor.r; 739 | spr.colorAdd.g += blinkColor.g; 740 | spr.colorAdd.b += blinkColor.b; 741 | 742 | // Debug label 743 | if( debugLabel!=null ) { 744 | debugLabel.x = Std.int(attachX - debugLabel.textWidth*0.5); 745 | debugLabel.y = Std.int(attachY+1); 746 | } 747 | 748 | // Debug bounds 749 | if( debugBounds!=null ) { 750 | if( invalidateDebugBounds ) { 751 | invalidateDebugBounds = false; 752 | renderDebugBounds(); 753 | } 754 | debugBounds.x = Std.int(attachX); 755 | debugBounds.y = Std.int(attachY); 756 | } 757 | } 758 | 759 | /** 760 | Loop that runs at the absolute end of the frame 761 | **/ 762 | public function finalUpdate() { 763 | prevFrameAttachX = attachX; 764 | prevFrameAttachY = attachY; 765 | } 766 | 767 | 768 | final function updateLastFixedUpdatePos() { 769 | lastFixedUpdateX = attachX; 770 | lastFixedUpdateY = attachY; 771 | } 772 | 773 | 774 | 775 | /** Called at the beginning of each X movement step **/ 776 | function onPreStepX() { 777 | } 778 | 779 | /** Called at the beginning of each Y movement step **/ 780 | function onPreStepY() { 781 | } 782 | 783 | 784 | /** 785 | Main loop, but it only runs at a "guaranteed" 30 fps (so it might not be called during some frames, if the app runs at 60fps). This is usually where most gameplay elements affecting physics should occur, to ensure these will not depend on FPS at all. 786 | **/ 787 | public function fixedUpdate() { 788 | updateLastFixedUpdatePos(); 789 | 790 | /* 791 | Stepping: any movement greater than 33% of grid size (ie. 0.33) will increase the number of `steps` here. These steps will break down the full movement into smaller iterations to avoid jumping over grid collisions. 792 | */ 793 | var steps = M.ceil( ( M.fabs(dxTotal) + M.fabs(dyTotal) ) / 0.33 ); 794 | if( steps>0 ) { 795 | var n = 0; 796 | while ( n1 ) { xr--; cx++; } 804 | while( xr<0 ) { xr++; cx--; } 805 | 806 | 807 | // Y movement 808 | yr += dyTotal / steps; 809 | 810 | if( dyTotal!=0 ) 811 | onPreStepY(); // <---- Add Y collisions checks and physics in here 812 | 813 | while( yr>1 ) { yr--; cy++; } 814 | while( yr<0 ) { yr++; cy--; } 815 | 816 | n++; 817 | } 818 | } 819 | 820 | // Update velocities 821 | for(v in allVelocities) 822 | v.fixedUpdate(); 823 | } 824 | 825 | 826 | /** 827 | Main loop running at full FPS (ie. always happen once on every frames, after preUpdate and before postUpdate) 828 | **/ 829 | public function frameUpdate() { 830 | } 831 | } -------------------------------------------------------------------------------- /src/game/Fx.hx: -------------------------------------------------------------------------------- 1 | import h2d.Sprite; 2 | import dn.heaps.HParticle; 3 | 4 | 5 | class Fx extends GameChildProcess { 6 | var pool : ParticlePool; 7 | 8 | public var bg_add : h2d.SpriteBatch; 9 | public var bg_normal : h2d.SpriteBatch; 10 | public var main_add : h2d.SpriteBatch; 11 | public var main_normal : h2d.SpriteBatch; 12 | 13 | public function new() { 14 | super(); 15 | 16 | pool = new ParticlePool(Assets.tiles.tile, 2048, Const.FPS); 17 | 18 | bg_add = new h2d.SpriteBatch(Assets.tiles.tile); 19 | game.scroller.add(bg_add, Const.DP_FX_BG); 20 | bg_add.blendMode = Add; 21 | bg_add.hasRotationScale = true; 22 | 23 | bg_normal = new h2d.SpriteBatch(Assets.tiles.tile); 24 | game.scroller.add(bg_normal, Const.DP_FX_BG); 25 | bg_normal.hasRotationScale = true; 26 | 27 | main_normal = new h2d.SpriteBatch(Assets.tiles.tile); 28 | game.scroller.add(main_normal, Const.DP_FX_FRONT); 29 | main_normal.hasRotationScale = true; 30 | 31 | main_add = new h2d.SpriteBatch(Assets.tiles.tile); 32 | game.scroller.add(main_add, Const.DP_FX_FRONT); 33 | main_add.blendMode = Add; 34 | main_add.hasRotationScale = true; 35 | } 36 | 37 | override public function onDispose() { 38 | super.onDispose(); 39 | 40 | pool.dispose(); 41 | bg_add.remove(); 42 | bg_normal.remove(); 43 | main_add.remove(); 44 | main_normal.remove(); 45 | } 46 | 47 | /** Clear all particles **/ 48 | public function clear() { 49 | pool.clear(); 50 | } 51 | 52 | /** Create a HParticle instance in the BG layer, using ADDITIVE blendmode **/ 53 | public inline function allocBg_add(id,x,y) return pool.alloc(bg_add, Assets.tiles.getRandomTile(id), x, y); 54 | 55 | /** Create a HParticle instance in the BG layer, using NORMAL blendmode **/ 56 | public inline function allocBg_normal(id,x,y) return pool.alloc(bg_normal, Assets.tiles.getRandomTile(id), x, y); 57 | 58 | /** Create a HParticle instance in the MAIN layer, using ADDITIVE blendmode **/ 59 | public inline function allocMain_add(id,x,y) return pool.alloc( main_add, Assets.tiles.getRandomTile(id), x, y ); 60 | 61 | /** Create a HParticle instance in the MAIN layer, using NORMAL blendmode **/ 62 | public inline function allocMain_normal(id,x,y) return pool.alloc(main_normal, Assets.tiles.getRandomTile(id), x, y); 63 | 64 | 65 | public inline function markerEntity(e:Entity, c:Col=Pink, sec=3.0) { 66 | #if debug 67 | if( e!=null && e.isAlive() ) { 68 | var p = allocMain_add(D.tiles.fxCircle15, e.attachX, e.attachY); 69 | p.setCenterRatio(e.pivotX, e.pivotY); 70 | p.resizeTo(e.wid, e.hei); 71 | p.setFadeS(1, 0, 0.06); 72 | p.colorize(c); 73 | p.lifeS = sec; 74 | 75 | var p = allocMain_add(D.tiles.pixel, e.attachX, e.attachY); 76 | p.setFadeS(1, 0, 0.06); 77 | p.colorize(c); 78 | p.setScale(2); 79 | p.lifeS = sec; 80 | } 81 | #end 82 | } 83 | 84 | public inline function markerCase(cx:Int, cy:Int, sec=3.0, c:Col=Pink) { 85 | #if debug 86 | var p = allocMain_add(D.tiles.fxCircle15, (cx+0.5)*Const.GRID, (cy+0.5)*Const.GRID); 87 | p.setFadeS(1, 0, 0.06); 88 | p.colorize(c); 89 | p.lifeS = sec; 90 | 91 | var p = allocMain_add(D.tiles.pixel, (cx+0.5)*Const.GRID, (cy+0.5)*Const.GRID); 92 | p.setFadeS(1, 0, 0.06); 93 | p.colorize(c); 94 | p.setScale(2); 95 | p.lifeS = sec; 96 | #end 97 | } 98 | 99 | public inline function markerFree(x:Float, y:Float, sec=3.0, c:Col=Pink) { 100 | #if debug 101 | var p = allocMain_add(D.tiles.fxDot, x,y); 102 | p.setCenterRatio(0.5,0.5); 103 | p.setFadeS(1, 0, 0.06); 104 | p.colorize(c); 105 | p.setScale(3); 106 | p.lifeS = sec; 107 | #end 108 | } 109 | 110 | public inline function markerText(cx:Int, cy:Int, txt:String, t=1.0) { 111 | #if debug 112 | var tf = new h2d.Text(Assets.fontPixel, main_normal); 113 | tf.text = txt; 114 | 115 | var p = allocMain_add(D.tiles.fxCircle15, (cx+0.5)*Const.GRID, (cy+0.5)*Const.GRID); 116 | p.colorize(0x0080FF); 117 | p.alpha = 0.6; 118 | p.lifeS = 0.3; 119 | p.fadeOutSpeed = 0.4; 120 | p.onKill = tf.remove; 121 | 122 | tf.setPosition(p.x-tf.textWidth*0.5, p.y-tf.textHeight*0.5); 123 | #end 124 | } 125 | 126 | 127 | public inline function markerLine(fx:Float, fy:Float, tx:Float, ty:Float, c:Col, sec=3.) { 128 | #if debug 129 | var p = allocMain_add(D.tiles.fxLine, fx,fy); 130 | p.setFadeS(1, 0, 0); 131 | p.colorize(c); 132 | p.setCenterRatio(0,0.5); 133 | p.scaleX = M.dist(fx,fy,tx,ty) / p.t.width; 134 | p.rotation = Math.atan2(ty-fy, tx-fx); 135 | p.lifeS = sec; 136 | #end 137 | } 138 | 139 | inline function collides(p:HParticle, offX=0., offY=0.) { 140 | return level.hasCollision( Std.int((p.x+offX)/Const.GRID), Std.int((p.y+offY)/Const.GRID) ); 141 | } 142 | 143 | public inline function flashBangS(c:Col, a:Float, t=0.1) { 144 | var e = new h2d.Bitmap(h2d.Tile.fromColor(c,1,1,a)); 145 | game.root.add(e, Const.DP_FX_FRONT); 146 | e.scaleX = game.stageWid; 147 | e.scaleY = game.stageHei; 148 | e.blendMode = Add; 149 | game.tw.createS(e.alpha, 0, t).end( function() { 150 | e.remove(); 151 | }); 152 | } 153 | 154 | 155 | /** 156 | A small sample to demonstrate how basic particles work. This example produces a small explosion of yellow dots that will fall and slowly fade to purple. 157 | 158 | USAGE: fx.dotsExplosionExample(50,50, 0xffcc00) 159 | **/ 160 | public inline function dotsExplosionExample(x:Float, y:Float, color:Col) { 161 | for(i in 0...80) { 162 | var p = allocMain_add( D.tiles.fxDot, x+rnd(0,3,true), y+rnd(0,3,true) ); 163 | p.alpha = rnd(0.4,1); 164 | p.colorAnimS(color, 0x762087, rnd(0.6, 3)); // fade particle color from given color to some purple 165 | p.moveAwayFrom(x,y, rnd(1,3)); // move away from source 166 | p.frict = rnd(0.8, 0.9); // friction applied to velocities 167 | p.gy = rnd(0, 0.02); // gravity Y (added on each frame) 168 | p.lifeS = rnd(2,3); // life time in seconds 169 | } 170 | } 171 | 172 | 173 | override function update() { 174 | super.update(); 175 | pool.update(game.tmod); 176 | } 177 | } -------------------------------------------------------------------------------- /src/game/Game.hx: -------------------------------------------------------------------------------- 1 | class Game extends AppChildProcess { 2 | public static var ME : Game; 3 | 4 | /** Game controller (pad or keyboard) **/ 5 | public var ca : ControllerAccess; 6 | 7 | /** Particles **/ 8 | public var fx : Fx; 9 | 10 | /** Basic viewport control **/ 11 | public var camera : Camera; 12 | 13 | /** Container of all visual game objects. Ths wrapper is moved around by Camera. **/ 14 | public var scroller : h2d.Layers; 15 | 16 | /** Level data **/ 17 | public var level : Level; 18 | 19 | /** UI **/ 20 | public var hud : ui.Hud; 21 | 22 | /** Slow mo internal values**/ 23 | var curGameSpeed = 1.0; 24 | var slowMos : Map = new Map(); 25 | 26 | 27 | public function new() { 28 | super(); 29 | 30 | ME = this; 31 | ca = App.ME.controller.createAccess(); 32 | ca.lockCondition = isGameControllerLocked; 33 | createRootInLayers(App.ME.root, Const.DP_BG); 34 | dn.Gc.runNow(); 35 | 36 | scroller = new h2d.Layers(); 37 | root.add(scroller, Const.DP_BG); 38 | scroller.filter = new h2d.filter.Nothing(); // force rendering for pixel perfect 39 | 40 | fx = new Fx(); 41 | hud = new ui.Hud(); 42 | camera = new Camera(); 43 | 44 | startLevel(Assets.worldData.all_worlds.SampleWorld.all_levels.FirstLevel); 45 | } 46 | 47 | 48 | public static function isGameControllerLocked() { 49 | return !exists() || ME.isPaused() || App.ME.anyInputHasFocus(); 50 | } 51 | 52 | 53 | public static inline function exists() { 54 | return ME!=null && !ME.destroyed; 55 | } 56 | 57 | 58 | /** Load a level **/ 59 | function startLevel(l:World.World_Level) { 60 | if( level!=null ) 61 | level.destroy(); 62 | fx.clear(); 63 | for(e in Entity.ALL) // <---- Replace this with more adapted entity destruction (eg. keep the player alive) 64 | e.destroy(); 65 | garbageCollectEntities(); 66 | 67 | level = new Level(l); 68 | // <---- Here: instanciate your level entities 69 | 70 | camera.centerOnTarget(); 71 | hud.onLevelStart(); 72 | dn.Process.resizeAll(); 73 | dn.Gc.runNow(); 74 | } 75 | 76 | 77 | 78 | /** Called when either CastleDB or `const.json` changes on disk **/ 79 | @:allow(App) 80 | function onDbReload() { 81 | hud.notify("DB reloaded"); 82 | } 83 | 84 | 85 | /** Called when LDtk file changes on disk **/ 86 | @:allow(assets.Assets) 87 | function onLdtkReload() { 88 | hud.notify("LDtk reloaded"); 89 | if( level!=null ) 90 | startLevel( Assets.worldData.all_worlds.SampleWorld.getLevel(level.data.uid) ); 91 | } 92 | 93 | /** Window/app resize event **/ 94 | override function onResize() { 95 | super.onResize(); 96 | } 97 | 98 | 99 | /** Garbage collect any Entity marked for destruction. This is normally done at the end of the frame, but you can call it manually if you want to make sure marked entities are disposed right away, and removed from lists. **/ 100 | public function garbageCollectEntities() { 101 | if( Entity.GC==null || Entity.GC.allocated==0 ) 102 | return; 103 | 104 | for(e in Entity.GC) 105 | e.dispose(); 106 | Entity.GC.empty(); 107 | } 108 | 109 | /** Called if game is destroyed, but only at the end of the frame **/ 110 | override function onDispose() { 111 | super.onDispose(); 112 | 113 | fx.destroy(); 114 | for(e in Entity.ALL) 115 | e.destroy(); 116 | garbageCollectEntities(); 117 | 118 | if( ME==this ) 119 | ME = null; 120 | } 121 | 122 | 123 | /** 124 | Start a cumulative slow-motion effect that will affect `tmod` value in this Process 125 | and all its children. 126 | 127 | @param sec Realtime second duration of this slowmo 128 | @param speedFactor Cumulative multiplier to the Process `tmod` 129 | **/ 130 | public function addSlowMo(id:SlowMoId, sec:Float, speedFactor=0.3) { 131 | if( slowMos.exists(id) ) { 132 | var s = slowMos.get(id); 133 | s.f = speedFactor; 134 | s.t = M.fmax(s.t, sec); 135 | } 136 | else 137 | slowMos.set(id, { id:id, t:sec, f:speedFactor }); 138 | } 139 | 140 | 141 | /** The loop that updates slow-mos **/ 142 | final function updateSlowMos() { 143 | // Timeout active slow-mos 144 | for(s in slowMos) { 145 | s.t -= utmod * 1/Const.FPS; 146 | if( s.t<=0 ) 147 | slowMos.remove(s.id); 148 | } 149 | 150 | // Update game speed 151 | var targetGameSpeed = 1.0; 152 | for(s in slowMos) 153 | targetGameSpeed*=s.f; 154 | curGameSpeed += (targetGameSpeed-curGameSpeed) * (targetGameSpeed>curGameSpeed ? 0.2 : 0.6); 155 | 156 | if( M.fabs(curGameSpeed-targetGameSpeed)<=0.001 ) 157 | curGameSpeed = targetGameSpeed; 158 | } 159 | 160 | 161 | /** 162 | Pause briefly the game for 1 frame: very useful for impactful moments, 163 | like when hitting an opponent in Street Fighter ;) 164 | **/ 165 | public inline function stopFrame() { 166 | ucd.setS("stopFrame", 4/Const.FPS); 167 | } 168 | 169 | 170 | /** Loop that happens at the beginning of the frame **/ 171 | override function preUpdate() { 172 | super.preUpdate(); 173 | 174 | for(e in Entity.ALL) if( !e.destroyed ) e.preUpdate(); 175 | } 176 | 177 | /** Loop that happens at the end of the frame **/ 178 | override function postUpdate() { 179 | super.postUpdate(); 180 | 181 | // Update slow-motions 182 | updateSlowMos(); 183 | baseTimeMul = ( 0.2 + 0.8*curGameSpeed ) * ( ucd.has("stopFrame") ? 0.1 : 1 ); 184 | Assets.tiles.tmod = tmod; 185 | 186 | // Entities post-updates 187 | for(e in Entity.ALL) if( !e.destroyed ) e.postUpdate(); 188 | 189 | // Entities final updates 190 | for(e in Entity.ALL) if( !e.destroyed ) e.finalUpdate(); 191 | 192 | // Dispose entities marked as "destroyed" 193 | garbageCollectEntities(); 194 | } 195 | 196 | 197 | /** Main loop but limited to 30 fps (so it might not be called during some frames) **/ 198 | override function fixedUpdate() { 199 | super.fixedUpdate(); 200 | 201 | // Entities "30 fps" loop 202 | for(e in Entity.ALL) if( !e.destroyed ) e.fixedUpdate(); 203 | } 204 | 205 | /** Main loop **/ 206 | override function update() { 207 | super.update(); 208 | 209 | // Entities main loop 210 | for(e in Entity.ALL) if( !e.destroyed ) e.frameUpdate(); 211 | 212 | 213 | // Global key shortcuts 214 | if( !App.ME.anyInputHasFocus() && !ui.Window.hasAnyModal() && !Console.ME.isActive() ) { 215 | // Exit by pressing ESC twice 216 | #if hl 217 | if( ca.isKeyboardPressed(K.ESCAPE) ) 218 | if( !cd.hasSetS("exitWarn",3) ) 219 | hud.notify(Lang.t._("Press ESCAPE again to exit.")); 220 | else 221 | App.ME.exit(); 222 | #end 223 | 224 | // Attach debug drone (CTRL-SHIFT-D) 225 | #if debug 226 | if( ca.isPressed(ToggleDebugDrone) ) 227 | new DebugDrone(); // <-- HERE: provide an Entity as argument to attach Drone near it 228 | #end 229 | 230 | // Restart whole game 231 | if( ca.isPressed(Restart) ) 232 | App.ME.startGame(); 233 | 234 | } 235 | } 236 | } 237 | 238 | -------------------------------------------------------------------------------- /src/game/Level.hx: -------------------------------------------------------------------------------- 1 | class Level extends GameChildProcess { 2 | /** Level grid-based width**/ 3 | public var cWid(default,null): Int; 4 | /** Level grid-based height **/ 5 | public var cHei(default,null): Int; 6 | 7 | /** Level pixel width**/ 8 | public var pxWid(default,null) : Int; 9 | /** Level pixel height**/ 10 | public var pxHei(default,null) : Int; 11 | 12 | public var data : World_Level; 13 | var tilesetSource : h2d.Tile; 14 | 15 | public var marks : dn.MarkerMap; 16 | var invalidated = true; 17 | 18 | public function new(ldtkLevel:World.World_Level) { 19 | super(); 20 | 21 | createRootInLayers(Game.ME.scroller, Const.DP_BG); 22 | data = ldtkLevel; 23 | cWid = data.l_Collisions.cWid; 24 | cHei = data.l_Collisions.cHei; 25 | pxWid = cWid * Const.GRID; 26 | pxHei = cHei * Const.GRID; 27 | tilesetSource = hxd.Res.levels.sampleWorldTiles.toAseprite().toTile(); 28 | 29 | marks = new dn.MarkerMap(cWid, cHei); 30 | for(cy in 0...cHei) 31 | for(cx in 0...cWid) { 32 | if( data.l_Collisions.getInt(cx,cy)==1 ) 33 | marks.set(M_Coll_Wall, cx,cy); 34 | } 35 | } 36 | 37 | override function onDispose() { 38 | super.onDispose(); 39 | data = null; 40 | tilesetSource = null; 41 | marks.dispose(); 42 | marks = null; 43 | } 44 | 45 | /** TRUE if given coords are in level bounds **/ 46 | public inline function isValid(cx,cy) return cx>=0 && cx=0 && cy{ 65 | // Only reload actual updated file from disk after a short delay, to avoid reading a file being written 66 | App.ME.delayer.cancelById("ldtk"); 67 | App.ME.delayer.addS("ldtk", function() { 68 | worldData.parseJson( res.entry.getText() ); 69 | if( Game.exists() ) 70 | Game.ME.onLdtkReload(); 71 | }, 0.2); 72 | }); 73 | #end 74 | } 75 | 76 | 77 | /** 78 | Pass `tmod` value from the game to atlases, to allow them to play animations at the same speed as the Game. 79 | For example, if the game has some slow-mo running, all atlas anims should also play in slow-mo 80 | **/ 81 | public static function update(tmod:Float) { 82 | if( Game.exists() && Game.ME.isPaused() ) 83 | tmod = 0; 84 | 85 | tiles.tmod = tmod; 86 | // <-- add other atlas TMOD updates here 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /src/game/assets/AssetsDictionaries.hx: -------------------------------------------------------------------------------- 1 | package assets; 2 | 3 | /** 4 | Access to slice names present in Aseprite files (eg. `trace( tiles.fxStar )` ). 5 | This class only provides access to *names* (ie. String). To get actual h2d.Tile, use Assets class. 6 | 7 | Examples: 8 | ```haxe 9 | Assets.tiles.getTile( AssetsDictionaries.tiles.mySlice ); 10 | Assets.tiles.getTile( D.tiles.mySlice ); // uses "D" alias defined in "import.hx" file 11 | ``` 12 | **/ 13 | class AssetsDictionaries { 14 | public static var tiles = dn.heaps.assets.Aseprite.getDict( hxd.Res.atlas.tiles ); 15 | } -------------------------------------------------------------------------------- /src/game/assets/CastleDb.hx: -------------------------------------------------------------------------------- 1 | package assets; 2 | 3 | private typedef Init = haxe.macro.MacroType<[cdb.Module.build("data.cdb")]>; 4 | -------------------------------------------------------------------------------- /src/game/assets/ConstDbBuilder.hx: -------------------------------------------------------------------------------- 1 | package assets; 2 | 3 | #if( macro || display ) 4 | import haxe.macro.Context; 5 | import haxe.macro.Expr; 6 | using haxe.macro.Tools; 7 | #end 8 | 9 | // Rough CastleDB JSON typedef 10 | typedef CastleDbJson = { 11 | sheets : Array<{ 12 | name : String, 13 | columns : Array<{ 14 | typeStr: String, 15 | name: String, 16 | }>, 17 | lines:Array<{ 18 | constId: String, 19 | values: Array<{ 20 | value : Dynamic, 21 | valueName : String, 22 | subValues: Dynamic, 23 | isInteger : Bool, 24 | doc : String, 25 | }>, 26 | }>, 27 | }>, 28 | } 29 | 30 | class ConstDbBuilder { 31 | 32 | /** 33 | Generate a class based on fields extracted from provided source files (JSON or CastleDB). Then return an instance of this class to be stored in some static var. Typically: 34 | ```haxe 35 | public static var db = ConstDbBuilder.buildVar(["data.cdb", "const.json"]); 36 | ``` 37 | **/ 38 | public static macro function buildVar(dbFileNames:Array) { 39 | var pos = Context.currentPos(); 40 | var rawMod = Context.getLocalModule(); 41 | var modPack = rawMod.split("."); 42 | var modName = modPack.pop(); 43 | 44 | // Create class type 45 | var classTypeDef : TypeDefinition = { 46 | pos : pos, 47 | name : cleanupIdentifier('Db_${dbFileNames.join("_")}'), 48 | pack : modPack, 49 | meta: [{ name:":keep", pos:pos }], 50 | doc: "Project specific Level class", 51 | kind : TDClass(), 52 | fields : (macro class { 53 | public function new() {} 54 | 55 | /** This callback will trigger when one of the files is reloaded. **/ 56 | public dynamic function onReload() {} 57 | }).fields, 58 | } 59 | 60 | // Parse given files and create class fields 61 | for(f in dbFileNames) { 62 | var fileFields = switch dn.FilePath.extractExtension(f) { 63 | case "cdb": readCdb(f); 64 | case "json": readJson(f); 65 | case _: Context.fatalError("Unsupported database file "+f, pos); 66 | } 67 | classTypeDef.fields = fileFields.concat(classTypeDef.fields); 68 | } 69 | 70 | // Register stuff 71 | Context.defineModule(rawMod, [classTypeDef]); 72 | for(f in dbFileNames) 73 | Context.registerModuleDependency(rawMod, resolveFilePath(f)); 74 | 75 | // Return class constructor 76 | var classTypePath : TypePath = { pack:classTypeDef.pack, name:classTypeDef.name } 77 | return macro new $classTypePath(); 78 | } 79 | 80 | 81 | #if( macro || display ) 82 | 83 | /** 84 | Parse a JSON and create class fields using its root values 85 | **/ 86 | static function readJson(fileName:String) : Array { 87 | var uid = cleanupIdentifier(fileName); 88 | var pos = Context.currentPos(); 89 | 90 | // Read file 91 | var path = resolveFilePath(fileName); 92 | if( path==null ) { 93 | Context.fatalError("File not found: "+fileName, pos); 94 | return []; 95 | } 96 | 97 | var fileName = dn.FilePath.extractFileWithExt(path); 98 | Context.registerModuleDependency(Context.getLocalModule(), path); 99 | 100 | 101 | // Parse JSON 102 | var raw = sys.io.File.getContent(path); 103 | var jsonPos = Context.makePosition({ file:path, min:1, max:1 }); 104 | var json = try haxe.Json.parse(raw) catch(_) null; 105 | if( json==null ) { 106 | Context.fatalError("Couldn't parse JSON: "+path, jsonPos); 107 | return []; 108 | } 109 | 110 | // List all supported fields in JSON 111 | var fields : Array = []; 112 | var initializers : Array = []; 113 | for(k in Reflect.fields(json)) { 114 | var val = Reflect.field(json, k); 115 | var kind : FieldType = null; 116 | 117 | // Build field type 118 | switch Type.typeof(val) { 119 | case TNull: 120 | kind = FVar(null); 121 | 122 | case TInt: 123 | kind = FVar(macro:Int, macro $v{val}); 124 | 125 | case TFloat: 126 | kind = FVar(macro:Float, macro $v{val}); 127 | 128 | case TBool: 129 | kind = FVar(macro:Bool, macro $v{val}); 130 | 131 | case TClass(String): 132 | kind = FVar(macro:String, macro $v{val}); 133 | 134 | case _: 135 | Context.warning('Unsupported JSON type "${Type.typeof(val)}" for $k', jsonPos); 136 | } 137 | 138 | // Add field and default value 139 | if( kind!=null ) { 140 | fields.push({ 141 | name: k, 142 | pos: pos, 143 | kind: kind, 144 | doc: '$k\n\n*From $fileName* ', 145 | access: [APublic], 146 | }); 147 | initializers.push( macro trace("hello "+$v{k}) ); 148 | } 149 | } 150 | 151 | // Update class fields using given JSON string (used for hot-reloading support) 152 | fields.push({ 153 | name: "reload_"+uid, 154 | doc: "Update class values using given JSON (useful if you want to support hot-reloading of the JSON db file)", 155 | pos: pos, 156 | access: [APublic], 157 | kind: FFun({ 158 | args: [{ name:"updatedJsonStr", type:macro:String }], 159 | expr: macro { 160 | var json = try haxe.Json.parse(updatedJsonStr) catch(_) null; 161 | if( json==null ) 162 | return; 163 | 164 | for(k in Reflect.fields(json)) 165 | if( Reflect.hasField(this, k) ) { 166 | try Reflect.setField( this, k, Reflect.field(json,k) ) 167 | catch(_) trace("ERROR: couldn't update JSON const "+k); 168 | } 169 | 170 | onReload(); 171 | }, 172 | }), 173 | }); 174 | 175 | return fields; 176 | } 177 | 178 | 179 | 180 | /** 181 | Parse CastleDB and create class fields using its "ConstDb" sheet 182 | **/ 183 | static function readCdb(fileName:String) : Array { 184 | var uid = cleanupIdentifier(fileName); 185 | var pos = Context.currentPos(); 186 | 187 | // Read file 188 | var path = resolveFilePath(fileName); 189 | if( path==null ) { 190 | Context.fatalError("File not found: "+fileName, pos); 191 | return []; 192 | } 193 | Context.registerModuleDependency(Context.getLocalModule(), path); 194 | 195 | // Parse JSON 196 | var raw = sys.io.File.getContent(path); 197 | var json : CastleDbJson = try haxe.Json.parse(raw) catch(_) null; 198 | if( json==null ) { 199 | Context.fatalError("CastleDB JSON parsing failed!", pos); 200 | return []; 201 | } 202 | 203 | // List sub-values types 204 | var subValueTypes : Map = new Map(); 205 | for(sheet in json.sheets) { 206 | if( sheet.name.indexOf("ConstDb")<0 || sheet.name.indexOf("@subValues")<0 ) 207 | continue; 208 | inline function _unsupported(typeName:String, valueName:String) { 209 | Context.fatalError("Unsupported CastleDB type "+typeName+" for sub-value "+valueName, pos); 210 | return null; 211 | } 212 | for(col in sheet.columns) { 213 | var ct : ComplexType = switch col.typeStr { 214 | case "1": macro:String; 215 | case "2": macro:Bool; 216 | case "3": macro:Int; 217 | case "4": macro:Float; 218 | case "11": macro:Int; 219 | case _: _unsupported(col.typeStr, col.name); 220 | } 221 | if( ct!=null ) 222 | subValueTypes.set(col.name, { ct:ct, typeStr:col.typeStr }); 223 | } 224 | } 225 | 226 | // List constants 227 | var fields : Array = []; 228 | var valid = false; 229 | for(sheet in json.sheets) 230 | if( sheet.name=="ConstDb" ) { 231 | if( sheet.columns.filter(c->c.name=="constId").length==0 ) 232 | continue; 233 | 234 | if( sheet.columns.filter(c->c.name=="values").length==0 ) 235 | continue; 236 | 237 | valid = true; 238 | for(l in sheet.lines) { 239 | var id = Reflect.field(l, "constId"); 240 | var doc = Reflect.field(l,"doc"); 241 | 242 | // List sub values 243 | var valuesFields : Array = []; 244 | var valuesIniters : Array = []; 245 | for( v in l.values ) { 246 | var doc = (v.doc==null ? v.valueName : v.doc ) + '\n\n*From $fileName* '; 247 | var vid = cleanupIdentifier(v.valueName); 248 | 249 | if( v.subValues!=null && Reflect.fields(v.subValues).length>0 ) { 250 | // Value is an object with sub fields 251 | var fields : Array = []; 252 | var initers : Array = []; 253 | 254 | // Read sub values 255 | for(k in Reflect.fields(v.subValues)) { 256 | if( k=="_value" ) 257 | Context.fatalError('[$fileName] "${l.constId}.${v.valueName}" value name "_value" is not allowed.', pos); 258 | 259 | var ct = subValueTypes.exists(k) ? subValueTypes.get(k).ct : (macro:Float); 260 | fields.push({ 261 | name: k, 262 | kind: FVar(ct), 263 | pos: pos, 264 | doc: doc, 265 | }); 266 | 267 | var rawVal = Reflect.field(v.subValues, k); 268 | var const : Constant = !subValueTypes.exists(k) 269 | ? CFloat( Std.string(rawVal) ) 270 | : switch subValueTypes.get(k).typeStr { 271 | case "1": CString(rawVal); 272 | case "2": CIdent( Std.string(rawVal) ); 273 | case "3": CInt( Std.string(rawVal) ); 274 | case "4": CFloat( Std.string(rawVal) ); 275 | case "11": CInt( Std.string(rawVal) ); 276 | case _: 277 | Context.fatalError("Unexpected CastleDB typeStr "+subValueTypes.get(k).typeStr+" for sub-value init expr", pos); 278 | } 279 | initers.push({ 280 | field: k, 281 | expr: { expr:EConst(const), pos:pos }, 282 | }); 283 | } 284 | 285 | // Also include column value if it's not zero 286 | if( v.value!=0 ) { 287 | fields.push({ 288 | name: "_value", 289 | pos: pos, 290 | doc: doc, 291 | kind: FVar( v.isInteger ? macro:Int : macro:Float ), 292 | }); 293 | if( v.isInteger && v.value != Std.int(v.value) ) 294 | Context.warning('[$fileName] "${l.constId}.${v.valueName}" is a Float instead of an Int', pos); 295 | var cleanVal = Std.string( v.isInteger ? Std.int(v.value) : v.value ); 296 | initers.push({ 297 | field: "_value", 298 | expr: { 299 | pos: pos, 300 | expr: EConst( v.isInteger ? CInt(cleanVal) : CFloat(cleanVal) ), 301 | }, 302 | }); 303 | } 304 | 305 | // Value definition 306 | valuesFields.push({ 307 | name: vid, 308 | pos: pos, 309 | doc: (v.doc==null ? v.valueName : v.doc ) + '\n\n*From $fileName* ', 310 | kind: FVar( TAnonymous(fields) ), 311 | }); 312 | // Value init 313 | valuesIniters.push({ 314 | field: vid, 315 | expr: { 316 | pos: pos, 317 | expr: EObjectDecl(initers), 318 | }, 319 | }); 320 | } 321 | else { 322 | // Simple value (int/float) 323 | valuesFields.push({ 324 | name: vid, 325 | pos: pos, 326 | doc: doc, 327 | kind: FVar( v.isInteger ? macro:Int : macro:Float ), 328 | }); 329 | 330 | // Initial value setter 331 | if( v.isInteger && v.value!=Std.int(v.value) ) 332 | Context.warning('[$fileName] "${l.constId}.${v.valueName}" is a Float instead of an Int', pos); 333 | var cleanVal = Std.string( v.isInteger ? Std.int(v.value) : v.value ); 334 | valuesIniters.push({ 335 | field: vid, 336 | expr: { 337 | pos: pos, 338 | expr: EConst( v.isInteger ? CInt(cleanVal) : CFloat(cleanVal) ), 339 | }, 340 | }); 341 | } 342 | } 343 | 344 | fields.push({ 345 | name: id, 346 | pos: pos, 347 | access: [APublic], 348 | doc: ( doc==null ? id : doc ) + '\n\n*From $fileName* ', 349 | kind: FVar( TAnonymous(valuesFields), { 350 | pos:pos, 351 | expr: EObjectDecl(valuesIniters), 352 | } ), 353 | }); 354 | } 355 | } 356 | 357 | // Check CDB sheets 358 | if( !valid ) { 359 | Context.fatalError('$fileName CastleDB file should contain a valid "ConstDb" sheet.', pos); 360 | return []; 361 | } 362 | 363 | // CDB hot reloader 364 | var cdbJsonType = Context.getType("ConstDbBuilder.CastleDbJson").toComplexType(); 365 | fields.push({ 366 | pos:pos, 367 | name: "reload_"+uid, 368 | doc: "Update class values using the content of the CastleDB file (useful if you want to support hot-reloading of the CastleDB file)", 369 | access: [ APublic ], 370 | kind: FFun({ 371 | args: [{ name:"updatedCdbJson", type:macro:String}], 372 | expr: macro { 373 | var json : $cdbJsonType = try haxe.Json.parse(updatedCdbJson) catch(_) null; 374 | if( json==null ) 375 | return; 376 | 377 | for(s in json.sheets) { 378 | if( s.name!="ConstDb" ) 379 | continue; 380 | 381 | for(l in s.lines) { 382 | var obj = Reflect.field(this, l.constId); 383 | if( obj==null ) { 384 | obj = {} 385 | Reflect.setField(this, l.constId, obj); 386 | } 387 | for(v in l.values) { 388 | var subValues = v.subValues==null ? [] : Reflect.fields(v.subValues); 389 | if( subValues.length>0 ) { 390 | // Reload sub values object 391 | var subObj = Reflect.field(obj, v.valueName); 392 | if( subObj==null ) { 393 | subObj = {}; 394 | Reflect.setField(obj, v.valueName, subObj); 395 | } 396 | for(k in subValues) 397 | Reflect.setField(subObj, k, Reflect.field(v.subValues, k)); 398 | 399 | // Also include (or remove) _value 400 | if( v.value!=0 ) 401 | Reflect.setField(subObj, "_value", v.isInteger ? Std.int(v.value) : v.value ); 402 | else 403 | Reflect.deleteField(subObj, "_value"); 404 | } 405 | else { 406 | // Reload int/float value 407 | Reflect.setField(obj, v.valueName, v.isInteger ? Std.int(v.value) : v.value ); 408 | } 409 | } 410 | } 411 | } 412 | onReload(); 413 | }, 414 | }), 415 | }); 416 | 417 | return fields; 418 | } 419 | 420 | 421 | 422 | /** Lookup a file in all known project paths **/ 423 | static function resolveFilePath(basePath:String) : Null { 424 | // Look in class paths 425 | var path = try Context.resolvePath(basePath) catch( e : Dynamic ) null; 426 | 427 | // Look in resourcesPath define 428 | if( path == null ) { 429 | var r = Context.definedValue("resourcesPath"); 430 | if( r != null ) { 431 | r = r.split("\\").join("/"); 432 | if( !StringTools.endsWith(r, "/") ) r += "/"; 433 | try path = Context.resolvePath(r + basePath) catch( e : Dynamic ) null; 434 | } 435 | } 436 | 437 | // Look in default Heaps resource dir 438 | if( path == null ) 439 | try path = Context.resolvePath("res/" + basePath) catch( e : Dynamic ) null; 440 | 441 | return path; 442 | } 443 | 444 | 445 | /** Remove invalid characters from a given string **/ 446 | static inline function cleanupIdentifier(str:String) { 447 | if( str==null ) 448 | return ""; 449 | else 450 | return ~/[^a-z0-9_]/gi.replace(str, "_"); 451 | } 452 | 453 | 454 | 455 | #end 456 | 457 | } -------------------------------------------------------------------------------- /src/game/assets/Lang.hx: -------------------------------------------------------------------------------- 1 | package assets; 2 | 3 | import dn.data.GetText; 4 | 5 | class Lang { 6 | static var _initDone = false; 7 | public static var CUR = "??"; 8 | public static var t : GetText; 9 | 10 | public static function init(?lid:String) { 11 | if( _initDone ) 12 | return; 13 | 14 | _initDone = true; 15 | CUR = lid==null ? getSystemLang() : lid; 16 | var res = 17 | try hxd.Res.load("lang/"+CUR+".po") 18 | catch(_) { 19 | CUR = "en"; 20 | hxd.Res.load("lang/"+CUR+".po"); 21 | } 22 | 23 | t = new GetText(); 24 | t.readPo( res.entry.getBytes() ); 25 | } 26 | 27 | public static function untranslated(str:Dynamic) : LocaleString { 28 | init(); 29 | return t.untranslated(str); 30 | } 31 | 32 | 33 | /** 34 | Return a simple language code, depending on current System setting (eg. "en", "fr", "de" etc.). If something goes wrong, this returns "en". 35 | See: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes 36 | **/ 37 | public static function getSystemLang() : String { 38 | try { 39 | var code = hxd.System.getLocale(); 40 | if( code.indexOf("-")>=0 ) 41 | code = code.substr(0,code.indexOf("-") ); 42 | return code.toLowerCase(); 43 | } 44 | catch(_) 45 | return "en"; 46 | } 47 | } -------------------------------------------------------------------------------- /src/game/assets/World.hx: -------------------------------------------------------------------------------- 1 | package assets; 2 | 3 | private typedef _Tmp = 4 | haxe.macro.MacroType<[ ldtk.Project.build("res/levels/sampleWorld.ldtk") ]>; -------------------------------------------------------------------------------- /src/game/en/DebugDrone.hx: -------------------------------------------------------------------------------- 1 | package en; 2 | 3 | /** 4 | This Entity is intended for quick debugging / level exploration. 5 | Create one by pressing CTRL-SHIFT-D in game, fly around using ARROWS. 6 | **/ 7 | @:access(Camera) 8 | class DebugDrone extends Entity { 9 | public static var ME : DebugDrone; 10 | static var DEFAULT_COLOR : Col = 0x00ff00; 11 | 12 | var ca : ControllerAccess; 13 | var prevCamTarget : Null; 14 | var prevCamZoom : Float; 15 | 16 | var g : h2d.Graphics; 17 | var help : h2d.Text; 18 | 19 | var droneDx = 0.; 20 | var droneDy = 0.; 21 | var droneFrict = 0.86; 22 | 23 | public function new() { 24 | if( ME!=null ) { 25 | ME.destroy(); 26 | Game.ME.garbageCollectEntities(); 27 | } 28 | 29 | super(0,0); 30 | 31 | ME = this; 32 | setPosPixel(camera.rawFocus.levelX, camera.rawFocus.levelY); 33 | 34 | // Controller 35 | ca = App.ME.controller.createAccess(); 36 | ca.takeExclusivity(); 37 | 38 | // Take control of camera 39 | if( camera.target!=null && camera.target.isAlive() ) 40 | prevCamTarget = camera.target; 41 | prevCamZoom = camera.zoom; 42 | camera.trackEntity(this,false); 43 | 44 | // Placeholder render 45 | g = new h2d.Graphics(spr); 46 | g.beginFill(0xffffff); 47 | g.drawCircle(0,0,6, 16); 48 | setPivots(0.5); 49 | setColor(DEFAULT_COLOR); 50 | 51 | help = new h2d.Text(Assets.fontPixel); 52 | game.root.add(help, Const.DP_TOP); 53 | help.filter = new dn.heaps.filter.PixelOutline(); 54 | help.textColor = DEFAULT_COLOR; 55 | help.text = [ 56 | "CANCEL -- Escape", 57 | "MOVE -- ARROWS/pad", 58 | "ZOOM IN -- "+ca.input.getAllBindindTextsFor(DebugDroneZoomIn).join(", "), 59 | "ZOOM OUT -- "+ca.input.getAllBindindTextsFor(DebugDroneZoomOut).join(", "), 60 | ].join("\n"); 61 | help.setScale(Const.UI_SCALE); 62 | help.x = 4*Const.UI_SCALE; 63 | 64 | // <----- HERE: add your own specific inits, like setting drone gravity to zero, updating collision behaviors etc. 65 | } 66 | 67 | inline function setColor(c:Col) { 68 | g.color.setColor( c.withAlpha(1) ); 69 | } 70 | 71 | override function dispose() { 72 | // Try to restore camera state 73 | if( prevCamTarget!=null ) 74 | camera.trackEntity(prevCamTarget, false); 75 | else 76 | camera.target = null; 77 | prevCamTarget = null; 78 | camera.forceZoom( prevCamZoom ); 79 | 80 | super.dispose(); 81 | 82 | // Clean up 83 | help.remove(); 84 | ca.dispose(); 85 | if( ME==this ) 86 | ME = null; 87 | } 88 | 89 | 90 | override function frameUpdate() { 91 | super.frameUpdate(); 92 | 93 | // Ignore game standard velocities 94 | cancelVelocities(); 95 | 96 | // Movement controls 97 | var spd = 0.02 * ( ca.isPadDown(X) ? 3 : 1 ); // turbo by holding pad-X 98 | 99 | if( !App.ME.anyInputHasFocus() ) { 100 | // Fly around 101 | var dist = ca.getAnalogDist4(MoveLeft,MoveRight, MoveUp,MoveDown); 102 | if( dist > 0 ) { 103 | var a = ca.getAnalogAngle4(MoveLeft,MoveRight, MoveUp,MoveDown); 104 | droneDx+=Math.cos(a) * dist*spd * tmod; 105 | droneDy+=Math.sin(a) * dist*spd * tmod; 106 | } 107 | 108 | // Zoom controls 109 | if( ca.isDown(DebugDroneZoomOut) ) 110 | camera.forceZoom( camera.baseZoom-0.04*camera.baseZoom ); 111 | 112 | if( ca.isDown(DebugDroneZoomIn) ) 113 | camera.forceZoom( camera.baseZoom+0.02*camera.baseZoom ); 114 | 115 | // Destroy 116 | if( ca.isKeyboardPressed(K.ESCAPE) || ca.isPressed(ToggleDebugDrone) ) { 117 | destroy(); 118 | return; 119 | } 120 | } 121 | 122 | 123 | // X physics 124 | xr += droneDx*tmod; 125 | while( xr>1 ) { xr--; cx++; } 126 | while( xr<0 ) { xr++; cx--; } 127 | droneDx*=Math.pow(droneFrict, tmod); 128 | 129 | // Y physics 130 | yr += droneDy*tmod; 131 | while( yr>1 ) { yr--; cy++; } 132 | while( yr<0 ) { yr++; cy--; } 133 | droneDy*=Math.pow(droneFrict, tmod); 134 | 135 | // Update previous cam target if it changes 136 | if( camera.target!=null && camera.target!=this && camera.target.isAlive() ) 137 | prevCamTarget = camera.target; 138 | 139 | // Display FPS 140 | debug( M.round(hxd.Timer.fps()) + " FPS" ); 141 | 142 | // Collisions 143 | if( level.hasCollision(cx,cy) ) 144 | setColor(0xff0000); 145 | else 146 | setColor(DEFAULT_COLOR); 147 | } 148 | } -------------------------------------------------------------------------------- /src/game/import.hx: -------------------------------------------------------------------------------- 1 | #if !macro 2 | 3 | // Libs 4 | import dn.M; 5 | import dn.Lib; 6 | import dn.Col; 7 | import dn.Tweenie; 8 | import dn.data.GetText; 9 | import dn.struct.*; 10 | import dn.heaps.input.*; 11 | import dn.heaps.slib.*; 12 | import dn.phys.Velocity; 13 | 14 | // Project classes 15 | import Types; 16 | import ui.Console; 17 | import ui.Bar; 18 | import ui.Window; 19 | import tools.*; 20 | import assets.*; 21 | import en.*; 22 | 23 | // Castle DB 24 | import assets.CastleDb; 25 | 26 | // LDtk 27 | import assets.World; 28 | 29 | // Aliases 30 | import dn.RandomTools as R; 31 | import assets.Assets as A; 32 | import assets.AssetsDictionaries as D; 33 | import hxd.Key as K; 34 | import tools.LPoint as P; 35 | import assets.Lang.t as L; 36 | import Const.db as DB; 37 | 38 | import dn.debug.MemTrack.measure as MM; 39 | 40 | #end -------------------------------------------------------------------------------- /src/game/sample/SampleGame.hx: -------------------------------------------------------------------------------- 1 | package sample; 2 | 3 | /** 4 | This small class just creates a SamplePlayer instance in current level 5 | **/ 6 | class SampleGame extends Game { 7 | public function new() { 8 | super(); 9 | } 10 | 11 | override function startLevel(l:World_Level) { 12 | super.startLevel(l); 13 | new SamplePlayer(); 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/game/sample/SamplePlayer.hx: -------------------------------------------------------------------------------- 1 | package sample; 2 | 3 | /** 4 | SamplePlayer is an Entity with some extra functionalities: 5 | - user controlled (using gamepad or keyboard) 6 | - falls with gravity 7 | - has basic level collisions 8 | - some squash animations, because it's cheap and they do the job 9 | **/ 10 | 11 | class SamplePlayer extends Entity { 12 | var ca : ControllerAccess; 13 | var walkSpeed = 0.; 14 | 15 | // This is TRUE if the player is not falling 16 | var onGround(get,never) : Bool; 17 | inline function get_onGround() return !destroyed && vBase.dy==0 && yr==1 && level.hasCollision(cx,cy+1); 18 | 19 | 20 | public function new() { 21 | super(5,5); 22 | 23 | // Start point using level entity "PlayerStart" 24 | var start = level.data.l_Entities.all_PlayerStart[0]; 25 | if( start!=null ) 26 | setPosCase(start.cx, start.cy); 27 | 28 | // Misc inits 29 | vBase.setFricts(0.84, 0.94); 30 | 31 | // Camera tracks this 32 | camera.trackEntity(this, true); 33 | camera.clampToLevelBounds = true; 34 | 35 | // Init controller 36 | ca = App.ME.controller.createAccess(); 37 | ca.lockCondition = Game.isGameControllerLocked; 38 | 39 | // Placeholder display 40 | var b = new h2d.Bitmap( h2d.Tile.fromColor(Green, iwid, ihei), spr ); 41 | b.tile.setCenterRatio(0.5,1); 42 | } 43 | 44 | 45 | override function dispose() { 46 | super.dispose(); 47 | ca.dispose(); // don't forget to dispose controller accesses 48 | } 49 | 50 | 51 | /** X collisions **/ 52 | override function onPreStepX() { 53 | super.onPreStepX(); 54 | 55 | // Right collision 56 | if( xr>0.8 && level.hasCollision(cx+1,cy) ) 57 | xr = 0.8; 58 | 59 | // Left collision 60 | if( xr<0.2 && level.hasCollision(cx-1,cy) ) 61 | xr = 0.2; 62 | } 63 | 64 | 65 | /** Y collisions **/ 66 | override function onPreStepY() { 67 | super.onPreStepY(); 68 | 69 | // Land on ground 70 | if( yr>1 && level.hasCollision(cx,cy+1) ) { 71 | setSquashY(0.5); 72 | vBase.dy = 0; 73 | vBump.dy = 0; 74 | yr = 1; 75 | ca.rumble(0.2, 0.06); 76 | onPosManuallyChangedY(); 77 | } 78 | 79 | // Ceiling collision 80 | if( yr<0.2 && level.hasCollision(cx,cy-1) ) 81 | yr = 0.2; 82 | } 83 | 84 | 85 | /** 86 | Control inputs are checked at the beginning of the frame. 87 | VERY IMPORTANT NOTE: because game physics only occur during the `fixedUpdate` (at a constant 30 FPS), no physics increment should ever happen here! What this means is that you can SET a physics value (eg. see the Jump below), but not make any calculation that happens over multiple frames (eg. increment X speed when walking). 88 | **/ 89 | override function preUpdate() { 90 | super.preUpdate(); 91 | 92 | walkSpeed = 0; 93 | if( onGround ) 94 | cd.setS("recentlyOnGround",0.1); // allows "just-in-time" jumps 95 | 96 | 97 | // Jump 98 | if( cd.has("recentlyOnGround") && ca.isPressed(Jump) ) { 99 | vBase.dy = -0.85; 100 | setSquashX(0.6); 101 | cd.unset("recentlyOnGround"); 102 | fx.dotsExplosionExample(centerX, centerY, 0xffcc00); 103 | ca.rumble(0.05, 0.06); 104 | } 105 | 106 | // Walk 107 | if( !isChargingAction() && ca.getAnalogDist2(MoveLeft,MoveRight)>0 ) { 108 | // As mentioned above, we don't touch physics values (eg. `dx`) here. We just store some "requested walk speed", which will be applied to actual physics in fixedUpdate. 109 | walkSpeed = ca.getAnalogValue2(MoveLeft,MoveRight); // -1 to 1 110 | } 111 | } 112 | 113 | 114 | override function fixedUpdate() { 115 | super.fixedUpdate(); 116 | 117 | // Gravity 118 | if( !onGround ) 119 | vBase.dy+=0.05; 120 | 121 | // Apply requested walk movement 122 | if( walkSpeed!=0 ) 123 | vBase.dx += walkSpeed * 0.045; // some arbitrary speed 124 | } 125 | } -------------------------------------------------------------------------------- /src/game/tools/AppChildProcess.hx: -------------------------------------------------------------------------------- 1 | package tools; 2 | 3 | class AppChildProcess extends dn.Process { 4 | public static var ALL : FixedArray = new FixedArray(256); 5 | 6 | public var app(get,never) : App; inline function get_app() return App.ME; 7 | 8 | public function new() { 9 | super(App.ME); 10 | ALL.push(this); 11 | } 12 | 13 | override function onDispose() { 14 | super.onDispose(); 15 | ALL.remove(this); 16 | } 17 | } -------------------------------------------------------------------------------- /src/game/tools/ChargedAction.hx: -------------------------------------------------------------------------------- 1 | package tools; 2 | 3 | /** An utility class to manage Entity charged actions, with a very low memory footprint (garbage collector friendly) **/ 4 | class ChargedAction implements dn.struct.RecyclablePool.Recyclable { 5 | public var id : ChargedActionId; 6 | public var durationS = 0.; 7 | public var elapsedS(default,null) = 0.; 8 | public var remainS(get,never) : Float; inline function get_remainS() return M.fclamp(durationS-elapsedS, 0, durationS); 9 | 10 | public var onComplete : ChargedAction->Void; 11 | public var onProgress : ChargedAction->Void; 12 | 13 | /** From 0 (start) to 1 (end) **/ 14 | public var elapsedRatio(get,never) : Float; 15 | inline function get_elapsedRatio() return durationS<=0 ? 1 : M.fclamp(elapsedS/durationS, 0, 1); 16 | 17 | /** From 1 (start) to 0 (end) **/ 18 | public var remainingRatio(get,never) : Float; 19 | inline function get_remainingRatio() return durationS<=0 ? 0 : M.fclamp(1-elapsedS/durationS, 0, 1); 20 | 21 | 22 | public inline function new() { 23 | recycle(); 24 | } 25 | 26 | public inline function recycle() { 27 | id = CA_Unknown; 28 | durationS = 0; 29 | elapsedS = 0; 30 | onComplete = _doNothing; 31 | onProgress = _doNothing; 32 | } 33 | 34 | public inline function resetProgress() { 35 | elapsedS = 0; 36 | onProgress(this); 37 | } 38 | 39 | public inline function reduceProgressS(lossS:Float) { 40 | elapsedS = M.fmax(0, elapsedS-lossS); 41 | onProgress(this); 42 | } 43 | 44 | public inline function isComplete() { 45 | return elapsedS>=durationS; 46 | } 47 | 48 | function _doNothing(a:ChargedAction) {} 49 | 50 | 51 | /** Update progress and return TRUE if completed **/ 52 | public inline function update(tmod:Float) { 53 | elapsedS = M.fmin( elapsedS + tmod/Const.FPS, durationS ); 54 | onProgress(this); 55 | if( isComplete() ) { 56 | onComplete(this); 57 | if( isComplete() ) 58 | recycle(); // breaks possibles mem refs, for GC 59 | return true; 60 | } 61 | else 62 | return false; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/game/tools/GameChildProcess.hx: -------------------------------------------------------------------------------- 1 | package tools; 2 | 3 | class GameChildProcess extends dn.Process { 4 | public var app(get,never) : App; inline function get_app() return App.ME; 5 | public var game(get,never) : Game; inline function get_game() return Game.ME; 6 | public var fx(get,never) : Fx; inline function get_fx() return Game.exists() ? Game.ME.fx : null; 7 | public var level(get,never) : Level; inline function get_level() return Game.exists() ? Game.ME.level : null; 8 | 9 | public function new() { 10 | super(Game.ME); 11 | } 12 | } -------------------------------------------------------------------------------- /src/game/tools/LPoint.hx: -------------------------------------------------------------------------------- 1 | package tools; 2 | 3 | class LPoint { 4 | /** Grid based X **/ 5 | public var cx : Int; 6 | 7 | /** Grid based Y **/ 8 | public var cy : Int; 9 | 10 | /** X-ratio (0-1) in current grid cell **/ 11 | public var xr : Float; 12 | 13 | /** Y-ratio (0-1) in current grid cell **/ 14 | public var yr : Float; 15 | 16 | 17 | 18 | /** Grid based X, including sub grid cell ratio **/ 19 | public var cxf(get,never) : Float; 20 | inline function get_cxf() return cx+xr; 21 | 22 | /** Grid based Y, including sub grid cell ratio **/ 23 | public var cyf(get,never) : Float; 24 | inline function get_cyf() return cy+yr; 25 | 26 | 27 | 28 | /** Level X pixel coord **/ 29 | public var levelX(get,set) : Float; 30 | inline function get_levelX() return (cx+xr)*Const.GRID; 31 | inline function set_levelX(v:Float) { 32 | setLevelPixelX(v); 33 | return levelX; 34 | } 35 | 36 | /** Level Y pixel coord **/ 37 | public var levelY(get,set) : Float; 38 | inline function get_levelY() return (cy+yr)*Const.GRID; 39 | inline function set_levelY(v:Float) { 40 | setLevelPixelY(v); 41 | return levelY; 42 | } 43 | 44 | 45 | 46 | /** Level X pixel coord (as Integer) **/ 47 | public var levelXi(get,never) : Int; 48 | inline function get_levelXi() return Std.int(levelX); 49 | 50 | /** Level Y pixel coord **/ 51 | public var levelYi(get,never) : Int; 52 | inline function get_levelYi() return Std.int(levelY); 53 | 54 | 55 | 56 | /** Screen X pixel coord **/ 57 | public var screenX(get,never) : Float; 58 | inline function get_screenX() { 59 | return !Game.exists() ? -1. : levelX*Const.SCALE + Game.ME.scroller.x; 60 | } 61 | 62 | /** Screen Y pixel coord **/ 63 | public var screenY(get,never) : Float; 64 | inline function get_screenY() { 65 | return !Game.exists() ? -1. : levelY*Const.SCALE + Game.ME.scroller.y; 66 | } 67 | 68 | 69 | 70 | private inline function new() { 71 | cx = cy = 0; 72 | xr = yr = 0; 73 | } 74 | 75 | @:keep 76 | public function toString() : String { 77 | return 'LPoint<${M.pretty(cxf)},${M.pretty(cyf)} / $levelXi,$levelYi>'; 78 | } 79 | 80 | public static inline function fromCase(cx:Float, cy:Float) { 81 | return new LPoint().setLevelCase( Std.int(cx), Std.int(cy), cx%1, cy%1 ); 82 | } 83 | 84 | public static inline function fromCaseCenter(cx:Int, cy:Int) { 85 | return new LPoint().setLevelCase(cx, cy, 0.5, 0.5); 86 | } 87 | 88 | public static inline function fromPixels(x:Float, y:Float) { 89 | return new LPoint().setLevelPixel(x,y); 90 | } 91 | 92 | public static inline function fromScreen(sx:Float, sy:Float) { 93 | return new LPoint().setScreen(sx,sy); 94 | } 95 | 96 | /** Init using level grid coords **/ 97 | public inline function setLevelCase(x, y, xr=0.5, yr=0.5) { 98 | this.cx = x; 99 | this.cy = y; 100 | this.xr = xr; 101 | this.yr = yr; 102 | return this; 103 | } 104 | 105 | 106 | /** Set this point using another LPoint **/ 107 | public inline function usePoint(other:LPoint) { 108 | cx = other.cx; 109 | cy = other.cy; 110 | xr = other.xr; 111 | yr = other.yr; 112 | } 113 | 114 | /** Init from screen coord **/ 115 | public inline function setScreen(sx:Float, sy:Float) { 116 | setLevelPixel( 117 | ( sx - Game.ME.scroller.x ) / Const.SCALE, 118 | ( sy - Game.ME.scroller.y ) / Const.SCALE 119 | ); 120 | return this; 121 | } 122 | 123 | /** Init using level pixels coords **/ 124 | public inline function setLevelPixel(x:Float,y:Float) { 125 | setLevelPixelX(x); 126 | setLevelPixelY(y); 127 | return this; 128 | } 129 | 130 | inline function setLevelPixelX(x:Float) { 131 | cx = Std.int(x/Const.GRID); 132 | this.xr = ( x % Const.GRID ) / Const.GRID; 133 | return this; 134 | } 135 | 136 | inline function setLevelPixelY(y:Float) { 137 | cy = Std.int(y/Const.GRID); 138 | this.yr = ( y % Const.GRID ) / Const.GRID; 139 | return this; 140 | } 141 | 142 | /** Return distance to something else, in grid unit **/ 143 | public inline function distCase(?e:Entity, ?pt:LPoint, tcx=0, tcy=0, txr=0.5, tyr=0.5) { 144 | if( e!=null ) 145 | return M.dist(this.cx+this.xr, this.cy+this.yr, e.cx+e.xr, e.cy+e.yr); 146 | else if( pt!=null ) 147 | return M.dist(this.cx+this.xr, this.cy+this.yr, pt.cx+pt.xr, pt.cy+pt.yr); 148 | else 149 | return M.dist(this.cx+this.xr, this.cy+this.yr, tcx+txr, tcy+tyr); 150 | } 151 | 152 | /** Distance to something else, in level pixels **/ 153 | public inline function distPx(?e:Entity, ?pt:LPoint, lvlX=0., lvlY=0.) { 154 | if( e!=null ) 155 | return M.dist(levelX, levelY, e.attachX, e.attachY); 156 | else if( pt!=null ) 157 | return M.dist(levelX, levelY, pt.levelX, pt.levelY); 158 | else 159 | return M.dist(levelX, levelY, lvlX, lvlY); 160 | } 161 | 162 | /** Angle in radians to something else, in level pixels **/ 163 | public inline function angTo(?e:Entity, ?pt:LPoint, lvlX=0., lvlY=0.) { 164 | if( e!=null ) 165 | return Math.atan2((e.cy+e.yr)-cyf, (e.cx+e.xr)-cxf ); 166 | else if( pt!=null ) 167 | return Math.atan2(pt.cyf-cyf, pt.cxf-cxf); 168 | else 169 | return Math.atan2(lvlY-levelY, lvlX-levelX); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/game/tools/LRect.hx: -------------------------------------------------------------------------------- 1 | package tools; 2 | 3 | class LRect { 4 | var topLeft : LPoint; 5 | var bottomRight : LPoint; 6 | 7 | /** Pixel based left coordinate **/ 8 | public var pxLeft(get,set) : Int; 9 | /** Pixel based top coordinate **/ 10 | public var pxTop(get,set) : Int; 11 | /** Pixel based right coordinate **/ 12 | public var pxRight(get,set) : Int; 13 | /** Pixel based bottom coordinate **/ 14 | public var pxBottom(get,set) : Int; 15 | /** Pixel based width **/ 16 | public var pxWid(get,set) : Int; 17 | /** Pixel based height **/ 18 | public var pxHei(get,set) : Int; 19 | 20 | /** Grid based left coordinate **/ 21 | public var cLeft(get,set) : Int; 22 | /** Grid based top coordinate **/ 23 | public var cTop(get,set) : Int; 24 | /** Grid based right coordinate **/ 25 | public var cRight(get,set) : Int; 26 | /** Grid based bottom coordinate **/ 27 | public var cBottom(get,set) : Int; 28 | /** Grid based width **/ 29 | public var cWid(get,set) : Int; 30 | /** Grid based height **/ 31 | public var cHei(get,set) : Int; 32 | 33 | private inline function new() { 34 | topLeft = LPoint.fromPixels(0,0); 35 | bottomRight = LPoint.fromPixels(0,0); 36 | } 37 | 38 | @:keep 39 | public function toString() : String { 40 | return 'LRect'; 41 | } 42 | 43 | /** 44 | Create a LRect using pixel coordinates and dimensions. 45 | **/ 46 | public static inline function fromPixels(x:Int, y:Int, w:Int, h:Int) { 47 | var r = new LRect(); 48 | r.topLeft.setLevelPixel(x,y); 49 | r.bottomRight.setLevelPixel(x+M.iabs(w)-1, y+M.iabs(h)-1); 50 | return r; 51 | } 52 | 53 | 54 | /** 55 | Create a LRect using grid-based coordinates and dimensions. 56 | **/ 57 | public static inline function fromCase(cx:Int, cy:Int, w:Int, h:Int) { 58 | var r = new LRect(); 59 | r.topLeft.setLevelCase(cx,cy, 0,0); 60 | r.bottomRight.setLevelCase(cx+M.iabs(w)-1, cy+M.iabs(h)-1, 0.999, 0.999); 61 | return r; 62 | } 63 | 64 | 65 | /** Swap coordinates if needed **/ 66 | inline function normalize() { 67 | if( topLeft.levelX > bottomRight.levelX ) { 68 | var swp = topLeft.levelX; 69 | topLeft.levelX = bottomRight.levelX; 70 | bottomRight.levelX = swp; 71 | } 72 | 73 | if( topLeft.levelY > bottomRight.levelY ) { 74 | var swp = topLeft.levelY; 75 | topLeft.levelY = bottomRight.levelY; 76 | bottomRight.levelY = swp; 77 | } 78 | } 79 | 80 | 81 | 82 | inline function get_pxLeft() return topLeft.levelXi; 83 | inline function set_pxLeft(v:Int) { topLeft.levelX = v; normalize(); return v; } 84 | 85 | inline function get_pxTop() return topLeft.levelYi; 86 | inline function set_pxTop(v:Int) { topLeft.levelY = v; normalize(); return v; } 87 | 88 | inline function get_pxBottom() return bottomRight.levelYi; 89 | inline function set_pxBottom(v:Int) { bottomRight.levelY = v; normalize(); return v; } 90 | 91 | inline function get_pxRight() return bottomRight.levelXi; 92 | inline function set_pxRight(v:Int) { bottomRight.levelX = v; normalize(); return v; } 93 | 94 | inline function get_pxWid() return bottomRight.levelXi - topLeft.levelXi + 1; 95 | inline function set_pxWid(v) { bottomRight.levelX = topLeft.levelXi + v; normalize(); return v; } 96 | 97 | inline function get_pxHei() return bottomRight.levelYi - topLeft.levelYi + 1; 98 | inline function set_pxHei(v) { bottomRight.levelY = topLeft.levelYi + v; normalize(); return v; } 99 | 100 | 101 | 102 | inline function get_cLeft() return topLeft.cx; 103 | inline function set_cLeft(v:Int) { topLeft.cx = v; topLeft.xr = 0; normalize(); return v; } 104 | 105 | inline function get_cTop() return topLeft.cy; 106 | inline function set_cTop(v:Int) { topLeft.cy = v; topLeft.yr = 0; normalize(); return v; } 107 | 108 | inline function get_cRight() return bottomRight.cx; 109 | inline function set_cRight(v:Int) { bottomRight.cx = v; bottomRight.xr = 0.999; normalize(); return v; } 110 | 111 | inline function get_cBottom() return bottomRight.cy; 112 | inline function set_cBottom(v:Int) { bottomRight.cy = v; bottomRight.yr = 0.999; normalize(); return v; } 113 | 114 | inline function get_cWid() return bottomRight.cx - topLeft.cx + 1; 115 | inline function set_cWid(v:Int) { bottomRight.cx = topLeft.cx + v-1; bottomRight.xr = 0.999; normalize(); return v; } 116 | 117 | inline function get_cHei() return bottomRight.cy - topLeft.cy + 1; 118 | inline function set_cHei(v:Int) { bottomRight.cy = topLeft.cy + v-1; bottomRight.yr = 0.999; normalize(); return v; } 119 | } 120 | -------------------------------------------------------------------------------- /src/game/tools/script/Api.hx: -------------------------------------------------------------------------------- 1 | package tools.script; 2 | 3 | /** 4 | Everything in this class will be available in HScript execution context. 5 | **/ 6 | @:keep 7 | class Api { 8 | public var levelWid(get,never) : Int; inline function get_levelWid() return Game.ME.level.pxWid; 9 | public var levelHei(get,never) : Int; inline function get_levelHei() return Game.ME.level.pxHei; 10 | 11 | public function new() {} 12 | } -------------------------------------------------------------------------------- /src/game/tools/script/Script.hx: -------------------------------------------------------------------------------- 1 | package tools.script; 2 | 3 | class Script { 4 | public static var log : dn.Log; 5 | public static var parser : hscript.Parser; 6 | 7 | /** 8 | Execute provided hscript. 9 | USAGE: 10 | Script.run('var a=1 ; a++ ; log(a) ; return a'); 11 | **/ 12 | public static function run(script:String) { 13 | // Init script 14 | init(); 15 | log.clear(); 16 | log.add("exec", "Script started."); 17 | 18 | // API 19 | var interp = new hscript.Interp(); 20 | interp.variables.set("api", new tools.script.Api()); 21 | interp.variables.set("log", (v:Dynamic)->log.add("run", Std.string(v))); 22 | 23 | // Execute 24 | var program = parser.parseString(script); 25 | var out : Dynamic = try interp.execute(program) 26 | catch( e:hscript.Expr.Error ) { 27 | log.error( Std.string(e) ); 28 | null; 29 | } 30 | 31 | // Returned value 32 | if( out!=null ) 33 | log.add("exec", "Returned: "+out); 34 | 35 | if( log.containsAnyCriticalEntry() ) { 36 | // Error 37 | printLastLog(); 38 | return false; 39 | } 40 | else { 41 | // Done! 42 | log.add("exec", "Script completed."); 43 | return true; 44 | } 45 | } 46 | 47 | 48 | /** 49 | Print last script log to default output 50 | **/ 51 | public static function printLastLog() { 52 | log.printAll(); 53 | } 54 | 55 | 56 | static var initDone = false; 57 | static function init() { 58 | if( initDone ) 59 | return; 60 | initDone = true; 61 | 62 | parser = new hscript.Parser(); 63 | 64 | log = new dn.Log(); 65 | log.outputConsole = Console.ME; 66 | log.tagColors.set("error", "#ff6c6c"); 67 | log.tagColors.set("exec", "#a1b2db"); 68 | log.tagColors.set("run", "#3affe5"); 69 | } 70 | } -------------------------------------------------------------------------------- /src/game/ui/Bar.hx: -------------------------------------------------------------------------------- 1 | package ui; 2 | 3 | class Bar extends h2d.Object { 4 | var cd : dn.Cooldown; 5 | var bg : h2d.ScaleGrid; 6 | var bar : h2d.ScaleGrid; 7 | var oldBar : Null; 8 | 9 | public var innerBarMaxWidth(get,never) : Float; 10 | public var innerBarHeight(get,never) : Float; 11 | public var outerWidth(get,never) : Float; 12 | public var outerHeight(get,never) : Float; 13 | public var color(default,set) : Col; 14 | public var defaultColor(default,null) : Col; 15 | var padding : Int; 16 | var oldBarSpeed : Float = 1.; 17 | 18 | var blinkColor : h3d.Vector; 19 | var gradTg : Null; 20 | 21 | var curValue : Float; 22 | var curMax : Float; 23 | 24 | public function new(wid:Int, hei:Int, c:Col, ?p:h2d.Object) { 25 | super(p); 26 | 27 | curValue = 0; 28 | curMax = 1; 29 | cd = new dn.Cooldown(Const.FPS); 30 | 31 | bg = new h2d.ScaleGrid( Assets.tiles.getTile(D.tiles.uiBarBg), 2, 2, this ); 32 | bg.colorAdd = blinkColor = new h3d.Vector(); 33 | 34 | bar = new h2d.ScaleGrid( Assets.tiles.getTile(D.tiles.uiBar), 1,1, this ); 35 | 36 | setSize(wid,hei,1); 37 | defaultColor = color = c; 38 | } 39 | 40 | public function enableOldValue(oldBarColor:Col, speed=1.0) { 41 | if( oldBar!=null ) 42 | oldBar.remove(); 43 | oldBar = new h2d.ScaleGrid( h2d.Tile.fromColor(oldBarColor,3,3), 1, 1 ); 44 | this.addChildAt( oldBar, this.getChildIndex(bar) ); 45 | oldBar.height = bar.height; 46 | oldBar.width = 0; 47 | oldBar.setPosition(padding,padding); 48 | 49 | oldBarSpeed = speed; 50 | } 51 | 52 | public function setGraduationPx(step:Int, alpha=0.5) { 53 | if( step<=1 ) 54 | throw "Invalid bar graduation "+step; 55 | 56 | if( gradTg!=null ) 57 | gradTg.remove(); 58 | 59 | gradTg = new h2d.TileGroup(Assets.tiles.tile, this); 60 | gradTg.colorAdd = blinkColor; 61 | gradTg.setDefaultColor(0x0, alpha); 62 | 63 | var x = step-1; 64 | var t = Assets.tiles.getTile(D.tiles.pixel); 65 | while( xbar.width) { 114 | cd.setS("oldMaintain",0.06); 115 | oldBar.width = oldWidth; 116 | } 117 | } 118 | 119 | function renderBar() { 120 | bar.visible = curValue>0; 121 | bar.width = innerBarMaxWidth * (curValue/curMax); 122 | } 123 | 124 | public function skipOldValueBar() { 125 | if( oldBar!=null ) 126 | oldBar.width = 0; 127 | } 128 | 129 | public function blink(c:Col=0, a=1.0) { 130 | blinkColor.setColor( (c==0 ? color : c).withAlpha(a) ); 131 | cd.setS("blinkMaintain", 0.15 * 1/oldBarSpeed); 132 | } 133 | 134 | override function sync(ctx:h2d.RenderContext) { 135 | var tmod = Game.ME.tmod; 136 | cd.update(tmod); 137 | 138 | // Decrease oldValue bar 139 | if( oldBar!=null ) { 140 | if( !cd.has("oldMaintain") ) 141 | oldBar.width = M.fmax(0, oldBar.width - oldBarSpeed*2*tmod); 142 | oldBar.visible = oldBar.width>0; 143 | } 144 | 145 | // Blink fade 146 | if( !cd.has("blinkMaintain") ) { 147 | blinkColor.r*=Math.pow(0.60, tmod); 148 | blinkColor.g*=Math.pow(0.55, tmod); 149 | blinkColor.b*=Math.pow(0.50, tmod); 150 | } 151 | 152 | super.sync(ctx); 153 | } 154 | } -------------------------------------------------------------------------------- /src/game/ui/Console.hx: -------------------------------------------------------------------------------- 1 | package ui; 2 | 3 | enum abstract ConsoleFlag(Int) to Int from Int { 4 | var F_Camera; 5 | var F_CameraScrolling; 6 | var F_Bounds; 7 | var F_Affects; 8 | } 9 | 10 | class Console extends h2d.Console { 11 | public static var ME : Console; 12 | #if debug 13 | var flags : Map; 14 | var allFlags : Array<{ name:String, value:Int }> = []; 15 | #end 16 | 17 | var stats : Null; 18 | 19 | public function new(f:h2d.Font, p:h2d.Object) { 20 | super(f, p); 21 | 22 | logTxt.filter = new dn.heaps.filter.PixelOutline(); 23 | scale(2); // TODO smarter scaling for 4k screens 24 | logTxt.condenseWhite = false; 25 | errorColor = 0xff6666; 26 | 27 | // Settings 28 | ME = this; 29 | h2d.Console.HIDE_LOG_TIMEOUT = #if debug 60 #else 5 #end; 30 | Lib.redirectTracesToH2dConsole(this); 31 | 32 | #if debug 33 | // Debug console flags 34 | flags = new Map(); 35 | allFlags = dn.MacroTools.getAbstractEnumValues(ConsoleFlag); 36 | allFlags.sort( (a,b)->Reflect.compare(a.name, b.name) ); 37 | this.addCommand("flags", "Open the console flags window", [], function() { 38 | this.hide(); 39 | var w = new ui.win.SimpleMenu(); 40 | w.verticalAlign = End; 41 | w.addButton("Disable all", false, ()->{ 42 | for(f in allFlags) 43 | if( hasFlag(f.value) ) 44 | setFlag(f.value, false); 45 | }); 46 | for(f in allFlags) 47 | w.addCheckBox(f.name.substr(2), ()->hasFlag(f.value), v->setFlag(f.value,v)); 48 | }); 49 | this.addAlias("f","flags"); 50 | this.addAlias("flag","flags"); 51 | 52 | // List all console flags 53 | this.addCommand("list", [], function() { 54 | for(f in allFlags) 55 | log( (hasFlag(f.value) ? "+" : "-")+f.name, hasFlag(f.value)?0x80ff00:0xff8888 ); 56 | }); 57 | 58 | // Controller debugger 59 | this.addCommand("ctrl", [], ()->{ 60 | App.ME.ca.toggleDebugger(App.ME, dbg->{ 61 | dbg.root.filter = new dn.heaps.filter.PixelOutline(); 62 | }); 63 | }); 64 | 65 | // Garbage collector 66 | this.addCommand("gc", [{ name:"state", t:AInt, opt:true }], (?state:Int)->{ 67 | if( !dn.Gc.isSupported() ) 68 | log("GC is not supported on this platform", Red); 69 | else { 70 | if( state!=null ) 71 | dn.Gc.setState(state!=0); 72 | dn.Gc.runNow(); 73 | log("GC forced (current state: "+(dn.Gc.isActive() ? "active" : "inactive" )+")", dn.Gc.isActive()?Green:Yellow); 74 | } 75 | }); 76 | 77 | // Level marks 78 | var allLevelMarks : Array<{ name:String, value:Int }>; 79 | allLevelMarks = dn.MacroTools.getAbstractEnumValues(Types.LevelMark); 80 | this.addCommand( 81 | "mark", 82 | [ 83 | { name:"levelMark", t:AEnum( allLevelMarks.map(m->m.name) ), opt:true }, 84 | { name:"bit", t:AInt, opt:true }, 85 | ], 86 | (k:String, bit:Null)->{ 87 | if( !Game.exists() ) { 88 | error('Game is not running'); 89 | return; 90 | } 91 | if( k==null ) { 92 | // Game.ME.level.clearDebug(); 93 | return; 94 | } 95 | 96 | var bit : Null = cast bit; 97 | var mark = -1; 98 | for(m in allLevelMarks) 99 | if( m.name==k ) { 100 | mark = m.value; 101 | break; 102 | } 103 | if( mark<0 ) { 104 | error('Unknown level mark $k'); 105 | return; 106 | } 107 | 108 | var col = 0xffcc00; 109 | log('Displaying $mark (bit=$bit)...', col); 110 | // Game.ME.level.renderDebugMark(cast mark, bit); 111 | } 112 | ); 113 | this.addAlias("m","mark"); 114 | #end 115 | 116 | // List all active dn.Process 117 | this.addCommand("process", [], ()->{ 118 | for( l in App.ME.rprintChildren().split("\n") ) 119 | log(l); 120 | }); 121 | this.addAlias("p", "process"); 122 | 123 | // Show build info 124 | this.addCommand("build", [], ()->log( Const.BUILD_INFO ) ); 125 | 126 | // Create a debug drone 127 | #if debug 128 | this.addCommand("drone", [], ()->{ 129 | new en.DebugDrone(); 130 | }); 131 | #end 132 | 133 | // Create a stats box 134 | this.addCommand("fps", [], ()->toggleStats()); 135 | this.addAlias("stats","fps"); 136 | 137 | // All flag aliases 138 | #if debug 139 | for(f in allFlags) 140 | addCommand(f.name.substr(2), [], ()->{ 141 | setFlag(f.value, !hasFlag(f.value)); 142 | }); 143 | #end 144 | } 145 | 146 | public function disableStats() { 147 | if( stats!=null ) { 148 | stats.destroy(); 149 | stats = null; 150 | } 151 | } 152 | 153 | public function enableStats() { 154 | disableStats(); 155 | stats = new dn.heaps.StatsBox(App.ME); 156 | stats.addFpsChart(); 157 | stats.addDrawCallsChart(); 158 | #if hl 159 | stats.addMemoryChart(); 160 | #end 161 | } 162 | 163 | public function toggleStats() { 164 | if( stats!=null ) 165 | disableStats(); 166 | else 167 | enableStats(); 168 | } 169 | 170 | override function getCommandSuggestion(cmd:String):String { 171 | var sugg = super.getCommandSuggestion(cmd); 172 | if( sugg.length>0 ) 173 | return sugg; 174 | 175 | if( cmd.length==0 ) 176 | return ""; 177 | 178 | // Simplistic argument auto-complete 179 | for(c in commands.keys()) { 180 | var reg = new EReg("([ \t\\/]*"+c+"[ \t]+)(.*)", "gi"); 181 | if( reg.match(cmd) ) { 182 | var lowArg = reg.matched(2).toLowerCase(); 183 | for(a in commands.get(c).args) 184 | switch a.t { 185 | case AInt: 186 | case AArray(_): 187 | case AFloat: 188 | case AString: 189 | case ABool: 190 | case AEnum(values): 191 | for(v in values) 192 | if( v.toLowerCase().indexOf(lowArg)==0 ) 193 | return reg.matched(1) + v; 194 | } 195 | } 196 | } 197 | 198 | return ""; 199 | } 200 | 201 | /** Creates a shortcut command "/flag" to toggle specified flag state **/ 202 | // inline function addFlagCommandAlias(flag:ConsoleFlag) { 203 | // #if debug 204 | // var str = Std.string(flag); 205 | // for(f in allFlags) 206 | // if( f.value==flag ) { 207 | // str = f.name; 208 | // break; 209 | // } 210 | // addCommand(str, [], ()->{ 211 | // setFlag(flag, !hasFlag(flag)); 212 | // }); 213 | // #end 214 | // } 215 | 216 | override function handleCommand(command:String) { 217 | var flagReg = ~/[\/ \t]*\+[ \t]*([\w]+)/g; // cleanup missing spaces 218 | super.handleCommand( flagReg.replace(command, "/+ $1") ); 219 | } 220 | 221 | public function error(msg:Dynamic) { 222 | log("[ERROR] "+Std.string(msg), errorColor); 223 | h2d.Console.HIDE_LOG_TIMEOUT = Const.INFINITE; 224 | } 225 | 226 | #if debug 227 | public function setFlag(f:ConsoleFlag, v:Bool) { 228 | var hadBefore = hasFlag(f); 229 | 230 | if( v ) 231 | flags.set(f,v); 232 | else 233 | flags.remove(f); 234 | 235 | if( v && !hadBefore || !v && hadBefore ) 236 | onFlagChange(f,v); 237 | return v; 238 | } 239 | public function hasFlag(f:ConsoleFlag) return flags.get(f)==true; 240 | #else 241 | public inline function hasFlag(f:ConsoleFlag) return false; 242 | #end 243 | 244 | public function onFlagChange(f:ConsoleFlag, v:Bool) {} 245 | 246 | 247 | override function log(text:String, ?color:Int) { 248 | if( !App.ME.screenshotMode ) 249 | super.log(text, color); 250 | } 251 | 252 | public inline function clearAndLog(str:Dynamic) { 253 | runCommand("cls"); 254 | log( Std.string(str) ); 255 | } 256 | } -------------------------------------------------------------------------------- /src/game/ui/Hud.hx: -------------------------------------------------------------------------------- 1 | package ui; 2 | 3 | class Hud extends GameChildProcess { 4 | var flow : h2d.Flow; 5 | var invalidated = true; 6 | var notifications : Array = []; 7 | var notifTw : dn.Tweenie; 8 | 9 | var debugText : h2d.Text; 10 | 11 | public function new() { 12 | super(); 13 | 14 | notifTw = new Tweenie(Const.FPS); 15 | 16 | createRootInLayers(game.root, Const.DP_UI); 17 | root.filter = new h2d.filter.Nothing(); // force pixel perfect rendering 18 | 19 | flow = new h2d.Flow(root); 20 | notifications = []; 21 | 22 | debugText = new h2d.Text(Assets.fontPixel, root); 23 | debugText.filter = new dn.heaps.filter.PixelOutline(); 24 | clearDebug(); 25 | } 26 | 27 | override function onResize() { 28 | super.onResize(); 29 | root.setScale(Const.UI_SCALE); 30 | } 31 | 32 | /** Clear debug printing **/ 33 | public inline function clearDebug() { 34 | debugText.text = ""; 35 | debugText.visible = false; 36 | } 37 | 38 | /** Display a debug string **/ 39 | public inline function debug(v:Dynamic, clear=true) { 40 | if( clear ) 41 | debugText.text = Std.string(v); 42 | else 43 | debugText.text += "\n"+v; 44 | debugText.visible = true; 45 | debugText.x = Std.int( stageWid/Const.UI_SCALE - 4 - debugText.textWidth ); 46 | } 47 | 48 | 49 | /** Pop a quick s in the corner **/ 50 | public function notify(str:String, color:Col=0x0) { 51 | // Bg 52 | var t = Assets.tiles.getTile( D.tiles.uiNotification ); 53 | var f = new dn.heaps.FlowBg(t, 5, root); 54 | f.colorizeBg(color); 55 | f.paddingHorizontal = 6; 56 | f.paddingBottom = 4; 57 | f.paddingTop = 0; 58 | f.paddingLeft = 9; 59 | f.y = 4; 60 | 61 | // Text 62 | var tf = new h2d.Text(Assets.fontPixel, f); 63 | tf.text = str; 64 | tf.maxWidth = 0.6 * stageWid/Const.UI_SCALE; 65 | tf.textColor = 0xffffff; 66 | tf.filter = new dn.heaps.filter.PixelOutline( color.toBlack(0.2) ); 67 | 68 | // Notification lifetime 69 | var durationS = 2 + str.length*0.04; 70 | var p = createChildProcess(); 71 | notifications.insert(0,f); 72 | p.tw.createS(f.x, -f.outerWidth>-2, TEaseOut, 0.1); 73 | p.onUpdateCb = ()->{ 74 | if( p.stime>=durationS && !p.cd.hasSetS("done",Const.INFINITE) ) 75 | p.tw.createS(f.x, -f.outerWidth, 0.2).end( p.destroy ); 76 | } 77 | p.onDisposeCb = ()->{ 78 | notifications.remove(f); 79 | f.remove(); 80 | } 81 | 82 | // Move existing notifications 83 | var y = 4; 84 | for(f in notifications) { 85 | notifTw.terminateWithoutCallbacks(f.y); 86 | notifTw.createS(f.y, y, TEaseOut, 0.2); 87 | y+=f.outerHeight+1; 88 | } 89 | 90 | } 91 | 92 | public inline function invalidate() invalidated = true; 93 | 94 | function render() {} 95 | 96 | public function onLevelStart() {} 97 | 98 | override function preUpdate() { 99 | super.preUpdate(); 100 | notifTw.update(tmod); 101 | } 102 | 103 | override function postUpdate() { 104 | super.postUpdate(); 105 | 106 | if( invalidated ) { 107 | invalidated = false; 108 | render(); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/game/ui/IconBar.hx: -------------------------------------------------------------------------------- 1 | package ui; 2 | 3 | class IconBar extends h2d.TileGroup { 4 | var curX = 0; 5 | public var width(default,null) = 0; 6 | public var height(default,null) = 0; 7 | public var overlapPx = 2; 8 | 9 | public function new(?p) { 10 | super(Assets.tiles.tile, p); 11 | } 12 | 13 | public inline function empty() { 14 | clear(); 15 | curX = 0; 16 | } 17 | 18 | public function addIcons(iconId:String, n=1) { 19 | for(i in 0...n) { 20 | var t = Assets.tiles.getTile(iconId); 21 | add(curX, 0, t); 22 | width = curX + t.iwidth; 23 | height = M.imax(height, t.iheight); 24 | curX += t.iwidth-overlapPx; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/game/ui/UiComponent.hx: -------------------------------------------------------------------------------- 1 | package ui; 2 | 3 | class UiComponent extends h2d.Flow { 4 | var _tmpPt : h2d.col.Point; 5 | 6 | public var uid(default,null) : Int; 7 | 8 | public var globalLeft(get,never) : Float; 9 | public var globalRight(get,never) : Float; 10 | public var globalTop(get,never) : Float; 11 | public var globalBottom(get,never) : Float; 12 | 13 | public var globalWidth(get,never) : Int; 14 | public var globalHeight(get,never) : Int; 15 | 16 | public var globalCenterX(get,never) : Float; 17 | public var globalCenterY(get,never) : Float; 18 | 19 | 20 | public function new(?p:h2d.Object) { 21 | super(p); 22 | uid = Const.makeUniqueId(); 23 | _tmpPt = new h2d.col.Point(); 24 | } 25 | 26 | @:keep override function toString() { 27 | return super.toString()+".UiComponent"; 28 | } 29 | 30 | public final function use() { 31 | onUse(); 32 | onUseCb(); 33 | } 34 | function onUse() {} 35 | public dynamic function onUseCb() {} 36 | 37 | @:allow(ui.UiGroupController) 38 | function onFocus() { 39 | filter = new dn.heaps.filter.Invert(); 40 | } 41 | 42 | @:allow(ui.UiGroupController) 43 | function onBlur() { 44 | filter = null; 45 | } 46 | 47 | 48 | 49 | function get_globalLeft() { 50 | _tmpPt.set(); 51 | localToGlobal(_tmpPt); 52 | return _tmpPt.x; 53 | } 54 | 55 | function get_globalRight() { 56 | _tmpPt.set(outerWidth, outerHeight); 57 | localToGlobal(_tmpPt); 58 | return _tmpPt.x; 59 | } 60 | 61 | function get_globalTop() { 62 | _tmpPt.set(); 63 | localToGlobal(_tmpPt); 64 | return _tmpPt.y; 65 | } 66 | 67 | function get_globalBottom() { 68 | _tmpPt.set(outerWidth, outerHeight); 69 | localToGlobal(_tmpPt); 70 | return _tmpPt.y; 71 | } 72 | 73 | inline function get_globalWidth() return Std.int( globalRight - globalLeft ); 74 | inline function get_globalHeight() return Std.int( globalBottom - globalTop ); 75 | inline function get_globalCenterX() return ( globalLeft + globalRight ) * 0.5; 76 | inline function get_globalCenterY() return ( globalTop + globalBottom ) * 0.5; 77 | 78 | 79 | public function getRelativeX(relativeTo:h2d.Object) { 80 | _tmpPt.set(); 81 | localToGlobal(_tmpPt); 82 | relativeTo.globalToLocal(_tmpPt); 83 | return _tmpPt.x; 84 | } 85 | 86 | public function getRelativeY(relativeTo:h2d.Object) { 87 | _tmpPt.set(); 88 | localToGlobal(_tmpPt); 89 | relativeTo.globalToLocal(_tmpPt); 90 | return _tmpPt.y; 91 | } 92 | 93 | public inline function globalAngTo(to:UiComponent) { 94 | return Math.atan2(to.globalCenterY-globalCenterY, to.globalCenterX-globalCenterX); 95 | } 96 | 97 | public inline function globalDistTo(to:UiComponent) { 98 | return M.dist(globalCenterX, globalCenterY, to.globalCenterX, to.globalCenterY); 99 | } 100 | 101 | public function overlapsRect(x:Float, y:Float, w:Int, h:Int) { 102 | return dn.geom.Geom.rectOverlapsRect( 103 | globalLeft, globalTop, globalWidth, globalHeight, 104 | x, y, w, h 105 | ); 106 | } 107 | 108 | override function sync(ctx:h2d.RenderContext) { 109 | super.sync(ctx); 110 | update(); 111 | } 112 | 113 | function update() {} 114 | } -------------------------------------------------------------------------------- /src/game/ui/UiGroupController.hx: -------------------------------------------------------------------------------- 1 | package ui; 2 | 3 | enum abstract GroupDir(Int) { 4 | var North; 5 | var East; 6 | var South; 7 | var West; 8 | } 9 | 10 | /** 11 | This process takes care of interactions with a group of UiComponents. 12 | This includes: 13 | - user interaction with a component, 14 | - focus/blur of a component, 15 | - supports gamepad, keyboard and mouse. 16 | 17 | USAGE: 18 | - Add some UiComponents to your scene, 19 | - Create a UiGroupController instance, 20 | - Register all these UiComponents in the UiGroupController. 21 | **/ 22 | class UiGroupController extends dn.Process { 23 | var uid : Int; 24 | var ca : ControllerAccess; 25 | public var currentComp(default,null) : Null; 26 | 27 | var components : Array = []; 28 | 29 | var connectionsNeedRebuild = false; 30 | var uiGroupsConnections : Map = new Map(); 31 | var componentsConnections : Map> = new Map(); 32 | 33 | var groupFocused = true; 34 | var useMouse : Bool; 35 | 36 | 37 | public function new(parentProcess:dn.Process, useMouse=true) { 38 | super(parentProcess); 39 | 40 | this.useMouse = useMouse; 41 | 42 | uid = Const.makeUniqueId(); 43 | ca = App.ME.controller.createAccess(); 44 | ca.lockCondition = ()->!groupFocused || customControllerLock(); 45 | ca.lock(0.1); 46 | } 47 | 48 | 49 | public dynamic function customControllerLock() return false; 50 | 51 | 52 | public function registerComponent(comp:ui.UiComponent) { 53 | components.push(comp); 54 | comp.onAfterReflow = invalidateConnections; // TODO not a reliable solution 55 | 56 | if( useMouse ) { 57 | comp.enableInteractive = true; 58 | comp.interactive.cursor = Button; 59 | 60 | comp.interactive.onOver = _->{ 61 | focusComponent(comp); 62 | focusGroup(); 63 | } 64 | 65 | comp.interactive.onOut = _->{ 66 | blurComponent(comp); 67 | } 68 | 69 | comp.interactive.onClick = ev->{ 70 | if( ev.button==0 ) 71 | comp.use(); 72 | } 73 | 74 | comp.interactive.enableRightButton = true; 75 | } 76 | } 77 | 78 | 79 | public function focusGroup() { 80 | var wasFocused = groupFocused; 81 | groupFocused = true; 82 | ca.lock(0.2); 83 | blurAllConnectedGroups(); 84 | if( !wasFocused ) 85 | onGroupFocusCb(); 86 | } 87 | 88 | public function blurGroup() { 89 | var wasFocused = groupFocused; 90 | groupFocused = false; 91 | if( currentComp!=null ) { 92 | currentComp.onBlur(); 93 | currentComp = null; 94 | } 95 | if( wasFocused ) 96 | onGroupBlurCb(); 97 | } 98 | 99 | public dynamic function onGroupFocusCb() {} 100 | public dynamic function onGroupBlurCb() {} 101 | 102 | function blurAllConnectedGroups(?ignoredGroup:UiGroupController) { 103 | var pending = [this]; 104 | var dones = new Map(); 105 | dones.set(uid,true); 106 | 107 | while( pending.length>0 ) { 108 | var cur = pending.pop(); 109 | dones.set(cur.uid, true); 110 | for(g in cur.uiGroupsConnections) { 111 | if( dones.exists(g.uid) ) 112 | continue; 113 | g.blurGroup(); 114 | pending.push(g); 115 | } 116 | } 117 | } 118 | 119 | public function connectComponents(from:UiComponent, to:UiComponent, dir:GroupDir) { 120 | if( !componentsConnections.exists(from.uid) ) 121 | componentsConnections.set(from.uid, new Map()); 122 | componentsConnections.get(from.uid).set(dir, to); 123 | } 124 | 125 | public function countComponentConnections(c:UiComponent) { 126 | if( !componentsConnections.exists(c.uid) ) 127 | return 0; 128 | 129 | var n = 0; 130 | for( next in componentsConnections.get(c.uid) ) 131 | n++; 132 | return n; 133 | } 134 | 135 | public inline function hasComponentConnectionDir(c:UiComponent, dir:GroupDir) { 136 | return componentsConnections.exists(c.uid) && componentsConnections.get(c.uid).exists(dir); 137 | } 138 | 139 | public function hasComponentConnection(from:UiComponent, to:UiComponent) { 140 | if( !componentsConnections.exists(from.uid) ) 141 | return false; 142 | 143 | for(next in componentsConnections.get(from.uid)) 144 | if( next==to ) 145 | return true; 146 | return false; 147 | } 148 | 149 | public inline function getComponentConnectionDir(from:UiComponent, dir:GroupDir) { 150 | return componentsConnections.exists(from.uid) 151 | ? componentsConnections.get(from.uid).get(dir) 152 | : null; 153 | } 154 | 155 | 156 | 157 | public inline function invalidateConnections() { 158 | connectionsNeedRebuild = true; 159 | } 160 | 161 | function buildConnections() { 162 | // Clear 163 | componentsConnections = new Map(); 164 | 165 | // Build connections with closest aligned components 166 | for(from in components) 167 | for(dir in [North,East,South,West]) { 168 | var other = findComponentRaycast(from,dir); 169 | if( other!=null ) { 170 | connectComponents(from, other, dir); 171 | connectComponents(other, from, getOppositeDir(dir)); 172 | } 173 | } 174 | 175 | // Fix missing connections 176 | for(from in components) 177 | for(dir in [North,East,South,West]) { 178 | if( hasComponentConnectionDir(from,dir) ) 179 | continue; 180 | var next = findComponentFromAng(from, dirToAng(dir), M.PI*0.8, true); 181 | if( next!=null ) 182 | connectComponents(from,next,dir); 183 | } 184 | } 185 | 186 | 187 | // Returns closest UiComponent using an angle range 188 | function findComponentFromAng(from:UiComponent, ang:Float, angRange:Float, ignoreConnecteds:Bool) : Null { 189 | var best = null; 190 | for( other in components ) { 191 | if( other==from || hasComponentConnection(from,other) ) 192 | continue; 193 | 194 | if( M.radDistance(ang, from.globalAngTo(other)) < angRange*0.5 ) { 195 | if( best==null ) 196 | best = other; 197 | else { 198 | if( from.globalDistTo(other) < from.globalDistTo(best) ) 199 | best = other; 200 | } 201 | } 202 | } 203 | return best; 204 | 205 | 206 | } 207 | 208 | // Returns closest UiComponent using a collider-raycast 209 | function findComponentRaycast(from:UiComponent, dir:GroupDir) : Null { 210 | var ang = dirToAng(dir); 211 | var step = switch dir { 212 | case North, South: from.globalHeight; 213 | case East,West: from.globalWidth; 214 | } 215 | var x = from.globalLeft + Math.cos(ang)*step; 216 | var y = from.globalTop + Math.sin(ang)*step; 217 | var elapsedDist = step; 218 | 219 | var possibleNexts = []; 220 | while( elapsedDist0 ) 226 | return dn.Lib.findBestInArray(possibleNexts, (t)->-t.globalDistTo(from) ); 227 | 228 | x += Math.cos(ang)*step; 229 | y += Math.sin(ang)*step; 230 | elapsedDist+=step; 231 | } 232 | 233 | 234 | return null; 235 | } 236 | 237 | 238 | function findClosest(from:UiComponent) : Null { 239 | var best = null; 240 | for(other in components) 241 | if( other!=from && ( best==null || from.globalDistTo(other) < from.globalDistTo(best) ) ) 242 | best = other; 243 | return best; 244 | } 245 | 246 | 247 | public function createDebugger() { 248 | var g = new h2d.Graphics(App.ME.root); 249 | var debugProc = createChildProcess(); 250 | debugProc.onUpdateCb = ()->{ 251 | if( !debugProc.cd.hasSetS("tick",0.1) ) 252 | renderDebugToGraphics(g); 253 | } 254 | debugProc.onDisposeCb = ()->{ 255 | g.remove(); 256 | } 257 | } 258 | 259 | /** 260 | Draw a debug render of the group structure into an existing Graphics object. 261 | NOTE: the render uses global coordinates, so the Graphics object should be attached to the scene root. 262 | **/ 263 | public function renderDebugToGraphics(g:h2d.Graphics) { 264 | g.clear(); 265 | g.removeChildren(); 266 | buildConnections(); 267 | var font = hxd.res.DefaultFont.get(); 268 | for(from in components) { 269 | // Bounds 270 | g.lineStyle(2, Pink); 271 | g.beginFill(Pink, 0.5); 272 | g.drawRect(from.globalLeft, from.globalTop, from.globalWidth, from.globalHeight); 273 | g.endFill(); 274 | // Connections 275 | for(dir in [North,East,South,West]) { 276 | if( !hasComponentConnectionDir(from,dir) ) 277 | continue; 278 | 279 | var next = getComponentConnectionDir(from,dir); 280 | var ang = from.globalAngTo(next); 281 | g.lineStyle(2, Yellow); 282 | g.moveTo(from.globalCenterX, from.globalCenterY); 283 | g.lineTo(next.globalCenterX, next.globalCenterY); 284 | 285 | // Arrow head 286 | var arrowDist = 16; 287 | var arrowAng = M.PI*0.95; 288 | g.moveTo(next.globalCenterX, next.globalCenterY); 289 | g.lineTo(next.globalCenterX+Math.cos(ang+arrowAng)*arrowDist, next.globalCenterY+Math.sin(ang+arrowAng)*arrowDist); 290 | 291 | g.moveTo(next.globalCenterX, next.globalCenterY); 292 | g.lineTo(next.globalCenterX+Math.cos(ang-arrowAng)*arrowDist, next.globalCenterY+Math.sin(ang-arrowAng)*arrowDist); 293 | 294 | var tf = new h2d.Text(font,g); 295 | tf.text = switch dir { 296 | case North: 'N'; 297 | case East: 'E'; 298 | case South: 'S'; 299 | case West: 'W'; 300 | } 301 | tf.x = Std.int( ( from.globalCenterX*0.3 + next.globalCenterX*0.7 ) - tf.textWidth*0.5 ); 302 | tf.y = Std.int( ( from.globalCenterY*0.3 + next.globalCenterY*0.7 ) - tf.textHeight*0.5 ); 303 | tf.filter = new dn.heaps.filter.PixelOutline(); 304 | } 305 | } 306 | } 307 | 308 | 309 | override function onDispose() { 310 | super.onDispose(); 311 | 312 | ca.dispose(); 313 | ca = null; 314 | 315 | components = null; 316 | currentComp = null; 317 | } 318 | 319 | public function clearAllRegisteredComponents() { 320 | currentComp = null; 321 | components = []; 322 | invalidateConnections(); 323 | } 324 | 325 | function focusClosestComponentFromGlobalCoord(x:Float, y:Float) { 326 | var best = Lib.findBestInArray(components, e->{ 327 | return -M.dist(x, y, e.globalCenterX, e.globalCenterY); 328 | }); 329 | if( best!=null ) 330 | focusComponent(best); 331 | } 332 | 333 | function blurComponent(ge:UiComponent) { 334 | if( currentComp==ge ) { 335 | currentComp.onBlur(); 336 | currentComp = null; 337 | } 338 | } 339 | 340 | function focusComponent(ge:UiComponent) { 341 | if( currentComp==ge ) 342 | return; 343 | 344 | if( currentComp!=null ) 345 | currentComp.onBlur(); 346 | currentComp = ge; 347 | currentComp.onFocus(); 348 | } 349 | 350 | inline function getOppositeDir(dir:GroupDir) { 351 | return switch dir { 352 | case North: South; 353 | case East: West; 354 | case South: North; 355 | case West: East; 356 | } 357 | } 358 | 359 | inline function dirToAng(dir:GroupDir) : Float { 360 | return switch dir { 361 | case North: -M.PIHALF; 362 | case East: 0; 363 | case South: M.PIHALF; 364 | case West: M.PI; 365 | } 366 | } 367 | 368 | function angToDir(ang:Float) : GroupDir { 369 | return M.radDistance(ang,0)<=M.PIHALF*0.5 ? East 370 | : M.radDistance(ang,M.PIHALF)<=M.PIHALF*0.5 ? South 371 | : M.radDistance(ang,M.PI)<=M.PIHALF*0.5 ? West 372 | : North; 373 | } 374 | 375 | 376 | function gotoNextDir(dir:GroupDir) { 377 | if( currentComp==null ) 378 | return; 379 | 380 | if( hasComponentConnectionDir(currentComp,dir) ) 381 | focusComponent( getComponentConnectionDir(currentComp,dir) ); 382 | else 383 | gotoConnectedGroup(dir); 384 | } 385 | 386 | 387 | function gotoConnectedGroup(dir:GroupDir) : Bool { 388 | if( !uiGroupsConnections.exists(dir) ) 389 | return false; 390 | 391 | if( uiGroupsConnections.get(dir).components.length==0 ) 392 | return false; 393 | 394 | var g = uiGroupsConnections.get(dir); 395 | var from = currentComp; 396 | // var pt = new h2d.col.Point(from.width*0.5, from.height*0.5); 397 | // from.f.localToGlobal(pt); 398 | blurGroup(); 399 | g.focusGroup(); 400 | g.focusClosestComponentFromGlobalCoord(from.globalCenterX, from.globalCenterY); 401 | return true; 402 | } 403 | 404 | 405 | public function connectGroup(dir:GroupDir, targetGroup:UiGroupController, symetric=true) { 406 | uiGroupsConnections.set(dir,targetGroup); 407 | if( symetric ) 408 | targetGroup.connectGroup(getOppositeDir(dir), this, false); 409 | 410 | if( groupFocused ) 411 | blurAllConnectedGroups(); 412 | } 413 | 414 | 415 | override function preUpdate() { 416 | super.preUpdate(); 417 | 418 | if( !groupFocused ) 419 | return; 420 | 421 | // Build components connections 422 | if( connectionsNeedRebuild ) { 423 | connectionsNeedRebuild = false; 424 | buildConnections(); 425 | } 426 | 427 | // Init default currentComp 428 | if( currentComp==null && components.length>0 ) 429 | if( !cd.hasSetS("firstInitDone",Const.INFINITE) || ca.isDown(MenuLeft) || ca.isDown(MenuRight) || ca.isDown(MenuUp) || ca.isDown(MenuDown) ) 430 | focusComponent(components[0]); 431 | 432 | if( currentComp!=null ) { 433 | // Use current 434 | if( ca.isPressed(MenuOk) ) 435 | currentComp.use(); 436 | 437 | // Move current 438 | if( ca.isPressedAutoFire(MenuLeft) ) 439 | gotoNextDir(West); 440 | else if( ca.isPressedAutoFire(MenuRight) ) 441 | gotoNextDir(East); 442 | 443 | if( ca.isPressedAutoFire(MenuUp) ) 444 | gotoNextDir(North); 445 | else if( ca.isPressedAutoFire(MenuDown) ) 446 | gotoNextDir(South); 447 | } 448 | } 449 | } 450 | 451 | -------------------------------------------------------------------------------- /src/game/ui/Window.hx: -------------------------------------------------------------------------------- 1 | package ui; 2 | 3 | enum WindowAlign { 4 | Start; 5 | End; 6 | Center; 7 | Fill; 8 | } 9 | 10 | class Window extends dn.Process { 11 | public static var ALL : Array = []; 12 | 13 | var uiWid(get,never) : Int; inline function get_uiWid() return M.ceil( stageWid/Const.UI_SCALE ); 14 | var uiHei(get,never) : Int; inline function get_uiHei() return M.ceil( stageHei/Const.UI_SCALE ); 15 | 16 | public var content: h2d.Flow; 17 | 18 | var ca : ControllerAccess; 19 | var mask : Null; 20 | 21 | public var isModal(default, null) = false; 22 | public var canBeClosedManually = true; 23 | public var horizontalAlign(default,set) : WindowAlign = WindowAlign.Center; 24 | public var verticalAlign(default,set) : WindowAlign = WindowAlign.Center; 25 | 26 | 27 | public function new(modal:Bool, ?p:dn.Process) { 28 | var parentProc = p==null ? App.ME : p; 29 | super(parentProc); 30 | 31 | ALL.push(this); 32 | createRootInLayers(parentProc.root, Const.DP_UI); 33 | root.filter = new h2d.filter.Nothing(); // force pixel perfect rendering 34 | 35 | content = new h2d.Flow(root); 36 | content.backgroundTile = h2d.Tile.fromColor(0xffffff, 32,32); 37 | content.borderWidth = 7; 38 | content.borderHeight = 7; 39 | content.layout = Vertical; 40 | content.verticalSpacing = 2; 41 | content.onAfterReflow = onResize; 42 | content.enableInteractive = true; 43 | 44 | ca = App.ME.controller.createAccess(); 45 | ca.lockCondition = ()->App.ME.anyInputHasFocus() || !isActive(); 46 | ca.lock(0.1); 47 | 48 | emitResizeAtEndOfFrame(); 49 | 50 | if( modal ) 51 | makeModal(); 52 | } 53 | 54 | function getModalIndex() { 55 | if( !isModal ) 56 | return -1; 57 | 58 | var i = 0; 59 | for( w in ALL ) 60 | if( w.isModal ) { 61 | if( w==this ) 62 | return i; 63 | i++; 64 | } 65 | Console.ME.error('$this has no valid modalIndex'); 66 | return -1; 67 | } 68 | 69 | function set_horizontalAlign(v:WindowAlign) { 70 | if( v!=horizontalAlign ) { 71 | switch horizontalAlign { 72 | case Fill: content.minWidth = content.maxWidth = null; // clear previous constraint from onResize() 73 | case _: 74 | } 75 | horizontalAlign = v; 76 | emitResizeAtEndOfFrame(); 77 | } 78 | return v; 79 | } 80 | 81 | function set_verticalAlign(v:WindowAlign) { 82 | if( v!=verticalAlign ) { 83 | switch verticalAlign { 84 | case Fill: content.minHeight = content.maxHeight = null; // clear previous constraint from onResize() 85 | case _: 86 | } 87 | verticalAlign = v; 88 | emitResizeAtEndOfFrame(); 89 | } 90 | return v; 91 | } 92 | 93 | public function setAlign(h:WindowAlign, ?v:WindowAlign) { 94 | horizontalAlign = h; 95 | verticalAlign = v!=null ? v : h; 96 | } 97 | 98 | public function isActive() { 99 | return !destroyed && ( !isModal || isLatestModal() ); 100 | } 101 | 102 | public function makeTransparent() { 103 | content.backgroundTile = null; 104 | } 105 | 106 | override function onDispose() { 107 | super.onDispose(); 108 | 109 | ALL.remove(this); 110 | 111 | ca.dispose(); 112 | ca = null; 113 | 114 | if( !hasAnyModal() ) 115 | Game.ME.resume(); 116 | 117 | emitResizeAtEndOfFrame(); 118 | } 119 | 120 | @:keep override function toString():String { 121 | return isModal ? 'ModalWin${isActive()?"*":""}(${getModalIndex()})' : 'Win'; 122 | } 123 | 124 | function makeModal() { 125 | if( isModal ) 126 | return; 127 | 128 | isModal = true; 129 | 130 | if( getModalIndex()==0 ) 131 | Game.ME.pause(); 132 | 133 | mask = new h2d.Flow(root); 134 | mask.backgroundTile = h2d.Tile.fromColor(0x0, 1, 1, 0.8); 135 | mask.enableInteractive = true; 136 | mask.interactive.onClick = _->{ 137 | if( canBeClosedManually ) 138 | close(); 139 | } 140 | mask.interactive.enableRightButton = true; 141 | root.under(mask); 142 | } 143 | 144 | function isLatestModal() { 145 | var idx = ALL.length-1; 146 | while( idx>=0 ) { 147 | var w = ALL[idx]; 148 | if( !w.destroyed ) { 149 | if( w!=this && w.isModal ) 150 | return false; 151 | if( w==this ) 152 | return true; 153 | } 154 | idx--; 155 | } 156 | return false; 157 | } 158 | 159 | public static function hasAnyModal() { 160 | for(e in ALL) 161 | if( !e.destroyed && e.isModal ) 162 | return true; 163 | return false; 164 | } 165 | 166 | public function clearContent() { 167 | content.removeChildren(); 168 | } 169 | 170 | 171 | override function onResize() { 172 | super.onResize(); 173 | 174 | root.setScale(Const.UI_SCALE); 175 | 176 | // Horizontal 177 | if( horizontalAlign==Fill ) 178 | content.minWidth = content.maxWidth = uiWid; 179 | 180 | switch horizontalAlign { 181 | case Start: content.x = 0; 182 | case End: content.x = uiWid-content.outerWidth; 183 | case Center: content.x = Std.int( uiWid*0.5 - content.outerWidth*0.5 + getModalIndex()*8 ); 184 | case Fill: content.x = 0; content.minWidth = content.maxWidth = uiWid; 185 | } 186 | 187 | // Vertical 188 | if( verticalAlign==Fill ) 189 | content.minHeight = content.maxHeight = uiHei; 190 | 191 | switch verticalAlign { 192 | case Start: content.y = 0; 193 | case End: content.y = uiHei-content.outerHeight; 194 | case Center: content.y = Std.int( uiHei*0.5 - content.outerHeight*0.5 + getModalIndex()*4 ); 195 | case Fill: content.y = 0; content.minHeight = content.maxHeight = uiHei; 196 | } 197 | 198 | // Mask 199 | if( mask!=null ) { 200 | mask.minWidth = uiWid; 201 | mask.minHeight = uiHei; 202 | } 203 | } 204 | 205 | public dynamic function onClose() {} 206 | 207 | public function close() { 208 | if( !destroyed ) { 209 | destroy(); 210 | onClose(); 211 | } 212 | } 213 | 214 | 215 | public function addSpacer(pixels=4) { 216 | var f = new h2d.Flow(content); 217 | f.minWidth = f.minHeight = pixels; 218 | } 219 | 220 | public function addTitle(str:String) { 221 | new ui.component.Text( str.toUpperCase(), Col.coldGray(0.5), content ); 222 | addSpacer(); 223 | } 224 | 225 | public function addText(str:String, col:Col=Black) { 226 | new ui.component.Text( str, col, content ); 227 | } 228 | 229 | 230 | 231 | override function update() { 232 | super.update(); 233 | if( canBeClosedManually && isModal && ca.isPressed(MenuCancel) ) 234 | close(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/game/ui/component/Button.hx: -------------------------------------------------------------------------------- 1 | package ui.component; 2 | 3 | class Button extends ui.UiComponent { 4 | var tf : h2d.Text; 5 | 6 | public function new(?label:String, ?iconTile:h2d.Tile, col:dn.Col=Black, ?p:h2d.Object) { 7 | super(p); 8 | 9 | verticalAlign = Middle; 10 | padding = 2; 11 | paddingBottom = 4; 12 | backgroundTile = h2d.Tile.fromColor(White); 13 | 14 | if( iconTile!=null ) 15 | new h2d.Bitmap(iconTile, this); 16 | 17 | tf = new h2d.Text(Assets.fontPixelMono, this); 18 | if( label!=null ) 19 | setLabel(label, col); 20 | } 21 | 22 | public function setLabel(str:String, col:dn.Col=Black) { 23 | tf.text = str; 24 | tf.textColor = col; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/game/ui/component/CheckBox.hx: -------------------------------------------------------------------------------- 1 | package ui.component; 2 | 3 | class CheckBox extends ui.component.Button { 4 | var label : String; 5 | var lastDisplayedValue : Bool; 6 | var getter : Void->Bool; 7 | var setter : Bool->Void; 8 | 9 | public function new(label:String, getter:Void->Bool, setter:Bool->Void, ?p:h2d.Object) { 10 | this.getter = getter; 11 | this.setter = setter; 12 | super(label, p); 13 | } 14 | 15 | override function onUse() { 16 | super.onUse(); 17 | 18 | setter(!getter()); 19 | setLabel(label); 20 | } 21 | 22 | override function setLabel(str:String, col:Col = Black) { 23 | label = str; 24 | lastDisplayedValue = getter(); 25 | super.setLabel( (getter()?"[ON]":"[ ]")+" "+label, col ); 26 | } 27 | 28 | override function sync(ctx:h2d.RenderContext) { 29 | super.sync(ctx); 30 | if( lastDisplayedValue!=getter() ) 31 | setLabel(label); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/game/ui/component/ControlsHelp.hx: -------------------------------------------------------------------------------- 1 | package ui.component; 2 | 3 | class ControlsHelp extends ui.UiComponent { 4 | public function new(?p) { 5 | super(p); 6 | 7 | layout = Horizontal; 8 | horizontalSpacing = 16; 9 | } 10 | 11 | 12 | public function addControl(a:GameAction, label:String, col:Col=White) { 13 | var f = new h2d.Flow(this); 14 | f.layout = Horizontal; 15 | f.verticalAlign = Middle; 16 | 17 | var icon = App.ME.controller.getFirstBindindIconFor(a, "agnostic", f); 18 | f.addSpacing(4); 19 | 20 | var tf = new h2d.Text(Assets.fontPixel, f); 21 | f.getProperties(tf).offsetY = -2; 22 | tf.textColor = col; 23 | tf.text = txt; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/game/ui/component/Text.hx: -------------------------------------------------------------------------------- 1 | package ui.component; 2 | 3 | class Text extends ui.UiComponent { 4 | public function new(label:String, col:dn.Col=Black, ?p) { 5 | super(p); 6 | 7 | paddingTop = 4; 8 | paddingBottom = 4; 9 | var tf = new h2d.Text(Assets.fontPixelMono, this); 10 | tf.textColor = col; 11 | tf.text = label; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/game/ui/win/DebugWindow.hx: -------------------------------------------------------------------------------- 1 | package ui.win; 2 | 3 | class DebugWindow extends ui.Window { 4 | public var updateCooldownS = 0.0; 5 | 6 | public function new(?renderCb:DebugWindow->Void) { 7 | super(false); 8 | 9 | if( renderCb!=null ) 10 | this.renderCb = renderCb; 11 | 12 | content.backgroundTile = Col.white().toTile(1,1, 0.5); 13 | content.padding = 4; 14 | content.horizontalSpacing = 4; 15 | content.verticalSpacing = 0; 16 | content.layout = Vertical; 17 | setAlign(End,Start); 18 | } 19 | 20 | public dynamic function renderCb(thisWin:DebugWindow) {} 21 | 22 | override function onResize() { 23 | super.onResize(); 24 | switch verticalAlign { 25 | case Start,End: content.maxHeight = Std.int( 0.4 * stageHei/Const.UI_SCALE ); 26 | case Center: content.maxHeight = Std.int( 0.8 * stageHei/Const.UI_SCALE ); 27 | case Fill: content.maxHeight = Std.int( stageHei/Const.UI_SCALE ); 28 | } 29 | } 30 | 31 | override function update() { 32 | super.update(); 33 | if( updateCooldownS<=0 || !cd.hasSetS("updateLock",updateCooldownS) ) 34 | renderCb(this); 35 | } 36 | } -------------------------------------------------------------------------------- /src/game/ui/win/SimpleMenu.hx: -------------------------------------------------------------------------------- 1 | package ui.win; 2 | 3 | class SimpleMenu extends ui.Window { 4 | public var uiCtrl : UiGroupController; 5 | 6 | public function new() { 7 | super(true); 8 | 9 | content.padding = 1; 10 | content.horizontalSpacing = 4; 11 | content.verticalSpacing = 0; 12 | content.layout = Vertical; 13 | content.multiline = true; 14 | content.colWidth = 150; 15 | 16 | uiCtrl = new UiGroupController(this); 17 | uiCtrl.customControllerLock = ()->!isActive(); 18 | } 19 | 20 | 21 | public function setColumnWidth(w:Int) { 22 | content.colWidth = w; 23 | } 24 | 25 | override function onResize() { 26 | super.onResize(); 27 | switch verticalAlign { 28 | case Start,End: content.maxHeight = Std.int( 0.4 * stageHei/Const.UI_SCALE ); 29 | case Center: content.maxHeight = Std.int( 0.8 * stageHei/Const.UI_SCALE ); 30 | case Fill: content.maxHeight = Std.int( stageHei/Const.UI_SCALE ); 31 | } 32 | } 33 | 34 | public function addButton(label:String, ?tile:h2d.Tile, autoClose=true, cb:Void->Void) { 35 | var bt = new ui.component.Button(label, tile, content); 36 | bt.minWidth = content.colWidth; 37 | bt.onUseCb = ()->{ 38 | cb(); 39 | if( autoClose ) 40 | close(); 41 | } 42 | uiCtrl.registerComponent(bt); 43 | } 44 | 45 | public function addCheckBox(label:String, getter:Void->Bool, setter:Bool->Void, autoClose=false) { 46 | var bt = new ui.component.CheckBox(label,getter,setter,content); 47 | bt.minWidth = content.colWidth; 48 | bt.onUseCb = ()->{ 49 | if( autoClose ) 50 | close(); 51 | } 52 | 53 | uiCtrl.registerComponent(bt); 54 | } 55 | } -------------------------------------------------------------------------------- /src/langParser/LangParser.hx: -------------------------------------------------------------------------------- 1 | import dn.data.GetText; 2 | 3 | class LangParser { 4 | public static function main() { 5 | var allEntries : Array = []; 6 | 7 | // Extract from source code 8 | GetText.parseSourceCode(allEntries, "src"); 9 | 10 | // Extract from LDtk 11 | GetText.parseLdtk(allEntries, "res/levels/sampleWorld.ldtk", { 12 | entityFields: [], // fill this with Entity fields that should be extracted for localization 13 | levelFieldIds: [], // fill this with Level fields that should be extracted for localization 14 | }); 15 | 16 | // Extract from CastleDB 17 | GetText.parseCastleDB(allEntries, "res/data.cdb"); 18 | 19 | // Write POT 20 | GetText.writePOT("res/lang/sourceTexts.pot", allEntries); 21 | 22 | Sys.println("Done."); 23 | } 24 | } -------------------------------------------------------------------------------- /tools.langParser.hxml: -------------------------------------------------------------------------------- 1 | -cp src/langParser 2 | -main LangParser 3 | -lib castle 4 | -lib deepnightLibs 5 | -D potools 6 | -hl bin/langParser.hl 7 | 8 | --next 9 | -cmd hl bin/langParser.hl --------------------------------------------------------------------------------