pixospritz-core 0.10.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -286
- package/dist/bundle.js +13 -3
- package/dist/bundle.js.map +1 -1
- package/dist/style.css +1 -0
- package/package.json +43 -44
- package/src/components/WebGLView.jsx +318 -0
- package/src/css/pixos.css +372 -0
- package/src/engine/actions/animate.js +41 -0
- package/src/engine/actions/changezone.js +135 -0
- package/src/engine/actions/chat.js +109 -0
- package/src/engine/actions/dialogue.js +90 -0
- package/src/engine/actions/face.js +22 -0
- package/src/engine/actions/greeting.js +28 -0
- package/src/engine/actions/interact.js +86 -0
- package/src/engine/actions/move.js +67 -0
- package/src/engine/actions/patrol.js +109 -0
- package/src/engine/actions/prompt.js +185 -0
- package/src/engine/actions/script.js +42 -0
- package/src/engine/core/audio/AudioSystem.js +543 -0
- package/src/engine/core/cutscene/PxcPlayer.js +956 -0
- package/src/engine/core/cutscene/manager.js +243 -0
- package/src/engine/core/database/index.js +75 -0
- package/src/engine/core/debug/index.js +371 -0
- package/src/engine/core/hud/index.js +765 -0
- package/src/engine/core/index.js +540 -0
- package/src/engine/core/input/gamepad/Controller.js +71 -0
- package/src/engine/core/input/gamepad/ControllerButtons.js +231 -0
- package/src/engine/core/input/gamepad/ControllerStick.js +173 -0
- package/src/engine/core/input/gamepad/index.js +592 -0
- package/src/engine/core/input/keyboard.js +196 -0
- package/src/engine/core/input/manager.js +485 -0
- package/src/engine/core/input/mouse.js +203 -0
- package/src/engine/core/input/touch.js +175 -0
- package/src/engine/core/mode/manager.js +199 -0
- package/src/engine/core/net/manager.js +535 -0
- package/src/engine/core/queue/action.js +83 -0
- package/src/engine/core/queue/event.js +82 -0
- package/src/engine/core/queue/index.js +44 -0
- package/src/engine/core/queue/loadable.js +33 -0
- package/src/engine/core/render/CameraEffects.js +494 -0
- package/src/engine/core/render/FrustumCuller.js +417 -0
- package/src/engine/core/render/LODManager.js +285 -0
- package/src/engine/core/render/ParticleManager.js +529 -0
- package/src/engine/core/render/TextureAtlas.js +465 -0
- package/src/engine/core/render/camera.js +338 -0
- package/src/engine/core/render/light.js +197 -0
- package/src/engine/core/render/manager.js +1079 -0
- package/src/engine/core/render/shaders.js +110 -0
- package/src/engine/core/render/skybox.js +342 -0
- package/src/engine/core/resource/manager.js +133 -0
- package/src/engine/core/resource/object.js +611 -0
- package/src/engine/core/resource/texture.js +103 -0
- package/src/engine/core/resource/tileset.js +177 -0
- package/src/engine/core/scene/avatar.js +215 -0
- package/src/engine/core/scene/speech.js +138 -0
- package/src/engine/core/scene/sprite.js +702 -0
- package/src/engine/core/scene/spritz.js +189 -0
- package/src/engine/core/scene/world.js +681 -0
- package/src/engine/core/scene/zone.js +1167 -0
- package/src/engine/core/store/index.js +110 -0
- package/src/engine/dynamic/animatedSprite.js +64 -0
- package/src/engine/dynamic/animatedTile.js +98 -0
- package/src/engine/dynamic/avatar.js +110 -0
- package/src/engine/dynamic/map.js +174 -0
- package/src/engine/dynamic/sprite.js +255 -0
- package/src/engine/dynamic/spritz.js +119 -0
- package/src/engine/events/EventSystem.js +609 -0
- package/src/engine/events/camera.js +142 -0
- package/src/engine/events/chat.js +75 -0
- package/src/engine/events/menu.js +186 -0
- package/src/engine/scripting/CallbackManager.js +514 -0
- package/src/engine/scripting/PixoScriptInterpreter.js +81 -0
- package/src/engine/scripting/PixoScriptLibrary.js +704 -0
- package/src/engine/shaders/effects/index.js +450 -0
- package/src/engine/shaders/fs.js +222 -0
- package/src/engine/shaders/particles/fs.js +41 -0
- package/src/engine/shaders/particles/vs.js +61 -0
- package/src/engine/shaders/picker/fs.js +34 -0
- package/src/engine/shaders/picker/init.js +62 -0
- package/src/engine/shaders/picker/vs.js +42 -0
- package/src/engine/shaders/pxsl/README.md +250 -0
- package/src/engine/shaders/pxsl/index.js +25 -0
- package/src/engine/shaders/pxsl/library.js +608 -0
- package/src/engine/shaders/pxsl/manager.js +338 -0
- package/src/engine/shaders/pxsl/specification.js +363 -0
- package/src/engine/shaders/pxsl/transpiler.js +753 -0
- package/src/engine/shaders/skybox/cosmic/fs.js +147 -0
- package/src/engine/shaders/skybox/cosmic/vs.js +23 -0
- package/src/engine/shaders/skybox/matrix/fs.js +127 -0
- package/src/engine/shaders/skybox/matrix/vs.js +23 -0
- package/src/engine/shaders/skybox/morning/fs.js +109 -0
- package/src/engine/shaders/skybox/morning/vs.js +23 -0
- package/src/engine/shaders/skybox/neon/fs.js +119 -0
- package/src/engine/shaders/skybox/neon/vs.js +23 -0
- package/src/engine/shaders/skybox/sky/fs.js +114 -0
- package/src/engine/shaders/skybox/sky/vs.js +23 -0
- package/src/engine/shaders/skybox/sunset/fs.js +101 -0
- package/src/engine/shaders/skybox/sunset/vs.js +23 -0
- package/src/engine/shaders/transition/blur/fs.js +42 -0
- package/src/engine/shaders/transition/blur/vs.js +26 -0
- package/src/engine/shaders/transition/cross/fs.js +36 -0
- package/src/engine/shaders/transition/cross/vs.js +26 -0
- package/src/engine/shaders/transition/crossBlur/fs.js +41 -0
- package/src/engine/shaders/transition/crossBlur/vs.js +25 -0
- package/src/engine/shaders/transition/dissolve/fs.js +78 -0
- package/src/engine/shaders/transition/dissolve/vs.js +24 -0
- package/src/engine/shaders/transition/fade/fs.js +31 -0
- package/src/engine/shaders/transition/fade/vs.js +27 -0
- package/src/engine/shaders/transition/iris/fs.js +52 -0
- package/src/engine/shaders/transition/iris/vs.js +24 -0
- package/src/engine/shaders/transition/pixelate/fs.js +44 -0
- package/src/engine/shaders/transition/pixelate/vs.js +24 -0
- package/src/engine/shaders/transition/slide/fs.js +53 -0
- package/src/engine/shaders/transition/slide/vs.js +24 -0
- package/src/engine/shaders/transition/swirl/fs.js +39 -0
- package/src/engine/shaders/transition/swirl/vs.js +26 -0
- package/src/engine/shaders/transition/wipe/fs.js +50 -0
- package/src/engine/shaders/transition/wipe/vs.js +24 -0
- package/src/engine/shaders/vs.js +60 -0
- package/src/engine/utils/CameraController.js +506 -0
- package/src/engine/utils/ObjHelper.js +551 -0
- package/src/engine/utils/debug-logger.js +110 -0
- package/src/engine/utils/enums.js +305 -0
- package/src/engine/utils/generator.js +156 -0
- package/src/engine/utils/index.js +21 -0
- package/src/engine/utils/loaders/ActionLoader.js +77 -0
- package/src/engine/utils/loaders/AudioLoader.js +157 -0
- package/src/engine/utils/loaders/EventLoader.js +66 -0
- package/src/engine/utils/loaders/ObjectLoader.js +67 -0
- package/src/engine/utils/loaders/SpriteLoader.js +77 -0
- package/src/engine/utils/loaders/TilesetLoader.js +103 -0
- package/src/engine/utils/loaders/index.js +21 -0
- package/src/engine/utils/math/matrix4.js +367 -0
- package/src/engine/utils/math/vector.js +458 -0
- package/src/engine/utils/obj/_old_js/index.js +46 -0
- package/src/engine/utils/obj/_old_js/layout.js +308 -0
- package/src/engine/utils/obj/_old_js/material.js +711 -0
- package/src/engine/utils/obj/_old_js/mesh.js +761 -0
- package/src/engine/utils/obj/_old_js/utils.js +647 -0
- package/src/engine/utils/obj/index.js +24 -0
- package/src/engine/utils/obj/js/index.js +277 -0
- package/src/engine/utils/obj/js/loader.js +232 -0
- package/src/engine/utils/obj/layout.js +246 -0
- package/src/engine/utils/obj/material.js +665 -0
- package/src/engine/utils/obj/mesh.js +657 -0
- package/src/engine/utils/obj/ts/index.ts +72 -0
- package/src/engine/utils/obj/ts/layout.ts +265 -0
- package/src/engine/utils/obj/ts/material.ts +760 -0
- package/src/engine/utils/obj/ts/mesh.ts +785 -0
- package/src/engine/utils/obj/ts/utils.ts +501 -0
- package/src/engine/utils/obj/utils.js +428 -0
- package/src/engine/utils/resources.js +18 -0
- package/src/index.jsx +55 -0
- package/src/spritz/player.js +18 -0
- package/src/spritz/readme.md +18 -0
- package/LICENSE +0 -437
- package/dist/bundle.js.LICENSE.txt +0 -31
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
/* *\
|
|
2
|
+
** ----------------------------------------------- **
|
|
3
|
+
** Calliope - Pixos Game Engine **
|
|
4
|
+
** ----------------------------------------------- **
|
|
5
|
+
** Copyright (c) 2020-2025 - Kyle Derby MacInnis **
|
|
6
|
+
** **
|
|
7
|
+
** Any unauthorized distribution or transfer **
|
|
8
|
+
** of this work is strictly prohibited. **
|
|
9
|
+
** **
|
|
10
|
+
** All Rights Reserved. **
|
|
11
|
+
** ----------------------------------------------- **
|
|
12
|
+
\* */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @fileoverview Zone class for Pixos game engine.
|
|
16
|
+
* Manages map zones, sprites, objects, and rendering.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Direction, mergeDeep } from '@Engine/utils/enums.js';
|
|
20
|
+
import Resources from '@Engine/utils/resources.js';
|
|
21
|
+
import ActionQueue from '@Engine/core/queue/index.js';
|
|
22
|
+
import { Vector } from '@Engine/utils/math/vector.js';
|
|
23
|
+
import { EventLoader, SpriteLoader, TilesetLoader, ActionLoader, ObjectLoader } from '@Engine/utils/loaders/index.js';
|
|
24
|
+
import { loadMap, dynamicCells } from '@Engine/dynamic/map.js';
|
|
25
|
+
import Loadable from '@Engine/core/queue/loadable.js';
|
|
26
|
+
import { debug } from '@Engine/utils/debug-logger.js';
|
|
27
|
+
import PixoScriptInterpreter from '@Engine/scripting/PixoScriptInterpreter.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} ZoneData
|
|
31
|
+
* @property {string} id - Zone ID.
|
|
32
|
+
* @property {number} objId - Object ID.
|
|
33
|
+
* @property {object[]} scripts - Scripts.
|
|
34
|
+
* @property {object} data - Zone data.
|
|
35
|
+
* @property {string[]} objects - Object IDs.
|
|
36
|
+
* @property {string[]} sprites - Sprite IDs.
|
|
37
|
+
* @property {Array<number[]>} selectedTiles - Selected tiles.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Zone - Represents a map zone with tiles, sprites, and objects.
|
|
42
|
+
*/
|
|
43
|
+
export default class Zone extends Loadable {
|
|
44
|
+
/**
|
|
45
|
+
* Creates an instance of Zone.
|
|
46
|
+
* @param {string} zoneId - The zone ID.
|
|
47
|
+
* @param {import('./world.js').default} world - The world instance.
|
|
48
|
+
*/
|
|
49
|
+
constructor(zoneId, world) {
|
|
50
|
+
super();
|
|
51
|
+
/** @type {string} */
|
|
52
|
+
this.spritzName = world.id;
|
|
53
|
+
/** @type {string} */
|
|
54
|
+
this.id = zoneId;
|
|
55
|
+
/** @type {number} */
|
|
56
|
+
this.objId = Math.round(Math.random() * 100);
|
|
57
|
+
/** @type {import('./world.js').default} */
|
|
58
|
+
this.world = world;
|
|
59
|
+
/** @type {object} */
|
|
60
|
+
this.data = {};
|
|
61
|
+
/** @type {Object.<string, object>} */
|
|
62
|
+
this.spriteDict = Object.create(null);
|
|
63
|
+
/** @type {object[]} */
|
|
64
|
+
this.spriteList = [];
|
|
65
|
+
/** @type {Object.<string, object>} */
|
|
66
|
+
this.objectDict = Object.create(null);
|
|
67
|
+
/** @type {object[]} */
|
|
68
|
+
this.objectList = [];
|
|
69
|
+
/** @type {Array<number[]>} */
|
|
70
|
+
this.selectedTiles = [];
|
|
71
|
+
/** @type {object[]} */
|
|
72
|
+
this.lights = [];
|
|
73
|
+
/** @type {object[]} */
|
|
74
|
+
this.spritz = [];
|
|
75
|
+
/** @type {object[]} */
|
|
76
|
+
this.scripts = this.scripts || [];
|
|
77
|
+
/** @type {number} */
|
|
78
|
+
this.lastKey = 0;
|
|
79
|
+
/** @type {import('../index.js').default} */
|
|
80
|
+
this.engine = world.engine;
|
|
81
|
+
/** @type {ActionQueue} */
|
|
82
|
+
this.onLoadActions = new ActionQueue();
|
|
83
|
+
/** @type {SpriteLoader} */
|
|
84
|
+
this.spriteLoader = new SpriteLoader(world.engine);
|
|
85
|
+
/** @type {ObjectLoader} */
|
|
86
|
+
this.objectLoader = new ObjectLoader(world.engine);
|
|
87
|
+
/** @type {typeof EventLoader} */
|
|
88
|
+
this.EventLoader = EventLoader;
|
|
89
|
+
/** @type {TilesetLoader} */
|
|
90
|
+
this.tsLoader = new TilesetLoader(world.engine);
|
|
91
|
+
/** @type {object|null} */
|
|
92
|
+
this.audio = null;
|
|
93
|
+
/** @type {Set<string>|null} */
|
|
94
|
+
this._selectedSet = null;
|
|
95
|
+
/** @type {number[]|null} */
|
|
96
|
+
this._highlight = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Gets the zone data.
|
|
101
|
+
* @returns {ZoneData} The zone data.
|
|
102
|
+
*/
|
|
103
|
+
getZoneData = () => {
|
|
104
|
+
return {
|
|
105
|
+
id: this.id,
|
|
106
|
+
objId: this.objId,
|
|
107
|
+
scripts: this.scripts,
|
|
108
|
+
data: this.data,
|
|
109
|
+
objects: Object.keys(this.objectDict),
|
|
110
|
+
sprites: Object.keys(this.spriteDict),
|
|
111
|
+
selectedTiles: this.selectedTiles,
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Called after tileset and actors are loaded.
|
|
117
|
+
*/
|
|
118
|
+
afterTilesetAndActorsLoaded = () => {
|
|
119
|
+
if (
|
|
120
|
+
this.loaded ||
|
|
121
|
+
!this.tileset?.loaded ||
|
|
122
|
+
!this.spriteList.every((s) => s.loaded) ||
|
|
123
|
+
!this.objectList.every((o) => o.loaded)
|
|
124
|
+
) return;
|
|
125
|
+
|
|
126
|
+
this.loaded = true;
|
|
127
|
+
this.loadScripts(true);
|
|
128
|
+
this.onLoadActions.run();
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Attaches tileset listeners.
|
|
133
|
+
*/
|
|
134
|
+
attachTilesetListeners = () => {
|
|
135
|
+
this.tileset.runWhenDefinitionLoaded(this.onTilesetDefinitionLoaded);
|
|
136
|
+
this.tileset.runWhenLoaded(this.afterTilesetAndActorsLoaded);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Finalizes the zone loading.
|
|
141
|
+
*/
|
|
142
|
+
finalize = async () => {
|
|
143
|
+
for (const s of this.spriteList) s.runWhenLoaded(this.afterTilesetAndActorsLoaded);
|
|
144
|
+
for (const o of this.objectList) o.runWhenLoaded(this.afterTilesetAndActorsLoaded);
|
|
145
|
+
this.engine.networkManager.loadZone(this.id, this);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Loads the zone remotely.
|
|
150
|
+
*/
|
|
151
|
+
loadRemote = async () => {
|
|
152
|
+
const res = await fetch(Resources.zoneRequestUrl(this.id));
|
|
153
|
+
if (!res.ok) return;
|
|
154
|
+
try {
|
|
155
|
+
const data = await res.json();
|
|
156
|
+
this.bounds = data.bounds;
|
|
157
|
+
this.size = [data.bounds[2] - data.bounds[0], data.bounds[3] - data.bounds[1]];
|
|
158
|
+
|
|
159
|
+
this.cells = typeof data.cells === 'function' ? data.cells(this.bounds, this) : data.cells;
|
|
160
|
+
this.sprites = typeof data.sprites === 'function' ? data.sprites(this.bounds, this) : data.sprites || [];
|
|
161
|
+
this.objects = typeof data.objects === 'function' ? data.objects(this.bounds, this) : data.objects || [];
|
|
162
|
+
|
|
163
|
+
this.tileset = await this.tsLoader.load(data.tileset, this.spritzName);
|
|
164
|
+
this.attachTilesetListeners();
|
|
165
|
+
|
|
166
|
+
if (this.audioSrc) this.audio = this.engine.resourceManager.audioLoader.load(this.audioSrc, true);
|
|
167
|
+
|
|
168
|
+
await Promise.all([
|
|
169
|
+
Promise.all(this.sprites.map(this.loadSprite)),
|
|
170
|
+
Promise.all(this.objects.map(this.loadObject)),
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
// If zone specifies a skyboxShader, set it
|
|
174
|
+
if (data.skyboxShader && this.engine.renderManager?.skyboxManager?.setSkyboxShader) {
|
|
175
|
+
await this.engine.renderManager.skyboxManager.setSkyboxShader(data.skyboxShader);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await this.finalize();
|
|
179
|
+
// If the zone JSON declares a mode, load mode scripts from the spritz package
|
|
180
|
+
try {
|
|
181
|
+
if (data.mode)
|
|
182
|
+
await this.loadMode(data.mode);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.warn('zone mode load failed', e);
|
|
185
|
+
}
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.error('Error parsing zone ' + this.id, e);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Loads the zone.
|
|
193
|
+
*/
|
|
194
|
+
load = async () => {
|
|
195
|
+
try {
|
|
196
|
+
const mapModule = await import('../../../../spritz/' + this.spritzName + '/maps/' + this.id + '/map.js');
|
|
197
|
+
const data = mapModule.default;
|
|
198
|
+
Object.assign(this, data);
|
|
199
|
+
|
|
200
|
+
// dynamic cells (such as randomly generated)
|
|
201
|
+
if (typeof this.cells === 'function') this.cells = this.cells(this.bounds, this);
|
|
202
|
+
// Background audio
|
|
203
|
+
if (this.audioSrc) this.audio = this.engine.resourceManager.audioLoader.load(this.audioSrc, true);
|
|
204
|
+
|
|
205
|
+
// Load in tileset assets
|
|
206
|
+
this.size = [this.bounds[2] - this.bounds[0], this.bounds[3] - this.bounds[1]];
|
|
207
|
+
this.tileset = await this.tsLoader.load(this.tileset, this.spritzName);
|
|
208
|
+
this.attachTilesetListeners();
|
|
209
|
+
|
|
210
|
+
// dynamically add sprites (if appl.) - todo - possibly same thing for objects?
|
|
211
|
+
if (typeof this.sprites === 'function') this.sprites = this.sprites(this.bounds, this);
|
|
212
|
+
this.sprites = this.sprites || [];
|
|
213
|
+
this.objects = this.objects || [];
|
|
214
|
+
|
|
215
|
+
// populate
|
|
216
|
+
await Promise.all([
|
|
217
|
+
Promise.all(this.sprites.map(this.loadSprite)),
|
|
218
|
+
Promise.all(this.objects.map(this.loadObject)),
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
// If zone specifies a skyboxShader, set it
|
|
222
|
+
if (data.skyboxShader && this.engine.renderManager?.skyboxManager?.setSkyboxShader) {
|
|
223
|
+
await this.engine.renderManager.skyboxManager.setSkyboxShader(data.skyboxShader);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await this.finalize();
|
|
227
|
+
// If zone specifies a default mode name on the map data, attempt to load it from spritz package
|
|
228
|
+
try {
|
|
229
|
+
if (data.mode)
|
|
230
|
+
await this.loadMode(data.mode);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.warn('zone mode load failed', e);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
this.engine.networkManager.joinZone(this.id);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
console.warn('Network Error :: could not send zone commend to server')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
} catch (e) {
|
|
242
|
+
console.error('Error parsing zone ' + this.id, e);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Loads a trigger from a zip archive.
|
|
248
|
+
* @param {string} trigger - The trigger name.
|
|
249
|
+
* @param {object} zip - The zip archive.
|
|
250
|
+
* @returns {function(): void} The trigger function.
|
|
251
|
+
*/
|
|
252
|
+
loadTriggerFromZip = async (trigger, zip) => {
|
|
253
|
+
// Try Lua first
|
|
254
|
+
try {
|
|
255
|
+
const file = await zip.file(`triggers/${trigger}.pxs`);
|
|
256
|
+
if (file) {
|
|
257
|
+
const luaScript = await file.async('string');
|
|
258
|
+
return (_this, subject) => {
|
|
259
|
+
const interpreter = new PixoScriptInterpreter(_this.engine);
|
|
260
|
+
interpreter.setScope({ _this, zone: this, subject });
|
|
261
|
+
interpreter.initLibrary();
|
|
262
|
+
return interpreter.run(luaScript);
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
} catch (e) {
|
|
266
|
+
if (this.engine?.debug) console.warn('Lua trigger load failed', e);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Try .pxs extension (PixoScript/Lua)
|
|
270
|
+
try {
|
|
271
|
+
const pxsFile = await zip.file(`triggers/${trigger}.pxs`);
|
|
272
|
+
if (pxsFile) {
|
|
273
|
+
const luaScript = await pxsFile.async('string');
|
|
274
|
+
return (_this, subject) => {
|
|
275
|
+
const interpreter = new PixoScriptInterpreter(_this.engine);
|
|
276
|
+
interpreter.setScope({ _this, zone: this, subject });
|
|
277
|
+
interpreter.initLibrary();
|
|
278
|
+
return interpreter.run(luaScript);
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
} catch (e) {
|
|
282
|
+
if (this.engine?.debug) console.warn('PixoScript trigger load failed', e);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// JS fallback (sandboxed) — no global eval
|
|
286
|
+
const jsFile = await zip.file(`triggers/${trigger}.js`);
|
|
287
|
+
if (jsFile) {
|
|
288
|
+
const triggerScript = await jsFile.async('string');
|
|
289
|
+
// new Function isolates scope; it receives (zone, engine) and must return a function
|
|
290
|
+
const factory = new Function('zone', 'engine', `${triggerScript}; return (typeof module !== 'undefined' && module.exports) ? module.exports : (typeof exports !== 'undefined' ? exports : (typeof trigger === 'function' ? trigger : null));`);
|
|
291
|
+
const fn = factory(this, this.engine);
|
|
292
|
+
if (typeof fn === 'function') return fn.bind(this, this);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return () => { };
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Loads a mode from a zip archive.
|
|
300
|
+
* @param {string} modeName - The mode name.
|
|
301
|
+
* @param {object} zip - The zip archive.
|
|
302
|
+
*/
|
|
303
|
+
loadModeFromZip = async (modeName, zip) => {
|
|
304
|
+
try {
|
|
305
|
+
debug('Zone', 'Loading Game Mode From Zip');
|
|
306
|
+
|
|
307
|
+
const setupFile = zip.file(`modes/${modeName}/setup.pxs`);
|
|
308
|
+
const updateFile = zip.file(`modes/${modeName}/update.pxs`);
|
|
309
|
+
const teardownFile = zip.file(`modes/${modeName}/teardown.pxs`);
|
|
310
|
+
const world = this.world;
|
|
311
|
+
|
|
312
|
+
const interpreter = new PixoScriptInterpreter(this.engine);
|
|
313
|
+
interpreter.setScope({ zone: this, map: this, _this: this });
|
|
314
|
+
interpreter.initLibrary();
|
|
315
|
+
|
|
316
|
+
const handlers = {};
|
|
317
|
+
if (setupFile) {
|
|
318
|
+
const script = await setupFile.async('string');
|
|
319
|
+
// run the setup registration (it likely calls pixos.register_mode)
|
|
320
|
+
debug('Zone', 'loadModeFromZip: running setup.pxs for mode', modeName);
|
|
321
|
+
await interpreter.run(script);
|
|
322
|
+
}
|
|
323
|
+
// If update file exists, load it as a function and register as handler
|
|
324
|
+
if (updateFile) {
|
|
325
|
+
const updateScript = await updateFile.async('string');
|
|
326
|
+
// wrap as a function and register to call on each frame via ModeManager
|
|
327
|
+
// We return a JS function that executes the Lua chunk each time
|
|
328
|
+
handlers.update = async (time, params) => {
|
|
329
|
+
try {
|
|
330
|
+
// create a fresh interpreter env for update to avoid state bleed
|
|
331
|
+
const ui = new PixoScriptInterpreter(this.engine);
|
|
332
|
+
ui.setScope({ zone: this, map: this, _this: this, time, params });
|
|
333
|
+
ui.initLibrary();
|
|
334
|
+
// The update.pxs is expected to return a function
|
|
335
|
+
const res = await ui.run(updateScript);
|
|
336
|
+
// If the script returned a callable (Lua function) we invoke it
|
|
337
|
+
if (typeof res === 'function') res(time, params);
|
|
338
|
+
} catch (e) { console.warn('mode update exec failed', e); }
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (teardownFile) {
|
|
342
|
+
const tdScript = await teardownFile.async('string');
|
|
343
|
+
handlers.teardown = async (params) => {
|
|
344
|
+
try {
|
|
345
|
+
const td = new PixoScriptInterpreter(this.engine);
|
|
346
|
+
td.setScope({ zone: this });
|
|
347
|
+
td.initLibrary();
|
|
348
|
+
const res = await td.run(tdScript);
|
|
349
|
+
// if there is a returned callback, we can run it
|
|
350
|
+
if (typeof res === 'function') res(params);
|
|
351
|
+
} catch (e) {
|
|
352
|
+
console.warn('mode teardown failed', e);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// If the setup script used pixos.register_mode, the ModeManager will
|
|
358
|
+
// already have the registration. But ensure we add handlers if not.
|
|
359
|
+
if (world && world.modeManager) {
|
|
360
|
+
const existing = world.modeManager.registered[modeName];
|
|
361
|
+
if (!existing) world.modeManager.register(modeName, handlers);
|
|
362
|
+
}
|
|
363
|
+
} catch (e) {
|
|
364
|
+
console.warn('loadModeFromZip failed', modeName, e);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Loads a mode.
|
|
370
|
+
* @param {string} modeName - The mode name.
|
|
371
|
+
*/
|
|
372
|
+
loadMode = async (modeName) => {
|
|
373
|
+
try {
|
|
374
|
+
const world = this.world;
|
|
375
|
+
await this.loadModeFromZip(modeName, world.spritz.zip);
|
|
376
|
+
} catch (e) {
|
|
377
|
+
console.warn('loadMode failed', modeName, e);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Loads a zone from a zip archive.
|
|
383
|
+
* @param {object} zoneJson - The zone JSON.
|
|
384
|
+
* @param {object} cellJson - The cell JSON.
|
|
385
|
+
* @param {object} zip - The zip archive.
|
|
386
|
+
* @param {boolean} [skipCache=false] - Whether to skip cache.
|
|
387
|
+
*/
|
|
388
|
+
loadZoneFromZip = async (zoneJson, cellJson, zip, skipCache = false) => {
|
|
389
|
+
try {
|
|
390
|
+
// Zone extensions
|
|
391
|
+
if (zoneJson.extends?.length) {
|
|
392
|
+
let extension = {};
|
|
393
|
+
await Promise.all(zoneJson.extends.map(async (file) => {
|
|
394
|
+
const str = await zip.file('maps/' + file + '/map.json').async('string');
|
|
395
|
+
extension = mergeDeep(extension, JSON.parse(str));
|
|
396
|
+
}));
|
|
397
|
+
zoneJson = Object.assign(extension, { ...zoneJson, extends: null });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Cell extensions
|
|
401
|
+
if (cellJson.extends?.length) {
|
|
402
|
+
let cells = [];
|
|
403
|
+
await Promise.all(cellJson.extends.map(async (file) => {
|
|
404
|
+
const str = await zip.file('maps/' + file + '/cells.json').async('string');
|
|
405
|
+
const parsed = JSON.parse(str);
|
|
406
|
+
cells = cells.concat(parsed.cells ? parsed.cells : parsed);
|
|
407
|
+
}));
|
|
408
|
+
cellJson = cells.concat(cellJson.cells || []);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Load heights.json if it exists
|
|
412
|
+
let heightsJson = null;
|
|
413
|
+
try {
|
|
414
|
+
const heightsFile = zip.file('maps/' + this.id + '/heights.json');
|
|
415
|
+
if (heightsFile) {
|
|
416
|
+
const heightsStr = await heightsFile.async('string');
|
|
417
|
+
heightsJson = JSON.parse(heightsStr);
|
|
418
|
+
debug('Zone', `Loaded heights.json for ${this.id}:`, heightsJson?.length, 'rows');
|
|
419
|
+
debug('Zone', `First row heights:`, heightsJson?.[0]);
|
|
420
|
+
debug('Zone', `Heights data sample:`, JSON.stringify(heightsJson?.slice(0, 3)));
|
|
421
|
+
} else {
|
|
422
|
+
debug('Zone', `No heights.json found for ${this.id}, using default geometry heights`);
|
|
423
|
+
}
|
|
424
|
+
} catch (e) {
|
|
425
|
+
console.warn(`[Zone] Failed to load heights.json for ${this.id}:`, e.message);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Menus
|
|
429
|
+
if (zoneJson.menu) {
|
|
430
|
+
const menus = {};
|
|
431
|
+
await Promise.all(Object.keys(zoneJson.menu).map(async (id) => {
|
|
432
|
+
const menu = { ...zoneJson.menu[id], id };
|
|
433
|
+
if (menu.onOpen) menu.onOpen = (await this.loadTriggerFromZip(menu.onOpen, zip)).bind(this, this);
|
|
434
|
+
if (menu.trigger) menu.trigger = (await this.loadTriggerFromZip(menu.trigger, zip)).bind(this, this);
|
|
435
|
+
menus[id] = menu;
|
|
436
|
+
}));
|
|
437
|
+
this.menus = menus;
|
|
438
|
+
this.world.startMenu(this.menus);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Tileset / map / cells
|
|
442
|
+
const tileset = await this.tsLoader.loadFromZip(zip, zoneJson.tileset, this.spritzName);
|
|
443
|
+
const cells = dynamicCells(cellJson, tileset.tiles);
|
|
444
|
+
const map = await loadMap.call(this, zoneJson, cells, zip, heightsJson);
|
|
445
|
+
Object.assign(this, map);
|
|
446
|
+
|
|
447
|
+
// Cells generator (string -> function)
|
|
448
|
+
if (typeof this.cells === 'string') {
|
|
449
|
+
try {
|
|
450
|
+
// Strict scope function (no global eval)
|
|
451
|
+
const fn = new Function('bounds', 'zone', `return (${this.cells})(bounds, zone);`);
|
|
452
|
+
this.cells = fn.call(this, this.bounds, this);
|
|
453
|
+
} catch (e) {
|
|
454
|
+
console.error('error loading cell function', e);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Audio
|
|
459
|
+
if (zoneJson.mode) {
|
|
460
|
+
try { this.mode = zoneJson.mode } catch (e) { console.error('audio load', e); }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Audio
|
|
464
|
+
if (zoneJson.audioSrc) {
|
|
465
|
+
try { this.audio = await this.engine.resourceManager.audioLoader.loadFromZip(zip, zoneJson.audioSrc, true); } catch (e) { console.error('audio load', e); }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Lights
|
|
469
|
+
try {
|
|
470
|
+
this.lights = zoneJson.lights ?? [];
|
|
471
|
+
const lm = this.engine.renderManager.lightManager;
|
|
472
|
+
for (const l of this.lights) lm.addLight(l.id, l.pos, l.color, l.attenuation, l.direction, l.density, l.scatteringCoefficients, l.enabled);
|
|
473
|
+
} catch (e) { console.error('lights', e); }
|
|
474
|
+
|
|
475
|
+
// Tileset + size
|
|
476
|
+
this.tileset = tileset;
|
|
477
|
+
this.size = [this.bounds[2] - this.bounds[0], this.bounds[3] - this.bounds[1]];
|
|
478
|
+
|
|
479
|
+
// Sprite generators
|
|
480
|
+
if (typeof this.sprites === 'string') {
|
|
481
|
+
try {
|
|
482
|
+
const fn = new Function('bounds', 'zone', `return (${this.sprites})(bounds, zone);`);
|
|
483
|
+
this.sprites = fn.call(this, this.bounds, this);
|
|
484
|
+
} catch (e) { console.error('sprite fn', e); }
|
|
485
|
+
}
|
|
486
|
+
this.sprites = this.sprites || [];
|
|
487
|
+
this.objects = this.objects || [];
|
|
488
|
+
|
|
489
|
+
await Promise.all([
|
|
490
|
+
Promise.all(this.sprites.map((s) => this.loadSpriteFromZip(s, zip, skipCache))),
|
|
491
|
+
Promise.all(this.objects.map((o) => this.loadObjectFromZip(o, zip))),
|
|
492
|
+
]);
|
|
493
|
+
|
|
494
|
+
this.attachTilesetListeners();
|
|
495
|
+
|
|
496
|
+
// If the loaded map object includes a 'mode' property attempt to load a mode module
|
|
497
|
+
// todo - look into whether this should be updated or moved - not sure if the zone should control the mode like this.
|
|
498
|
+
// in some cases, it makes sense, but I feel like if multiple zones are loaded, there could be conflicts, and the idea of
|
|
499
|
+
// the zone controlling gameplay could be confusing in some cases, but for "battle zones" it kind of makes sense - but this could
|
|
500
|
+
// be done via scripts - so possibly something which could be fully scripted instead of this kind of logic - and instead
|
|
501
|
+
// I will likely move this to the world object - and then it will be the 'initial' mode.
|
|
502
|
+
try {
|
|
503
|
+
if (this.mode)
|
|
504
|
+
await this.loadMode(this.mode);
|
|
505
|
+
} catch (e) {
|
|
506
|
+
console.warn('zone mode load failed', e);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
await this.finalize();
|
|
510
|
+
|
|
511
|
+
} catch (e) {
|
|
512
|
+
console.error('Error parsing json zone ' + this.id, e);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Runs when the zone is deleted.
|
|
518
|
+
*/
|
|
519
|
+
runWhenDeleted = () => {
|
|
520
|
+
for (const l of this.lights) this.engine.renderManager.lightManager.removeLight(l.id);
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Called when tileset definition is loaded.
|
|
525
|
+
*/
|
|
526
|
+
onTilesetDefinitionLoaded = () => {
|
|
527
|
+
const width = this.size[0];
|
|
528
|
+
const height = this.size[1];
|
|
529
|
+
const rm = this.engine.renderManager;
|
|
530
|
+
const gl = this.engine.gl;
|
|
531
|
+
|
|
532
|
+
// Guard: Check if cells are properly loaded
|
|
533
|
+
if (!this.cells || this.cells.length === 0) {
|
|
534
|
+
console.error('[Zone.onTilesetDefinitionLoaded] No cells data - tileset may be missing tiles definition');
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
this.cellVertexPosBuf = Array.from({ length: height }, () => new Array(width));
|
|
539
|
+
this.cellVertexTexBuf = Array.from({ length: height }, () => new Array(width));
|
|
540
|
+
this.cellPickingId = Array.from({ length: height }, () => new Array(width));
|
|
541
|
+
this.walkability = new Uint16Array(width * height);
|
|
542
|
+
|
|
543
|
+
// Precompute
|
|
544
|
+
let k = 0;
|
|
545
|
+
for (let j = 0; j < height; j++) {
|
|
546
|
+
for (let i = 0; i < width; i++, k++) {
|
|
547
|
+
const cell = this.cells[k];
|
|
548
|
+
|
|
549
|
+
// Guard: Skip if cell is undefined (tile lookup failed)
|
|
550
|
+
if (!cell || !Array.isArray(cell)) {
|
|
551
|
+
console.warn(`[Zone] Cell [${j},${i}] is undefined - missing tile in tileset`);
|
|
552
|
+
// Create empty buffers
|
|
553
|
+
this.cellVertexPosBuf[j][i] = rm.createBuffer(new Float32Array([]), gl.STATIC_DRAW, 3);
|
|
554
|
+
this.cellVertexTexBuf[j][i] = rm.createBuffer(new Float32Array([]), gl.STATIC_DRAW, 2);
|
|
555
|
+
this.cellPickingId[j][i] = rm.pickingManager.nextPickingId();
|
|
556
|
+
this.walkability[k] = 0;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const layers = Math.floor(cell.length / 3);
|
|
561
|
+
|
|
562
|
+
let cellVertices = [];
|
|
563
|
+
let cellTex = [];
|
|
564
|
+
let walk = Direction.All;
|
|
565
|
+
|
|
566
|
+
// Get height override for this cell if heights data exists
|
|
567
|
+
const heightOverride = this.heights && this.heights[j] && typeof this.heights[j][i] === 'number'
|
|
568
|
+
? this.heights[j][i]
|
|
569
|
+
: null;
|
|
570
|
+
|
|
571
|
+
// Debug first few cells - show null/number for diagnostics
|
|
572
|
+
if (k < 5) {
|
|
573
|
+
console.log(`[Zone.finalize] Cell [${j},${i}] heightOverride:`, heightOverride);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
for (let l = 0; l < layers; l++) {
|
|
577
|
+
const tileId = cell[3 * l];
|
|
578
|
+
const tileVariant = cell[3 * l + 1];
|
|
579
|
+
let z = cell[3 * l + 2];
|
|
580
|
+
if (typeof z !== 'number') z = 0;
|
|
581
|
+
const tilePos = [this.bounds[0] + i, this.bounds[1] + j, z];
|
|
582
|
+
walk &= this.tileset.getWalkability(tileId);
|
|
583
|
+
|
|
584
|
+
// Pass height override to getTileVertices
|
|
585
|
+
cellVertices = cellVertices.concat(
|
|
586
|
+
this.tileset.getTileVertices(tileId, tilePos, heightOverride)
|
|
587
|
+
);
|
|
588
|
+
cellTex = cellTex.concat(this.tileset.getTileTexCoords(tileId, tileVariant));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// override walkability if provided
|
|
592
|
+
if (cell.length === 3 * layers + 1) walk = cell[3 * layers];
|
|
593
|
+
this.walkability[k] = walk;
|
|
594
|
+
|
|
595
|
+
// GPU buffers
|
|
596
|
+
const vPos = rm.createBuffer(new Float32Array(cellVertices), gl.STATIC_DRAW, 3);
|
|
597
|
+
const vTex = rm.createBuffer(new Float32Array(cellTex), gl.STATIC_DRAW, 2);
|
|
598
|
+
this.cellVertexPosBuf[j][i] = vPos;
|
|
599
|
+
this.cellVertexTexBuf[j][i] = vTex;
|
|
600
|
+
|
|
601
|
+
// Picking ID packed as floats [0..1, 0..1, 0..1, 255]
|
|
602
|
+
this.cellPickingId[j][i] = [
|
|
603
|
+
(this.objId & 0xff) / 255,
|
|
604
|
+
(j & 0xff) / 255,
|
|
605
|
+
(i & 0xff) / 255,
|
|
606
|
+
255,
|
|
607
|
+
];
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Loads scripts.
|
|
614
|
+
* @param {boolean} [refresh=false] - Whether to refresh.
|
|
615
|
+
*/
|
|
616
|
+
loadScripts = (refresh = false) => {
|
|
617
|
+
if (this.world.isPaused) return;
|
|
618
|
+
// CRITICAL: Zone load scripts must run even when paused
|
|
619
|
+
// They are responsible for initializing the zone state
|
|
620
|
+
const zone = this;
|
|
621
|
+
for (const x of this.scripts) {
|
|
622
|
+
if (x.id === 'load-spritz' && refresh) {
|
|
623
|
+
// Call trigger immediately when loading/refreshing
|
|
624
|
+
try {
|
|
625
|
+
x.trigger.call(zone);
|
|
626
|
+
} catch (e) {
|
|
627
|
+
console.error('[Zone.loadScripts] Error calling load-spritz trigger:', e);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Loads an object.
|
|
635
|
+
* @param {object} data - The object data.
|
|
636
|
+
*/
|
|
637
|
+
loadObject = async (data) => {
|
|
638
|
+
data.zone = this;
|
|
639
|
+
if (!this.objectDict[data.id]) {
|
|
640
|
+
const obj = await this.objectLoader.load(data, (o) => o.onLoad(o));
|
|
641
|
+
this.world.objectDict[data.id] = this.objectDict[data.id] = obj;
|
|
642
|
+
this.objectList.push(obj);
|
|
643
|
+
this.world.objectList.push(obj);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Loads an object from a zip archive.
|
|
649
|
+
* @param {object} data - The object data.
|
|
650
|
+
* @param {object} zip - The zip archive.
|
|
651
|
+
*/
|
|
652
|
+
loadObjectFromZip = async (data, zip) => {
|
|
653
|
+
data.zone = this;
|
|
654
|
+
if (!this.objectDict[data.id]) {
|
|
655
|
+
const obj = await this.objectLoader.loadFromZip(zip, data, async (o) => o.onLoadFromZip(o, zip));
|
|
656
|
+
this.world.objectDict[data.id] = this.objectDict[data.id] = obj;
|
|
657
|
+
this.objectList.push(obj);
|
|
658
|
+
this.world.objectList.push(obj);
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Loads a sprite.
|
|
664
|
+
* @param {object} data - The sprite data.
|
|
665
|
+
*/
|
|
666
|
+
loadSprite = async (data) => {
|
|
667
|
+
data.zone = this;
|
|
668
|
+
if (!this.spriteDict[data.id]) {
|
|
669
|
+
const spr = await this.spriteLoader.load(data.type, this.spritzName, (s) => s.onLoad(data));
|
|
670
|
+
this.world.spriteDict[data.id] = this.spriteDict[data.id] = spr;
|
|
671
|
+
this.spriteList.push(spr);
|
|
672
|
+
this.world.spriteList.push(spr);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Loads a sprite from a zip archive.
|
|
678
|
+
* @param {object} data - The sprite data.
|
|
679
|
+
* @param {object} zip - The zip archive.
|
|
680
|
+
*/
|
|
681
|
+
loadSpriteFromZip = async (data, zip) => {
|
|
682
|
+
data.zone = this;
|
|
683
|
+
if (!this.spriteDict[data.id]) {
|
|
684
|
+
const spr = await this.spriteLoader.loadFromZip(zip, data.type, this.spritzName, async (s) => s.onLoadFromZip(data, zip));
|
|
685
|
+
this.world.spriteDict[data.id] = this.spriteDict[data.id] = spr;
|
|
686
|
+
this.spriteList.push(spr);
|
|
687
|
+
this.world.spriteList.push(spr);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Adds a sprite.
|
|
693
|
+
* @param {object} sprite - The sprite.
|
|
694
|
+
*/
|
|
695
|
+
addSprite = (sprite) => {
|
|
696
|
+
sprite.zone = this;
|
|
697
|
+
this.world.spriteDict[sprite.id] = this.spriteDict[sprite.id] = sprite;
|
|
698
|
+
this.spriteList.push(sprite);
|
|
699
|
+
this.world.spriteList.push(sprite);
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Removes a sprite.
|
|
704
|
+
* @param {string} id - The sprite ID.
|
|
705
|
+
*/
|
|
706
|
+
removeSprite = (id) => {
|
|
707
|
+
const keep = (s) => {
|
|
708
|
+
if (s.id !== id) return true; s.removeAllActions(); return false;
|
|
709
|
+
};
|
|
710
|
+
this.spriteList = this.spriteList.filter(keep);
|
|
711
|
+
this.world.spriteList = this.world.spriteList.filter(keep);
|
|
712
|
+
delete this.spriteDict[id];
|
|
713
|
+
delete this.world.spriteDict[id];
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Removes all sprites.
|
|
718
|
+
*/
|
|
719
|
+
removeAllSprites = () => {
|
|
720
|
+
for (const s of this.spriteList) this.removeSprite(s.id);
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Gets a sprite by ID.
|
|
725
|
+
* @param {string} id - The sprite ID.
|
|
726
|
+
* @returns {object|null} The sprite.
|
|
727
|
+
*/
|
|
728
|
+
getSpriteById = (id) => this.spriteDict[id];
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Adds a portal.
|
|
732
|
+
* @param {object[]} sprites - The sprites.
|
|
733
|
+
* @param {number} x - The x position.
|
|
734
|
+
* @param {number} y - The y position.
|
|
735
|
+
* @returns {object[]} The sprites.
|
|
736
|
+
*/
|
|
737
|
+
addPortal = (sprites, x, y) => {
|
|
738
|
+
if (!this.portals?.length) return sprites;
|
|
739
|
+
const h = this.getHeight(x, y);
|
|
740
|
+
if (h !== 0) return sprites;
|
|
741
|
+
|
|
742
|
+
const make = (portal) => { portal.pos = new Vector(x, y, h); sprites.push(portal); };
|
|
743
|
+
if (this.portals.length > 0) {
|
|
744
|
+
if (((x * y) % 3) === 0) make(this.portals.shift()); else make(this.portals.pop());
|
|
745
|
+
}
|
|
746
|
+
return sprites;
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Gets the height at a position.
|
|
751
|
+
* @param {number} x - The x position.
|
|
752
|
+
* @param {number} y - The y position.
|
|
753
|
+
* @returns {number} The height.
|
|
754
|
+
*/
|
|
755
|
+
getHeight = (x, y) => {
|
|
756
|
+
if (!this.isInZone(x, y)) {
|
|
757
|
+
if (this.engine?.debug) console.error(`Height out of bounds [${x}, ${y}]`);
|
|
758
|
+
return 0;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const i = Math.floor(x), j = Math.floor(y);
|
|
762
|
+
const dp0 = x - i, dp1 = y - j;
|
|
763
|
+
|
|
764
|
+
// index into cells
|
|
765
|
+
const idx = (j - this.bounds[1]) * this.size[0] + (i - this.bounds[0]);
|
|
766
|
+
const cell = this.cells[idx];
|
|
767
|
+
const n = Math.floor(cell.length / 3);
|
|
768
|
+
|
|
769
|
+
// Get height override from heights.json if it exists for this cell
|
|
770
|
+
const heightOverride = this.heights && this.heights[j - this.bounds[1]] && typeof this.heights[j - this.bounds[1]][i - this.bounds[0]] === 'number'
|
|
771
|
+
? this.heights[j - this.bounds[1]][i - this.bounds[0]]
|
|
772
|
+
: null;
|
|
773
|
+
|
|
774
|
+
// local helper without allocations
|
|
775
|
+
const triUV = (t) => {
|
|
776
|
+
const ux = t[1][0] - t[0][0];
|
|
777
|
+
const uy = t[1][1] - t[0][1];
|
|
778
|
+
const vx = t[2][0] - t[0][0];
|
|
779
|
+
const vy = t[2][1] - t[0][1];
|
|
780
|
+
const d = 1 / (ux * vy - uy * vx);
|
|
781
|
+
const T0 = d * vy, T1 = -d * vx, T2 = -d * uy, T3 = d * ux;
|
|
782
|
+
const px = dp0 - t[0][0];
|
|
783
|
+
const py = dp1 - t[0][1];
|
|
784
|
+
return [px * T0 + py * T1, px * T2 + py * T3];
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
for (let l = 0; l < n; l++) {
|
|
788
|
+
const poly = this.tileset.getTileWalkPoly(cell[3 * l]);
|
|
789
|
+
if (!poly) continue;
|
|
790
|
+
const baseZ = (typeof cell[3 * l + 2] === 'number') ? cell[3 * l + 2] : 0;
|
|
791
|
+
// Add heightOverride to baseZ (heights.json is an offset, not a replacement)
|
|
792
|
+
const heightOffset = heightOverride !== null ? heightOverride : 0;
|
|
793
|
+
|
|
794
|
+
for (let p = 0; p < poly.length; p++) {
|
|
795
|
+
const uv = triUV(poly[p]);
|
|
796
|
+
const w = uv[0] + uv[1];
|
|
797
|
+
if (uv[0] >= 0 && uv[1] >= 0 && w <= 1) {
|
|
798
|
+
const t = poly[p];
|
|
799
|
+
const computed = baseZ + heightOffset + (1 - w) * t[0][2] + uv[0] * t[1][2] + uv[1] * t[2][2];
|
|
800
|
+
if (this.engine?.debug) {
|
|
801
|
+
this.__getHeightLogCount = (this.__getHeightLogCount || 0) + 1;
|
|
802
|
+
if (this.__getHeightLogCount < 4) console.log(`[Zone.getHeight] sample (x=${x},y=${y}) -> i=${i}, j=${j}, baseZ=${baseZ}, heightOffset=${heightOffset}, uv=[${uv[0].toFixed(2)},${uv[1].toFixed(2)}], w=${w.toFixed(2)}, computed=${computed.toFixed(2)}`);
|
|
803
|
+
}
|
|
804
|
+
return computed;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// No polygon matches - this shouldn't happen if walkPoly is properly defined
|
|
810
|
+
// Use the walkPoly itself for fallback by finding closest triangle
|
|
811
|
+
if (n > 0) {
|
|
812
|
+
const poly = this.tileset.getTileWalkPoly(cell[0]);
|
|
813
|
+
if (poly && poly.length > 0) {
|
|
814
|
+
const baseZ = (typeof cell[2] === 'number') ? cell[2] : 0;
|
|
815
|
+
const heightOffset = heightOverride !== null ? heightOverride : 0;
|
|
816
|
+
// Use first triangle's average as fallback
|
|
817
|
+
const t = poly[0];
|
|
818
|
+
const avgZ = baseZ + heightOffset + (t[0][2] + t[1][2] + t[2][2]) / 3;
|
|
819
|
+
if (this.engine?.debug) console.log(`[Zone.getHeight] walkPoly fallback for (${x},${y}), using avg of first tri = ${avgZ.toFixed(2)}`);
|
|
820
|
+
return avgZ;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Final fallback: add heightOffset to cell base z
|
|
825
|
+
const baseZ = (typeof cell[2] === 'number') ? cell[2] : 0;
|
|
826
|
+
const heightOffset = heightOverride !== null ? heightOverride : 0;
|
|
827
|
+
return baseZ + heightOffset;
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Draws a row.
|
|
832
|
+
* @param {number} row - The row.
|
|
833
|
+
* @param {Set<string>} selectedSet - The selected set.
|
|
834
|
+
* @param {number[]} highlight - The highlight.
|
|
835
|
+
* @param {object} rm - The render manager.
|
|
836
|
+
* @param {object} shaderProgram - The shader program.
|
|
837
|
+
* @param {object} pickerProgram - The picker program.
|
|
838
|
+
* @param {WebGLRenderingContext} gl - The WebGL context.
|
|
839
|
+
*/
|
|
840
|
+
drawRow = (row, selectedSet, highlight, rm, shaderProgram, pickerProgram, gl) => {
|
|
841
|
+
// Guard: Check if row data exists
|
|
842
|
+
if (!this.cellVertexPosBuf || !this.cellVertexPosBuf[row]) {
|
|
843
|
+
return; // Skip row if not initialized
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Attach tileset once per row (sprites may switch textures between rows)
|
|
847
|
+
this.tileset.texture.attach();
|
|
848
|
+
const vPosRow = this.cellVertexPosBuf[row];
|
|
849
|
+
const vTexRow = this.cellVertexTexBuf[row];
|
|
850
|
+
const width = this.size[0];
|
|
851
|
+
|
|
852
|
+
// If we have cached picking IDs, use them without checks in the loop
|
|
853
|
+
const pickingRow = this.cellPickingId[row];
|
|
854
|
+
|
|
855
|
+
for (let cell = 0; cell < width; cell++) {
|
|
856
|
+
const vPos = vPosRow[cell];
|
|
857
|
+
const vTex = vTexRow[cell];
|
|
858
|
+
|
|
859
|
+
// Guard: Skip cells with no vertices (empty or failed tile lookup)
|
|
860
|
+
if (!vPos || !vTex || vPos.numItems === 0) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
rm.bindBuffer(vPos, shaderProgram.aVertexPosition);
|
|
865
|
+
rm.bindBuffer(vTex, shaderProgram.aTextureCoord);
|
|
866
|
+
|
|
867
|
+
const id = pickingRow[cell];
|
|
868
|
+
pickerProgram.setMatrixUniforms({ id });
|
|
869
|
+
shaderProgram.setMatrixUniforms({
|
|
870
|
+
id,
|
|
871
|
+
isSelected: selectedSet ? selectedSet.has(`${row},${cell}`) : false,
|
|
872
|
+
sampler: 1.0,
|
|
873
|
+
colorMultiplier: highlight,
|
|
874
|
+
});
|
|
875
|
+
gl.drawArrays(gl.TRIANGLES, 0, vPos.numItems);
|
|
876
|
+
if (rm.debug) rm.debug.tilesDrawn++;
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Draws a cell.
|
|
882
|
+
* @param {number} row - The row.
|
|
883
|
+
* @param {number} cell - The cell.
|
|
884
|
+
*/
|
|
885
|
+
drawCell = (row, cell) => {
|
|
886
|
+
const rm = this.engine.renderManager;
|
|
887
|
+
const gl = this.engine.gl;
|
|
888
|
+
const shader = rm.shaderProgram;
|
|
889
|
+
const picker = rm.effectPrograms['picker'];
|
|
890
|
+
const isPickerPass = rm.isPickerPass;
|
|
891
|
+
|
|
892
|
+
const vPos = this.cellVertexPosBuf[row][cell];
|
|
893
|
+
const vTex = this.cellVertexTexBuf[row][cell];
|
|
894
|
+
rm.bindBuffer(vPos, shader.aVertexPosition);
|
|
895
|
+
rm.bindBuffer(vTex, shader.aTextureCoord);
|
|
896
|
+
|
|
897
|
+
const id = this.cellPickingId[row][cell];
|
|
898
|
+
|
|
899
|
+
if (isPickerPass) {
|
|
900
|
+
// During picker pass, only set picker shader uniforms
|
|
901
|
+
picker.setMatrixUniforms({ id });
|
|
902
|
+
} else {
|
|
903
|
+
// During normal render, set main shader uniforms
|
|
904
|
+
shader.setMatrixUniforms({
|
|
905
|
+
id,
|
|
906
|
+
isSelected: !!this._selectedSet?.has(`${row},${cell}`),
|
|
907
|
+
sampler: 1.0,
|
|
908
|
+
colorMultiplier: this._highlight || [1, 1, 0, 1],
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
gl.drawArrays(gl.TRIANGLES, 0, vPos.numItems);
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Draws the zone.
|
|
916
|
+
*/
|
|
917
|
+
draw = () => {
|
|
918
|
+
if (!this.loaded) return;
|
|
919
|
+
|
|
920
|
+
const rm = this.engine.renderManager;
|
|
921
|
+
const gl = this.engine.gl;
|
|
922
|
+
const shaderProgram = rm.shaderProgram;
|
|
923
|
+
const pickerProgram = rm.effectPrograms['picker'];
|
|
924
|
+
|
|
925
|
+
// Build selected set once per frame
|
|
926
|
+
const sel = this.selectedTiles;
|
|
927
|
+
this.selectedSet = (sel && sel.length) ? new Set(sel.map((t) => `${t[0]},${t[1]}`)) : null;
|
|
928
|
+
this.highlight = (this.engine.frameCount & 0x8) ? [1, 0, 0, 1] : [1, 1, 0, 1];
|
|
929
|
+
|
|
930
|
+
// look into this
|
|
931
|
+
const ensureSortedByY = (arr) => {
|
|
932
|
+
for (let i = 1; i < arr.length; i++) if (arr[i - 1].pos.y > arr[i].pos.y) { arr.sort((a, b) => a.pos.y - b.pos.y); break; }
|
|
933
|
+
};
|
|
934
|
+
ensureSortedByY(this.spriteList);
|
|
935
|
+
ensureSortedByY(this.objectList);
|
|
936
|
+
|
|
937
|
+
rm.mvPushMatrix();
|
|
938
|
+
// Do not reinitialize the camera when FreeCam is active — FreeCam edits camera.uViewMat directly
|
|
939
|
+
if (!this.engine._freecamActive) rm.camera.setCamera();
|
|
940
|
+
|
|
941
|
+
let si = 0; // sprite index
|
|
942
|
+
let oi = 0; // object index
|
|
943
|
+
|
|
944
|
+
// Need to update to handle the different directions (there are some issues with clipping on other angles)
|
|
945
|
+
const drawForward = this.engine.renderManager.camera.cameraDir === 'N' ||
|
|
946
|
+
this.engine.renderManager.camera.cameraDir === 'NE' ||
|
|
947
|
+
this.engine.renderManager.camera.cameraDir === 'NW' ||
|
|
948
|
+
this.engine.renderManager.camera.cameraDir === 'E';
|
|
949
|
+
|
|
950
|
+
if (drawForward) {
|
|
951
|
+
for (let j = 0; j < this.size[1]; j++) {
|
|
952
|
+
this.drawRow(j, this.selectedSet, this.highlight, rm, shaderProgram, pickerProgram, gl);
|
|
953
|
+
while (oi < this.objectList.length && (this.objectList[oi].pos.y - this.bounds[1]) <= j) this.objectList[oi++].draw();
|
|
954
|
+
while (si < this.spriteList.length && (this.spriteList[si].pos.y - this.bounds[1]) <= j) this.spriteList[si++].draw(this.engine);
|
|
955
|
+
}
|
|
956
|
+
} else {
|
|
957
|
+
for (let j = this.size[1] - 1; j >= 0; j--) {
|
|
958
|
+
this.drawRow(j, this.selectedSet, this.highlight, rm, shaderProgram, pickerProgram, gl);
|
|
959
|
+
while (oi < this.objectList.length && (this.bounds[1] - this.objectList[oi].pos.y) <= j) this.objectList[oi++].draw();
|
|
960
|
+
while (si < this.spriteList.length && (this.bounds[1] - this.spriteList[si].pos.y) <= j) this.spriteList[si++].draw(this.engine);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
while (oi < this.objectList.length) this.objectList[oi++].draw();
|
|
965
|
+
while (si < this.spriteList.length) this.spriteList[si++].draw(this.engine);
|
|
966
|
+
|
|
967
|
+
rm.mvPopMatrix();
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Ticks the zone.
|
|
972
|
+
* @param {number} time - The time.
|
|
973
|
+
* @param {boolean} isPaused - Whether paused.
|
|
974
|
+
*/
|
|
975
|
+
tick = (time, isPaused) => {
|
|
976
|
+
if (!this.loaded || isPaused) return;
|
|
977
|
+
this.checkInput(time);
|
|
978
|
+
for (const s of this.spriteList) s.tickOuter(time);
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Checks input.
|
|
983
|
+
* @param {number} time - The time.
|
|
984
|
+
*/
|
|
985
|
+
checkInput = async (time) => {
|
|
986
|
+
if (time <= this.lastKey + 200) return;
|
|
987
|
+
this.engine.gamepad.checkInput();
|
|
988
|
+
this.lastKey = time;
|
|
989
|
+
// todo - look into hooks - game modes (allow for scripting keymaps)
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Checks if a position is in the zone.
|
|
994
|
+
* @param {number} x - The x position.
|
|
995
|
+
* @param {number} y - The y position.
|
|
996
|
+
* @returns {boolean} Whether in zone.
|
|
997
|
+
*/
|
|
998
|
+
isInZone = (x, y) => (x >= this.bounds[0] && y >= this.bounds[1] && x < this.bounds[2] && y < this.bounds[3]);
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Handles selection.
|
|
1002
|
+
* @param {number} row - The row.
|
|
1003
|
+
* @param {number} cell - The cell.
|
|
1004
|
+
*/
|
|
1005
|
+
onSelect = async (row, cell) => {
|
|
1006
|
+
// allow active mode to intercept selection
|
|
1007
|
+
try {
|
|
1008
|
+
if (this.world?.modeManager && this.world.modeManager.handleSelect) {
|
|
1009
|
+
debug('Zone', 'Running Custom Select Handler')
|
|
1010
|
+
const handled = await this.world.modeManager.handleSelect(this, row, cell, 'tile');
|
|
1011
|
+
if (handled) return; // mode consumed selection
|
|
1012
|
+
}
|
|
1013
|
+
} catch (e) { console.warn('mode selection handler error', e); }
|
|
1014
|
+
// toggle select
|
|
1015
|
+
let removed = false;
|
|
1016
|
+
this.selectedTiles = this.selectedTiles.filter((t) => {
|
|
1017
|
+
const keep = !(t[0] === row && t[1] === cell);
|
|
1018
|
+
if (!keep) removed = true;
|
|
1019
|
+
return keep;
|
|
1020
|
+
});
|
|
1021
|
+
if (removed) return;
|
|
1022
|
+
|
|
1023
|
+
this.selectedTiles.push([row, cell]);
|
|
1024
|
+
if (!this.selectTrigger) return;
|
|
1025
|
+
|
|
1026
|
+
// Lua trigger from spritz zip
|
|
1027
|
+
try {
|
|
1028
|
+
let file = this.engine.spritz.zip.file(`triggers/${this.selectTrigger}.pxs`);
|
|
1029
|
+
if (!file) file = this.engine.spritz.zip.file(`triggers/${this.selectTrigger}.pxs`);
|
|
1030
|
+
if (!file) throw new Error('No Lua Script Found');
|
|
1031
|
+
const luaScript = await file.async('string');
|
|
1032
|
+
const interpreter = new PixoScriptInterpreter(this.engine);
|
|
1033
|
+
interpreter.setScope({ _this: this, zone: this, subject: new interpreter.pxs.Table([row, cell]) });
|
|
1034
|
+
interpreter.initLibrary();
|
|
1035
|
+
return await interpreter.run(luaScript);
|
|
1036
|
+
} catch (e) {
|
|
1037
|
+
if (this.engine?.debug) console.warn('select trigger missing', e);
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Checks if walkable.
|
|
1043
|
+
* @param {number} x - The x position.
|
|
1044
|
+
* @param {number} y - The y position.
|
|
1045
|
+
* @param {number} direction - The direction.
|
|
1046
|
+
* @returns {boolean|null} Whether walkable.
|
|
1047
|
+
*/
|
|
1048
|
+
isWalkable = (x, y, direction) => {
|
|
1049
|
+
if (!this.isInZone(x, y)) return null;
|
|
1050
|
+
|
|
1051
|
+
// sprites (values, not keys)
|
|
1052
|
+
for (const sId in this.spriteDict) {
|
|
1053
|
+
const s = this.spriteDict[sId];
|
|
1054
|
+
if (s.pos.x !== x || s.pos.y !== y) continue;
|
|
1055
|
+
if (!s.walkable && !s.blocking && s.override) return true; // bypass/override
|
|
1056
|
+
if (!s.walkable && s.blocking) return false; // blocking
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// objects (AABB-lite checks)
|
|
1060
|
+
for (const oId in this.objectDict) {
|
|
1061
|
+
const o = this.objectDict[oId];
|
|
1062
|
+
const minX = o.pos.x - o.scale.x * (o.size.x / 2);
|
|
1063
|
+
const minY = o.pos.y - o.scale.y * (o.size.y / 2);
|
|
1064
|
+
const withinX = (xx, a, b, inc = false) => (inc ? (xx >= a && xx <= b) : (xx > a && xx < b));
|
|
1065
|
+
const xHit = withinX(x, minX, o.pos.x, true);
|
|
1066
|
+
const yHit = withinX(y, minY, o.pos.y, true);
|
|
1067
|
+
|
|
1068
|
+
if (!o.walkable && xHit && yHit && !o.blocking && o.override) return true;
|
|
1069
|
+
if (!o.walkable && ((o.pos.x === x && o.pos.y === y) || (xHit && yHit)) && o.blocking) return false;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// tile walkability
|
|
1073
|
+
return (this.walkability[(y - this.bounds[1]) * this.size[0] + (x - this.bounds[0])] & direction) !== 0;
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Checks if within range.
|
|
1078
|
+
* @param {number} x - The value.
|
|
1079
|
+
* @param {number} a - The min.
|
|
1080
|
+
* @param {number} b - The max.
|
|
1081
|
+
* @param {boolean} [include=false] - Whether inclusive.
|
|
1082
|
+
* @returns {boolean} Whether within.
|
|
1083
|
+
*/
|
|
1084
|
+
within = (x, a, b, include = false) => (include ? (x >= a && x <= b) : (x > a && x < b));
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Triggers a script.
|
|
1088
|
+
* @param {string} id - The script ID.
|
|
1089
|
+
*/
|
|
1090
|
+
triggerScript = (id) => {
|
|
1091
|
+
for (const x of this.scripts) if (x.id === id) this.runWhenLoaded(x.trigger.bind(this));
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Moves a sprite.
|
|
1096
|
+
* @param {string} id - The sprite ID.
|
|
1097
|
+
* @param {number[]} location - The location.
|
|
1098
|
+
* @param {boolean} [running=false] - Whether running.
|
|
1099
|
+
* @returns {Promise} The promise.
|
|
1100
|
+
*/
|
|
1101
|
+
moveSprite = async (id, location, running = false) => new Promise(async (resolve) => {
|
|
1102
|
+
const sprite = this.getSpriteById(id);
|
|
1103
|
+
await sprite.addAction(new ActionLoader(this.engine, 'patrol', [sprite.pos.toArray(), location, running ? 200 : 600, this], sprite, resolve));
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Shows sprite dialogue.
|
|
1108
|
+
* @param {string} id - The sprite ID.
|
|
1109
|
+
* @param {string} dialogue - The dialogue.
|
|
1110
|
+
* @param {object} [options={ autoclose: true }] - The options.
|
|
1111
|
+
* @returns {Promise} The promise.
|
|
1112
|
+
*/
|
|
1113
|
+
spriteDialogue = async (id, dialogue, options = { autoclose: true }) => new Promise(async (resolve) => {
|
|
1114
|
+
const sprite = this.getSpriteById(id);
|
|
1115
|
+
await sprite.addAction(new ActionLoader(this.engine, 'dialogue', [dialogue, false, options], sprite, resolve));
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Runs actions.
|
|
1120
|
+
* @param {object[]} actions - The actions.
|
|
1121
|
+
* @returns {Promise} The promise.
|
|
1122
|
+
*/
|
|
1123
|
+
runActions = async (actions) => {
|
|
1124
|
+
const scope = this;
|
|
1125
|
+
let p = Promise.resolve();
|
|
1126
|
+
for (const action of actions) {
|
|
1127
|
+
p = p.then(async () => {
|
|
1128
|
+
if (!action) return;
|
|
1129
|
+
try {
|
|
1130
|
+
action.scope = action.scope || scope;
|
|
1131
|
+
if (action.sprite) {
|
|
1132
|
+
const sprite = action.scope.getSpriteById(action.sprite);
|
|
1133
|
+
if (sprite && action.action) {
|
|
1134
|
+
const args = [...action.args];
|
|
1135
|
+
const options = args.pop();
|
|
1136
|
+
await sprite.addAction(new ActionLoader(scope.engine, action.action, [...args, { ...options }], sprite, () => { }));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (action.trigger) {
|
|
1140
|
+
const avatar = action.scope.getSpriteById('avatar');
|
|
1141
|
+
if (avatar) await avatar.addAction(new ActionLoader(scope.engine, 'script', [action.trigger, action.scope, () => { }], avatar));
|
|
1142
|
+
}
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
console.warn('runActions error', e?.message || e);
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
return p.catch((err) => { if (this.engine?.debug) console.warn('runActions chain', err); });
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Plays a cutscene.
|
|
1153
|
+
* @param {string} id - The cutscene ID.
|
|
1154
|
+
* @param {object} [spritz=null] - The spritz.
|
|
1155
|
+
* @returns {Promise} The promise.
|
|
1156
|
+
*/
|
|
1157
|
+
playCutScene = async (id, spritz = null) => {
|
|
1158
|
+
const seq = spritz || this.spritz;
|
|
1159
|
+
for (const x of seq) {
|
|
1160
|
+
try {
|
|
1161
|
+
x.currentStep = x.currentStep || 0;
|
|
1162
|
+
if (x.currentStep > seq.length) continue;
|
|
1163
|
+
if (x.id === id) await this.runActions(x.actions);
|
|
1164
|
+
} catch (e) { console.error(e); }
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
}
|