├── .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 | [](https://github.com/deepnight/gameBase/actions/workflows/test.js.yml)
8 | [](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
--------------------------------------------------------------------------------