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,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
|
+
}
|