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.
Files changed (157) hide show
  1. package/README.md +36 -286
  2. package/dist/bundle.js +13 -3
  3. package/dist/bundle.js.map +1 -1
  4. package/dist/style.css +1 -0
  5. package/package.json +43 -44
  6. package/src/components/WebGLView.jsx +318 -0
  7. package/src/css/pixos.css +372 -0
  8. package/src/engine/actions/animate.js +41 -0
  9. package/src/engine/actions/changezone.js +135 -0
  10. package/src/engine/actions/chat.js +109 -0
  11. package/src/engine/actions/dialogue.js +90 -0
  12. package/src/engine/actions/face.js +22 -0
  13. package/src/engine/actions/greeting.js +28 -0
  14. package/src/engine/actions/interact.js +86 -0
  15. package/src/engine/actions/move.js +67 -0
  16. package/src/engine/actions/patrol.js +109 -0
  17. package/src/engine/actions/prompt.js +185 -0
  18. package/src/engine/actions/script.js +42 -0
  19. package/src/engine/core/audio/AudioSystem.js +543 -0
  20. package/src/engine/core/cutscene/PxcPlayer.js +956 -0
  21. package/src/engine/core/cutscene/manager.js +243 -0
  22. package/src/engine/core/database/index.js +75 -0
  23. package/src/engine/core/debug/index.js +371 -0
  24. package/src/engine/core/hud/index.js +765 -0
  25. package/src/engine/core/index.js +540 -0
  26. package/src/engine/core/input/gamepad/Controller.js +71 -0
  27. package/src/engine/core/input/gamepad/ControllerButtons.js +231 -0
  28. package/src/engine/core/input/gamepad/ControllerStick.js +173 -0
  29. package/src/engine/core/input/gamepad/index.js +592 -0
  30. package/src/engine/core/input/keyboard.js +196 -0
  31. package/src/engine/core/input/manager.js +485 -0
  32. package/src/engine/core/input/mouse.js +203 -0
  33. package/src/engine/core/input/touch.js +175 -0
  34. package/src/engine/core/mode/manager.js +199 -0
  35. package/src/engine/core/net/manager.js +535 -0
  36. package/src/engine/core/queue/action.js +83 -0
  37. package/src/engine/core/queue/event.js +82 -0
  38. package/src/engine/core/queue/index.js +44 -0
  39. package/src/engine/core/queue/loadable.js +33 -0
  40. package/src/engine/core/render/CameraEffects.js +494 -0
  41. package/src/engine/core/render/FrustumCuller.js +417 -0
  42. package/src/engine/core/render/LODManager.js +285 -0
  43. package/src/engine/core/render/ParticleManager.js +529 -0
  44. package/src/engine/core/render/TextureAtlas.js +465 -0
  45. package/src/engine/core/render/camera.js +338 -0
  46. package/src/engine/core/render/light.js +197 -0
  47. package/src/engine/core/render/manager.js +1079 -0
  48. package/src/engine/core/render/shaders.js +110 -0
  49. package/src/engine/core/render/skybox.js +342 -0
  50. package/src/engine/core/resource/manager.js +133 -0
  51. package/src/engine/core/resource/object.js +611 -0
  52. package/src/engine/core/resource/texture.js +103 -0
  53. package/src/engine/core/resource/tileset.js +177 -0
  54. package/src/engine/core/scene/avatar.js +215 -0
  55. package/src/engine/core/scene/speech.js +138 -0
  56. package/src/engine/core/scene/sprite.js +702 -0
  57. package/src/engine/core/scene/spritz.js +189 -0
  58. package/src/engine/core/scene/world.js +681 -0
  59. package/src/engine/core/scene/zone.js +1167 -0
  60. package/src/engine/core/store/index.js +110 -0
  61. package/src/engine/dynamic/animatedSprite.js +64 -0
  62. package/src/engine/dynamic/animatedTile.js +98 -0
  63. package/src/engine/dynamic/avatar.js +110 -0
  64. package/src/engine/dynamic/map.js +174 -0
  65. package/src/engine/dynamic/sprite.js +255 -0
  66. package/src/engine/dynamic/spritz.js +119 -0
  67. package/src/engine/events/EventSystem.js +609 -0
  68. package/src/engine/events/camera.js +142 -0
  69. package/src/engine/events/chat.js +75 -0
  70. package/src/engine/events/menu.js +186 -0
  71. package/src/engine/scripting/CallbackManager.js +514 -0
  72. package/src/engine/scripting/PixoScriptInterpreter.js +81 -0
  73. package/src/engine/scripting/PixoScriptLibrary.js +704 -0
  74. package/src/engine/shaders/effects/index.js +450 -0
  75. package/src/engine/shaders/fs.js +222 -0
  76. package/src/engine/shaders/particles/fs.js +41 -0
  77. package/src/engine/shaders/particles/vs.js +61 -0
  78. package/src/engine/shaders/picker/fs.js +34 -0
  79. package/src/engine/shaders/picker/init.js +62 -0
  80. package/src/engine/shaders/picker/vs.js +42 -0
  81. package/src/engine/shaders/pxsl/README.md +250 -0
  82. package/src/engine/shaders/pxsl/index.js +25 -0
  83. package/src/engine/shaders/pxsl/library.js +608 -0
  84. package/src/engine/shaders/pxsl/manager.js +338 -0
  85. package/src/engine/shaders/pxsl/specification.js +363 -0
  86. package/src/engine/shaders/pxsl/transpiler.js +753 -0
  87. package/src/engine/shaders/skybox/cosmic/fs.js +147 -0
  88. package/src/engine/shaders/skybox/cosmic/vs.js +23 -0
  89. package/src/engine/shaders/skybox/matrix/fs.js +127 -0
  90. package/src/engine/shaders/skybox/matrix/vs.js +23 -0
  91. package/src/engine/shaders/skybox/morning/fs.js +109 -0
  92. package/src/engine/shaders/skybox/morning/vs.js +23 -0
  93. package/src/engine/shaders/skybox/neon/fs.js +119 -0
  94. package/src/engine/shaders/skybox/neon/vs.js +23 -0
  95. package/src/engine/shaders/skybox/sky/fs.js +114 -0
  96. package/src/engine/shaders/skybox/sky/vs.js +23 -0
  97. package/src/engine/shaders/skybox/sunset/fs.js +101 -0
  98. package/src/engine/shaders/skybox/sunset/vs.js +23 -0
  99. package/src/engine/shaders/transition/blur/fs.js +42 -0
  100. package/src/engine/shaders/transition/blur/vs.js +26 -0
  101. package/src/engine/shaders/transition/cross/fs.js +36 -0
  102. package/src/engine/shaders/transition/cross/vs.js +26 -0
  103. package/src/engine/shaders/transition/crossBlur/fs.js +41 -0
  104. package/src/engine/shaders/transition/crossBlur/vs.js +25 -0
  105. package/src/engine/shaders/transition/dissolve/fs.js +78 -0
  106. package/src/engine/shaders/transition/dissolve/vs.js +24 -0
  107. package/src/engine/shaders/transition/fade/fs.js +31 -0
  108. package/src/engine/shaders/transition/fade/vs.js +27 -0
  109. package/src/engine/shaders/transition/iris/fs.js +52 -0
  110. package/src/engine/shaders/transition/iris/vs.js +24 -0
  111. package/src/engine/shaders/transition/pixelate/fs.js +44 -0
  112. package/src/engine/shaders/transition/pixelate/vs.js +24 -0
  113. package/src/engine/shaders/transition/slide/fs.js +53 -0
  114. package/src/engine/shaders/transition/slide/vs.js +24 -0
  115. package/src/engine/shaders/transition/swirl/fs.js +39 -0
  116. package/src/engine/shaders/transition/swirl/vs.js +26 -0
  117. package/src/engine/shaders/transition/wipe/fs.js +50 -0
  118. package/src/engine/shaders/transition/wipe/vs.js +24 -0
  119. package/src/engine/shaders/vs.js +60 -0
  120. package/src/engine/utils/CameraController.js +506 -0
  121. package/src/engine/utils/ObjHelper.js +551 -0
  122. package/src/engine/utils/debug-logger.js +110 -0
  123. package/src/engine/utils/enums.js +305 -0
  124. package/src/engine/utils/generator.js +156 -0
  125. package/src/engine/utils/index.js +21 -0
  126. package/src/engine/utils/loaders/ActionLoader.js +77 -0
  127. package/src/engine/utils/loaders/AudioLoader.js +157 -0
  128. package/src/engine/utils/loaders/EventLoader.js +66 -0
  129. package/src/engine/utils/loaders/ObjectLoader.js +67 -0
  130. package/src/engine/utils/loaders/SpriteLoader.js +77 -0
  131. package/src/engine/utils/loaders/TilesetLoader.js +103 -0
  132. package/src/engine/utils/loaders/index.js +21 -0
  133. package/src/engine/utils/math/matrix4.js +367 -0
  134. package/src/engine/utils/math/vector.js +458 -0
  135. package/src/engine/utils/obj/_old_js/index.js +46 -0
  136. package/src/engine/utils/obj/_old_js/layout.js +308 -0
  137. package/src/engine/utils/obj/_old_js/material.js +711 -0
  138. package/src/engine/utils/obj/_old_js/mesh.js +761 -0
  139. package/src/engine/utils/obj/_old_js/utils.js +647 -0
  140. package/src/engine/utils/obj/index.js +24 -0
  141. package/src/engine/utils/obj/js/index.js +277 -0
  142. package/src/engine/utils/obj/js/loader.js +232 -0
  143. package/src/engine/utils/obj/layout.js +246 -0
  144. package/src/engine/utils/obj/material.js +665 -0
  145. package/src/engine/utils/obj/mesh.js +657 -0
  146. package/src/engine/utils/obj/ts/index.ts +72 -0
  147. package/src/engine/utils/obj/ts/layout.ts +265 -0
  148. package/src/engine/utils/obj/ts/material.ts +760 -0
  149. package/src/engine/utils/obj/ts/mesh.ts +785 -0
  150. package/src/engine/utils/obj/ts/utils.ts +501 -0
  151. package/src/engine/utils/obj/utils.js +428 -0
  152. package/src/engine/utils/resources.js +18 -0
  153. package/src/index.jsx +55 -0
  154. package/src/spritz/player.js +18 -0
  155. package/src/spritz/readme.md +18 -0
  156. package/LICENSE +0 -437
  157. package/dist/bundle.js.LICENSE.txt +0 -31
@@ -0,0 +1,681 @@
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 World class for Pixos game engine.
16
+ * Manages zones, sprites, and game state.
17
+ */
18
+
19
+ import Zone from './zone.js';
20
+ import { debug } from '@Engine/utils/debug-logger.js';
21
+ import ModeManager from '../mode/manager.js';
22
+ import ActionQueue from '../queue/index.js';
23
+ import { Direction } from '@Engine/utils/enums.js';
24
+ import { EventLoader } from '@Engine/utils/loaders/index.js';
25
+ import Avatar from './avatar.js';
26
+ import { Vector } from '@Engine/utils/math/vector.js';
27
+ /**
28
+ * @typedef {object} MenuConfig
29
+ * @property {object} start - Start menu configuration.
30
+ */
31
+
32
+ /**
33
+ * World - Manages the game world including zones, sprites, and events.
34
+ */
35
+ export default class World {
36
+ /**
37
+ * Creates an instance of World.
38
+ * @param {object} spritz - The spritz instance.
39
+ * @param {string} id - The world ID.
40
+ */
41
+ constructor(spritz, id) {
42
+ /** @type {string} */
43
+ this.id = id;
44
+ /** @type {object} */
45
+ this.spritz = spritz;
46
+ /** @type {number} */
47
+ this.objId = Math.round(Math.random() * 1000) + 1;
48
+ /** @type {import('../index.js').default} */
49
+ this.engine = spritz.engine;
50
+ /** @type {Object.<string, Zone>} */
51
+ this.zoneDict = {};
52
+ /** @type {Zone[]} */
53
+ this.zoneList = [];
54
+ /** @type {Object.<string, object>} */
55
+ this.remoteAvatars = new Map();
56
+ /** @type {Object.<string, object>} */
57
+ this.spriteDict = {};
58
+ /** @type {object[]} */
59
+ this.spriteList = [];
60
+ /** @type {Object.<string, object>} */
61
+ this.objectDict = {};
62
+ /** @type {object[]} */
63
+ this.objectList = [];
64
+ /** @type {Object.<string, object>} */
65
+ this.tilesetDict = {};
66
+ /** @type {object[]} */
67
+ this.tilesetList = [];
68
+ /** @type {object[]} */
69
+ this.eventList = [];
70
+ /** @type {Object.<string, object>} */
71
+ this.eventDict = {};
72
+ /** @type {number} */
73
+ this.lastKey = new Date().getTime();
74
+ /** @type {number} */
75
+ this.lastZoneTransitionTime = 0;
76
+ /** @type {boolean} */
77
+ this.isPaused = true;
78
+ /** @type {ModeManager} */
79
+ this.modeManager = new ModeManager(this);
80
+ /** @type {ActionQueue} */
81
+ this.afterTickActions = new ActionQueue();
82
+ /** @type {MenuConfig} */
83
+ this.menuConfig = {
84
+ start: {
85
+ onOpen: (menu) => {
86
+ menu.completed = true;
87
+ },
88
+ },
89
+ };
90
+ }
91
+
92
+ addRemoteAvatar(clientId, avatarData) {
93
+ // Create and add a new avatar sprite for the remote player using engine Avatar class
94
+ try {
95
+ // If we already have this remote avatar, update and return it
96
+ if (this.remoteAvatars.has(clientId)) {
97
+ const existing = this.remoteAvatars.get(clientId);
98
+ try { debug('World', `Remote avatar for ${clientId} already exists, updating instead`); } catch (e) { }
99
+ if (avatarData.x != null) existing.pos.x = avatarData.x;
100
+ if (avatarData.y != null) existing.pos.y = avatarData.y;
101
+ if (avatarData.z != null) existing.pos.z = avatarData.z;
102
+ if (avatarData.facing != null) existing.facing = avatarData.facing;
103
+ return existing;
104
+ }
105
+ // Instantiate Avatar and try to copy template properties from the local player avatar
106
+ const avatar = new Avatar(this.engine);
107
+
108
+ // Try to find a local avatar template to copy necessary rendering/template fields
109
+ const localTemplate = this.getAvatar();
110
+ if (localTemplate) {
111
+ // Copy minimal template fields required by Sprite
112
+ avatar.src = localTemplate.src;
113
+ avatar.portraitSrc = localTemplate.portraitSrc;
114
+ avatar.sheetSize = localTemplate.sheetSize;
115
+ avatar.tileSize = localTemplate.tileSize;
116
+ avatar.frames = localTemplate.frames;
117
+ avatar.hotspotOffset = localTemplate.hotspotOffset;
118
+ avatar.drawOffset = localTemplate.drawOffset;
119
+ avatar.enableSpeech = localTemplate.enableSpeech;
120
+ avatar.bindCamera = false; // remote avatars shouldn't bind camera
121
+ // Copy runtime resources so remote avatar can render immediately
122
+ if (localTemplate.texture) avatar.texture = localTemplate.texture;
123
+ if (localTemplate.vertexTexBuf) avatar.vertexTexBuf = localTemplate.vertexTexBuf;
124
+ if (localTemplate.vertexPosBuf) avatar.vertexPosBuf = localTemplate.vertexPosBuf;
125
+ if (localTemplate.speech && localTemplate.speechTexBuf) avatar.speech = localTemplate.speech, avatar.speechTexBuf = localTemplate.speechTexBuf;
126
+ // mark as loaded so draw will render without waiting for async onLoad
127
+ avatar.loaded = true;
128
+ avatar.templateLoaded = true;
129
+ } else {
130
+ console.warn('No local avatar template found; remote avatar may not render correctly');
131
+ }
132
+
133
+ // Ensure unique sprite id to avoid collisions with local 'avatar' id
134
+ const baseId = avatarData.id || 'player';
135
+ const spriteId = `${baseId}-${clientId}`;
136
+
137
+ // Set properties and create buffers synchronously
138
+ const zone = this.getZoneById(avatarData.zone || avatarData.zoneId) || this.zoneContaining(avatarData.x || 0, avatarData.y || 0);
139
+ avatar.zone = zone;
140
+ avatar.id = spriteId;
141
+ // compute z if not provided. Use hotspot offset so we sample tile height for avatar foot position.
142
+ const rawX = avatarData.x ?? (avatarData.pos && avatarData.pos.x) ?? 0;
143
+ const rawY = avatarData.y ?? (avatarData.pos && avatarData.pos.y) ?? 0;
144
+ const hx = rawX + (avatar.hotspotOffset?.x ?? 0);
145
+ const hy = rawY + (avatar.hotspotOffset?.y ?? 0);
146
+ const zVal = (typeof avatarData.z === 'number') ? avatarData.z : (avatarData.pos && typeof avatarData.pos.z === 'number') ? avatarData.pos.z : (zone ? zone.getHeight(hx, hy) : 0);
147
+ avatar.pos = new Vector(rawX, rawY, zVal);
148
+ avatar.facing = avatarData.facing || 0;
149
+ avatar.isSelected = false; // remote avatars not selected
150
+
151
+ // Create buffers synchronously with fallback tile size
152
+ let tileSize = (zone && zone.tileset && zone.tileset.tileSize) ? zone.tileset.tileSize : 32;
153
+ let normTile = [avatar.tileSize[0] / tileSize, avatar.tileSize[1] / tileSize];
154
+ let verts = [
155
+ [0, 0, 0],
156
+ [normTile[0], 0, 0],
157
+ [normTile[0], 0, normTile[1]],
158
+ [0, 0, normTile[1]],
159
+ ];
160
+ let poly = [
161
+ [verts[2], verts[3], verts[0]],
162
+ [verts[2], verts[0], verts[1]]
163
+ ].flat(3);
164
+ avatar.vertexPosBuf = this.engine.renderManager.createBuffer(poly, this.engine.gl.STATIC_DRAW, 3);
165
+ let texCoords = avatar.getTexCoords();
166
+ avatar.vertexTexBuf = this.engine.renderManager.createBuffer(texCoords, this.engine.gl.DYNAMIC_DRAW, 2);
167
+ if (avatar.enableSpeech) {
168
+ avatar.speechVerBuf = this.engine.renderManager.createBuffer(avatar.getSpeechBubbleVertices(), this.engine.gl.STATIC_DRAW, 3);
169
+ avatar.speechTexBuf = this.engine.renderManager.createBuffer(avatar.getSpeechBubbleTexture(), this.engine.gl.DYNAMIC_DRAW, 2);
170
+ }
171
+
172
+ // Add to the zone if available. Ensure id/zone registration happens *before* we store
173
+ // this.remoteAvatars to avoid updates arriving before registration completes.
174
+ if (zone) {
175
+ // Ensure zone has spriteDict and spriteList
176
+ if (!zone.spriteDict) zone.spriteDict = {};
177
+ if (!zone.spriteList) zone.spriteList = [];
178
+ // register in dictionaries and lists synchronously
179
+ this.spriteDict[avatar.id] = avatar;
180
+ zone.spriteDict[avatar.id] = avatar;
181
+ if (!zone.spriteList.includes(avatar)) zone.spriteList.push(avatar);
182
+ if (!this.spriteList.includes(avatar)) this.spriteList.push(avatar);
183
+ debug('World', `Added remote avatar for client ${clientId} as sprite '${avatar.id}' to zone ${zone.id} at (${avatar.pos.x},${avatar.pos.y},${avatar.pos.z})`);
184
+ }
185
+
186
+ // store mapping after registration
187
+ this.remoteAvatars.set(clientId, avatar);
188
+ try { debug('World', `Remote avatar map now has ${this.remoteAvatars.size} entries`); } catch (e) { }
189
+ return avatar;
190
+ } catch (e) {
191
+ console.warn('Failed to add remote avatar', e);
192
+ return null;
193
+ }
194
+ }
195
+
196
+ removeRemoteAvatar(clientId) {
197
+ const avatar = this.remoteAvatars.get(clientId);
198
+ if (avatar) {
199
+ try {
200
+ if (avatar.zone) {
201
+ // remove by id if possible
202
+ const idToRemove = avatar.id || (avatar.objId ? avatar.objId : null);
203
+ if (idToRemove) avatar.zone.removeSprite(idToRemove);
204
+ else avatar.zone.removeSprite(avatar);
205
+ }
206
+ } catch (e) {
207
+ try { if (avatar.zone) avatar.zone.removeSprite(avatar); } catch (e2) { }
208
+ }
209
+ this.remoteAvatars.delete(clientId);
210
+ }
211
+ }
212
+
213
+ updateRemoteAvatar(clientId, avatarData) {
214
+ const avatar = this.remoteAvatars.get(clientId);
215
+ if (avatar) {
216
+ try { debug('World', `updateRemoteAvatar: client=${clientId} pre pos=${avatar.pos?.x},${avatar.pos?.y},${avatar.pos?.z} loaded=${avatar.loaded} id=${avatar.id} zone=${avatar.zone?.id}`); } catch (e) { }
217
+ if (typeof avatar.setPosition === 'function') {
218
+ avatar.setPosition(avatarData.x, avatarData.y, avatarData.z);
219
+ } else if (avatar.pos) {
220
+ avatar.pos.x = avatarData.x;
221
+ avatar.pos.y = avatarData.y;
222
+ avatar.pos.z = avatarData.z || avatar.pos.z;
223
+ }
224
+ if (typeof avatar.updateState === 'function') {
225
+ avatar.updateState(avatarData);
226
+ } else {
227
+ // fallback: apply facing and animation frame
228
+ if (avatarData.facing != null) avatar.facing = avatarData.facing;
229
+ if (avatarData.animFrame != null) avatar.animFrame = avatarData.animFrame;
230
+ }
231
+ // Defensive: ensure sprite is marked loaded so draw will execute
232
+ if (!avatar.loaded) {
233
+ console.warn(`Remote avatar ${clientId} was not loaded; forcing loaded=true so renderer will attempt to draw.`);
234
+ avatar.loaded = true;
235
+ avatar.templateLoaded = true;
236
+ if (!avatar.texture || typeof avatar.texture.attach !== 'function') avatar.texture = { loaded: true, attach: () => { } };
237
+ }
238
+ try { debug('World', `updateRemoteAvatar: client=${clientId} post pos=${avatar.pos?.x},${avatar.pos?.y},${avatar.pos?.z} loaded=${avatar.loaded} id=${avatar.id} zone=${avatar.zone?.id}`); } catch (e) { }
239
+ return avatar;
240
+ }
241
+ return null;
242
+ }
243
+
244
+ applyRemoteAction(clientId, action, params, spriteId) {
245
+ const avatar = this.remoteAvatars.get(clientId);
246
+ if (avatar) {
247
+ avatar.performAction(action, params); // implement this in your avatar class
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Creates an avatar in the world.
253
+ * @param {object} avatarData - The avatar data.
254
+ * @returns {Avatar|null} The created avatar or null.
255
+ */
256
+ createAvatar = (avatarData) => {
257
+ const zone = this.zoneContaining(avatarData.x, avatarData.y);
258
+ if (zone) {
259
+ const avatar = new Avatar(this.engine);
260
+ // leave z undefined so Avatar.onLoad will compute using hotspotOffset
261
+ avatar.onLoad({
262
+ zone: zone,
263
+ id: avatarData.id,
264
+ pos: new Vector(avatarData.x, avatarData.y),
265
+ ...avatarData
266
+ });
267
+ zone.addSprite(avatar);
268
+ return avatar;
269
+ }
270
+ return null;
271
+ };
272
+
273
+ /**
274
+ * Removes an avatar from the world.
275
+ * @param {Avatar} avatar - The avatar to remove.
276
+ */
277
+ removeAvatar = (avatar) => {
278
+ const zone = avatar.zone;
279
+ if (zone) {
280
+ zone.removeSprite(avatar);
281
+ }
282
+ };
283
+
284
+ /**
285
+ * Gets the avatar sprite.
286
+ * @returns {object|null} The avatar sprite.
287
+ */
288
+ getAvatar = () => {
289
+ return this.spriteDict['avatar'];
290
+ };
291
+
292
+ /**
293
+ * Pushes an action to run after the current tick.
294
+ * @param {function(): void} action - The action to run.
295
+ */
296
+ runAfterTick = (action) => {
297
+ this.afterTickActions.add(action);
298
+ };
299
+
300
+ /**
301
+ * Sorts zones for correct render order.
302
+ */
303
+ sortZones = () => {
304
+ this.zoneList.sort((a, b) => a.bounds[1] - b.bounds[1]);
305
+ };
306
+
307
+
308
+
309
+ /**
310
+ * Loads a zone from a zip archive.
311
+ * @param {string} zoneId - The zone ID.
312
+ * @param {object} zip - The zip archive.
313
+ * @param {boolean} [skipCache=false] - Whether to skip cache.
314
+ * @param {object} [transitionParams={ effect: 'cross', duration: 500 }] - Transition parameters.
315
+ * @returns {Promise<Zone>} The loaded zone.
316
+ */
317
+ loadZoneFromZip = async (zoneId, zip, skipCache = false, transitionParams = { effect: 'cross', duration: 500 }) => {
318
+ // check cache ?
319
+ if (!skipCache && this.zoneDict[zoneId]) return this.zoneDict[zoneId];
320
+ const engine = this.engine;
321
+
322
+ // transition effects
323
+ let useTransition = false;
324
+ if (transitionParams && engine?.renderManager) {
325
+ const rm = engine.renderManager;
326
+ const now = (typeof performance !== 'undefined' ? performance.now() : Date.now());
327
+ // Compute time since the last transition started. We allow a small
328
+ // grace period after a transition completes before a new one is
329
+ // permitted. If a transition is still running (isTransitioning),
330
+ // startTransition() will queue the next transition automatically.
331
+ const timeSinceLast = now - (rm.transitionStartTime + rm.transitionDuration);
332
+ if (!rm.isTransitioning && timeSinceLast > 100) {
333
+ useTransition = true;
334
+ }
335
+ }
336
+ if (useTransition) {
337
+ const { effect = 'cross', duration = 500 } = transitionParams;
338
+ await engine.renderManager.startTransition({ effect: effect, direction: 'out', duration: duration });
339
+ }
340
+
341
+ debug('World', 'Loading Zone from Zip:', zoneId);
342
+
343
+ let zoneJson = JSON.parse(await zip.file('maps/' + zoneId + '/map.json').async('string')); // main map file (/zip/maps/{zoneId}/map.json)
344
+ let cellJson = JSON.parse(await zip.file('maps/' + zoneId + '/cells.json').async('string')); // cells (/zip/maps/{zoneId}/cells.json)
345
+
346
+ // Fetch Zone Remotely (allows for custom maps - with approved sprites / actions)
347
+ let z = new Zone(zoneId, this);
348
+ await z.loadZoneFromZip(zoneJson, cellJson, zip);
349
+
350
+ // audio
351
+ this.zoneList.map((x) => {
352
+ if (x.audio) {
353
+ x.audio.pauseAudio();
354
+ }
355
+ });
356
+ if (z.audio) {
357
+ z.audio.playAudio();
358
+ }
359
+
360
+ // add zone
361
+ this.zoneDict[zoneId] = z;
362
+ this.zoneList.push(z);
363
+
364
+ // Sort for correct render order
365
+ z.runWhenLoaded(this.sortZones);
366
+
367
+ // fade back in once the new zone has finished loading
368
+ if (useTransition) {
369
+ const { effect = 'cross', duration = 500 } = transitionParams;
370
+ await engine.renderManager.startTransition({ effect: effect, direction: 'in', duration: duration });
371
+ }
372
+
373
+ return z;
374
+ };
375
+
376
+ /**
377
+ * Loads a zone.
378
+ * @param {string} zoneId - The zone ID.
379
+ * @param {boolean} [remotely=false] - Whether to load remotely.
380
+ * @param {boolean} [skipCache=false] - Whether to skip cache.
381
+ * @param {object} [transitionParams={ effect: 'cross', duration: 500 }] - Transition parameters.
382
+ * @returns {Promise<Zone>} The loaded zone.
383
+ */
384
+ loadZone = async (zoneId, remotely = false, skipCache = false, transitionParams = { effect: 'cross', duration: 500 }) => {
385
+ if (!skipCache && this.zoneDict[zoneId]) return this.zoneDict[zoneId];
386
+ const engine = this.engine;
387
+
388
+ // transition effects
389
+ let useTransition = false;
390
+ if (transitionParams && engine?.renderManager) {
391
+ const rm = engine.renderManager;
392
+ const now = (typeof performance !== 'undefined' ? performance.now() : Date.now());
393
+ const timeSinceLast = now - (rm.transitionStartTime + rm.transitionDuration);
394
+ if (!rm.isTransitioning && timeSinceLast > 100) {
395
+ useTransition = true;
396
+ }
397
+ }
398
+ if (useTransition) {
399
+ const { effect = 'cross', duration = 500 } = transitionParams;
400
+ await engine.renderManager.startTransition({ effect: effect, direction: 'out', duration: duration });
401
+ }
402
+
403
+ // Fetch Zone Remotely (allows for custom maps - with approved sprites / actions)
404
+ let z = new Zone(zoneId, this);
405
+ if (remotely) await z.loadRemote();
406
+ else await z.load();
407
+
408
+ // audio
409
+ this.zoneList.map((x) => {
410
+ if (x.audio) {
411
+ x.audio.pauseAudio();
412
+ }
413
+ });
414
+ if (z.audio) {
415
+ console.log(z.audio);
416
+ z.audio.playAudio();
417
+ }
418
+
419
+ // add zone
420
+ this.zoneDict[zoneId] = z;
421
+ this.zoneList.push(z);
422
+
423
+ // Sort for correct render order
424
+ z.runWhenLoaded(this.sortZones);
425
+
426
+ // fade back in once the new zone has finished loading
427
+ if (useTransition) {
428
+ const { effect = 'cross', duration = 500 } = transitionParams;
429
+ await engine.renderManager.startTransition({ effect: effect, direction: 'in', duration: duration });
430
+ }
431
+ return z;
432
+ };
433
+
434
+ /**
435
+ * Removes a zone.
436
+ * @param {string} zoneId - The zone ID to remove.
437
+ */
438
+ removeZone = (zoneId) => {
439
+ this.zoneList = this.zoneList.filter((zone) => {
440
+ if (zone.id !== zoneId) {
441
+ return true;
442
+ } else {
443
+ if (zone.audio) {
444
+ zone.audio.pauseAudio();
445
+ }
446
+ zone.removeAllSprites();
447
+ zone.runWhenDeleted();
448
+ }
449
+ });
450
+ delete this.zoneDict[zoneId];
451
+ };
452
+
453
+ /**
454
+ * Removes all zones.
455
+ */
456
+ removeAllZones = () => {
457
+ this.zoneList.map((z) => {
458
+ if (z.audio) {
459
+ z.audio.pauseAudio();
460
+ }
461
+ z.removeAllSprites();
462
+ z.runWhenDeleted();
463
+ });
464
+ this.zoneList = [];
465
+ this.zoneDict = {};
466
+ };
467
+
468
+ /**
469
+ * Updates the world.
470
+ * @param {number} time - The current time.
471
+ */
472
+ tick = (time) => {
473
+ for (let z in this.zoneDict) this.zoneDict[z]?.tick(time, this.isPaused);
474
+ this.afterTickActions.run(time);
475
+ };
476
+
477
+ /**
478
+ * Checks input at the world level.
479
+ * @param {number} time - The current time.
480
+ */
481
+ checkInput = (time) => {
482
+ if (time > this.lastKey + 200) {
483
+ this.lastKey = time;
484
+
485
+ if (this.modeManager && this.modeManager.handleInput) {
486
+ try {
487
+ if (this.modeManager.handleInput(time)) return;
488
+ } catch (e) { console.warn('mode input handler error', e); }
489
+ }
490
+ let touchmap = this.engine.gamepad.checkInput();
491
+ if (this.engine.gamepad.keyPressed('start')) {
492
+ touchmap['start'] = 0;
493
+ }
494
+ if (this.engine.gamepad.keyPressed('select')) {
495
+ touchmap['select'] = 0;
496
+ this.engine.toggleFullscreen();
497
+ }
498
+ }
499
+ };
500
+
501
+ /**
502
+ * Opens the start menu.
503
+ * @param {object} menuConfig - The menu configuration.
504
+ * @param {string[]} [defaultMenus=['start']] - Default menus.
505
+ */
506
+ startMenu = (menuConfig, defaultMenus = ['start']) => {
507
+ this.addEvent(
508
+ new EventLoader(this.engine, 'menu', [menuConfig ?? this.menuConfig, defaultMenus, false, { autoclose: false, closeOnEnter: true }], this)
509
+ );
510
+ };
511
+
512
+ /**
513
+ * Adds an event to the queue.
514
+ * @param {object} event - The event to add.
515
+ */
516
+ addEvent = (event) => {
517
+ if (this.eventDict[event.id]) this.removeAction(event.id);
518
+ this.eventDict[event.id] = event;
519
+ this.eventList.push(event);
520
+ };
521
+
522
+ /**
523
+ * Removes an action.
524
+ * @param {string} id - The action ID.
525
+ */
526
+ removeAction = (id) => {
527
+ this.eventList = this.eventList.filter((event) => event.id !== id);
528
+ delete this.eventDict[id];
529
+ };
530
+
531
+ /**
532
+ * Removes all actions.
533
+ */
534
+ removeAllActions = () => {
535
+ this.eventList = [];
536
+ this.eventDict = {};
537
+ };
538
+
539
+ /**
540
+ * Handles outer tick logic for events and zones.
541
+ * @param {number} time - The current time.
542
+ */
543
+ tickOuter = (time) => {
544
+ this.checkInput(time);
545
+ this.eventList.sort((a, b) => {
546
+ let dt = a.startTime - b.startTime;
547
+ if (!dt) return dt;
548
+ return a.id > b.id ? 1 : -1;
549
+ });
550
+ let toRemove = [];
551
+ this.eventList.forEach((event) => {
552
+ if (!event.loaded || event.startTime > time || (event.pausable && this.isPaused)) return;
553
+ if (event.tick(time)) {
554
+ toRemove.push(event);
555
+ event.onComplete();
556
+ }
557
+ });
558
+ toRemove.forEach((event) => this.removeAction(event.id));
559
+ if (this.tick && !this.isPaused) this.tick(time);
560
+ if (!this.isPaused && this.modeManager && this.modeManager.update) {
561
+ try {
562
+ this.modeManager.update(time);
563
+ } catch (e) {
564
+ console.warn('mode update error', e);
565
+ }
566
+ }
567
+ };
568
+
569
+ /**
570
+ * Draws each zone.
571
+ */
572
+ draw = () => {
573
+ for (let z in this.zoneDict) this.zoneDict[z].draw(this.engine);
574
+ };
575
+
576
+ /**
577
+ * Finds the zone containing the given coordinates.
578
+ * @param {number} x - The x coordinate.
579
+ * @param {number} y - The y coordinate.
580
+ * @returns {Zone|null} The zone containing the point.
581
+ */
582
+ zoneContaining = (x, y) => {
583
+ for (let z in this.zoneDict) {
584
+ let zone = this.zoneDict[z];
585
+ if (zone.loaded && zone.isInZone(x, y)) return zone;
586
+ }
587
+ return null;
588
+ };
589
+
590
+ /**
591
+ * Finds a path between two points.
592
+ * @param {Array<number>} from - The starting point.
593
+ * @param {Array<number>} to - The ending point.
594
+ * @returns {Array} The path.
595
+ */
596
+ pathFind = (from, to) => {
597
+ // memory
598
+ let steps = [],
599
+ visited = [],
600
+ found = false,
601
+ world = this,
602
+ x = from[0],
603
+ y = from[1];
604
+ // loop through tiles
605
+ function buildPath(neighbour, path) {
606
+ let jsonNeighbour = JSON.stringify([neighbour[0], neighbour[1]]);
607
+ if (found) return false; // ignore anything further
608
+ if (neighbour[0] == to[0] && neighbour[1] == to[1]) {
609
+ // found it
610
+ found = true;
611
+ // if final location is blocked, stop in front
612
+ if (!world.canWalk(neighbour, jsonNeighbour, visited)) {
613
+ return [found, [...path]];
614
+ }
615
+ // otherwise return whole path
616
+ return [found, [...path, to]];
617
+ }
618
+ // Check walkability
619
+ if (!world.canWalk(neighbour, jsonNeighbour, visited)) return false;
620
+ // Visit Node & continue Search
621
+ visited.push(jsonNeighbour);
622
+ return world
623
+ .getNeighbours(...neighbour)
624
+ .sort((a, b) => Math.min(Math.abs(to[0] - a[0]) - Math.abs(to[0] - b[0]), Math.abs(to[1] - a[1]) - Math.abs(to[1] - b[1])))
625
+ .map((neigh) => buildPath(neigh, [...path, [neighbour[0], neighbour[1], 600]]))
626
+ .filter((x) => x)
627
+ .flat();
628
+ }
629
+ // Fetch Steps
630
+ steps = world
631
+ .getNeighbours(x, y)
632
+ .sort((a, b) => Math.min(Math.abs(to[0] - a[0]) - Math.abs(to[0] - b[0]), Math.abs(to[1] - a[1]) - Math.abs(to[1] - b[1])))
633
+ .map((neighbour) => buildPath(neighbour, [[from[0], from[1], 600]]))
634
+ .filter((x) => x[0]);
635
+ // Flatten Path from Segments
636
+ return steps.flat();
637
+ };
638
+
639
+ /**
640
+ * Gets a zone by ID.
641
+ * @param {string} id - The zone ID.
642
+ * @returns {Zone|null} The zone.
643
+ */
644
+ getZoneById = (id) => {
645
+ return this.zoneDict[id];
646
+ };
647
+
648
+ /**
649
+ * Gets adjacent cells.
650
+ * @param {number} x - The x coordinate.
651
+ * @param {number} y - The y coordinate.
652
+ * @returns {Array<Array<number>>} The neighbors.
653
+ */
654
+ getNeighbours = (x, y) => {
655
+ let top = [x, y + 1, Direction.Up],
656
+ bottom = [x, y - 1, Direction.Down],
657
+ left = [x - 1, y, Direction.Left],
658
+ right = [x + 1, y, Direction.Right];
659
+ return [top, left, right, bottom];
660
+ };
661
+
662
+ /**
663
+ * Checks if a cell can be walked on.
664
+ * @param {Array<number>} neighbour - The neighbor cell.
665
+ * @param {string} jsonNeighbour - The JSON string of the neighbor.
666
+ * @param {Array<string>} visited - Visited cells.
667
+ * @returns {boolean} Whether it can be walked.
668
+ */
669
+ canWalk = (neighbour, jsonNeighbour, visited) => {
670
+ let zone = this.zoneContaining(...neighbour);
671
+ if (
672
+ !zone ||
673
+ visited.indexOf(jsonNeighbour) >= 0 ||
674
+ !zone.isWalkable(...neighbour) ||
675
+ !zone.isWalkable(neighbour[0], neighbour[1], Direction.reverse(neighbour[2]))
676
+ ) {
677
+ return false;
678
+ }
679
+ return true;
680
+ };
681
+ }