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,704 @@
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
+ import { EventLoader } from '@Engine/utils/loaders/index.js';
15
+ import PxcPlayer from '@Engine/core/cutscene/PxcPlayer.js';
16
+ import { debug } from '@Engine/utils/debug-logger.js';
17
+
18
+ export default class PixoScriptLibrary {
19
+ /**
20
+ * Constructor
21
+ * @param {pixoscript} pixoscript - PixoScript Library
22
+ * @constructor
23
+ */
24
+ constructor(pixoscript) {
25
+ this.pixoscript = pixoscript;
26
+ }
27
+
28
+ /**
29
+ * Create Script Environment
30
+ */
31
+ getLibrary = (engine, envScope) => {
32
+ debug('PixoScriptLibrary', 'creating library', { envScope });
33
+
34
+ return new this.pixoscript.Table({
35
+ // passed in scope
36
+ ...envScope,
37
+ // core functions
38
+ get_caller: () => {
39
+ return envScope._this;
40
+ },
41
+ get_subject: () => {
42
+ return envScope.subject;
43
+ },
44
+ get_map: () => {
45
+ return envScope.map || envScope.zone;
46
+ },
47
+ get_zone: () => {
48
+ return envScope.map || envScope.zone;
49
+ },
50
+ get_world: () => {
51
+ return engine.spritz.world;
52
+ },
53
+ // network functions
54
+ send_action: (action = false) => {
55
+ const { networkManager } = engine;
56
+ if (networkManager && networkManager.ws) {
57
+ const sent = networkManager.sendAction(action.toObject());
58
+ if (action)
59
+ return () => Promise.resolve(sent);
60
+ return sent;
61
+ }
62
+ if (action)
63
+ return () => Promise.resolve(false);
64
+ return false;
65
+ },
66
+ // flag functions
67
+ all_flags: (action = false) => {
68
+ const flags = engine.store.all();
69
+ if (action)
70
+ return () => Promise.resolve(flags);
71
+ return flags;
72
+ },
73
+ has_flag: (key, action = false) => {
74
+ debug('PixoScript', 'checking flag via lua', key, action);
75
+ const hasFlag = engine.store.keys().includes(key);
76
+ if (action)
77
+ return () => Promise.resolve(hasFlag);
78
+ return hasFlag;
79
+ },
80
+ set_flag: (key, value, action = false) => {
81
+ debug('PixoScript', 'setting flag via lua', key, action);
82
+ const flag = engine.store.set(key, value.toObject());
83
+ if (action)
84
+ return () => Promise.resolve(flag);
85
+ return flag;
86
+ },
87
+ add_flag: (key, value, action = false) => {
88
+ debug('PixoScript', 'adding flag via lua', key, action);
89
+ engine.store.add(key, value.toObject());
90
+ if (action)
91
+ return () => Promise.resolve(true);
92
+ return true;
93
+ },
94
+ get_flag: (key, action = false) => {
95
+ debug('PixoScript', 'getting flag via lua', key, action);
96
+ const flag = engine.store.get(key);
97
+ if (action)
98
+ return () => Promise.resolve(flag);
99
+ return flag;
100
+ },
101
+ // world functions
102
+ remove_all_zones: () => {
103
+ debug('PixoScript', 'removing all zones via lua');
104
+ return engine.spritz.world.removeAllZones();
105
+ },
106
+ load_zone_from_zip: (z, zip) => {
107
+ debug('PixoScript', 'loading zone from zip via lua', { world: engine.spritz.world, z, zip });
108
+ // When loading zones via Lua we allow the world to manage screen
109
+ // transitions. Passing `undefined` (or omitting the parameter) causes
110
+ // World.loadZoneFromZip() to use its default transition settings
111
+ // (typically a fade in/out). This keeps portal transitions consistent
112
+ // with other zone loads while still avoiding duplicate fades when the
113
+ // engine is already transitioning. The world implementation guards
114
+ // against overlapping transitions via the renderManager.isTransitioning
115
+ // flag and skips transitions if the zone is already cached.
116
+ return engine.spritz.world.loadZoneFromZip(z, zip, false);
117
+ },
118
+
119
+ /**
120
+ * Register a cutscene definition from Lua. The `steps` parameter
121
+ * should be a Lua table (array-like) whose entries are tables with
122
+ * step definitions (type, effect, direction, duration, zone, etc.).
123
+ * These tables are converted to plain JS objects and stored under
124
+ * the provided name. Once registered, call pixos.start_cutscene(name)
125
+ * to play it. If a cutscene with the same name already exists it
126
+ * will be replaced.
127
+ *
128
+ * Example Lua:
129
+ * pixos.register_cutscene("intro", {
130
+ * { type = 'transition', effect = 'fade', direction = 'out', duration = 500 },
131
+ * { type = 'load_zone', zone = 'village', effect = 'fade', duration = 500 },
132
+ * { type = 'transition', effect = 'fade', direction = 'in', duration = 500 },
133
+ * })
134
+ * @param {string} name
135
+ * @param {table} steps
136
+ */
137
+ register_cutscene: (name, steps) => {
138
+ try {
139
+ // Convert Lua table to JS array of step objects
140
+ const arr = this.pixoscript.utils.ensureArray(steps.toObject());
141
+ const jsSteps = arr.map((item) => {
142
+ // `item` may be a Lua Table; convert to JS object
143
+ return item && typeof item.toObject === 'function' ? item.toObject() : item;
144
+ });
145
+ engine.cutsceneManager.register(name, jsSteps);
146
+ } catch (e) {
147
+ console.warn('Failed to register cutscene from Lua', e);
148
+ }
149
+ },
150
+
151
+ /**
152
+ * Start a pre-registered cutscene by name. Returns immediately. The
153
+ * cutscene will run asynchronously. Use pixos.skip_cutscene() to
154
+ * cancel the currently playing cutscene.
155
+ * @param {string} name
156
+ */
157
+ start_cutscene: (name) => {
158
+ try {
159
+ engine.cutsceneManager.start(name);
160
+ } catch (e) {
161
+ console.warn('Failed to start cutscene', name, e);
162
+ }
163
+ },
164
+
165
+ /**
166
+ * Skip the currently active cutscene, if any. Clears the cutscene
167
+ * queue immediately. Safe to call even if no cutscene is running.
168
+ */
169
+ skip_cutscene: () => {
170
+ try {
171
+ engine.cutsceneManager.skip();
172
+ } catch (e) {
173
+ console.warn('Failed to skip cutscene', e);
174
+ }
175
+ },
176
+
177
+ /**
178
+ * Set the backdrop for the current cutscene.
179
+ * @param {string} backdrop - The backdrop label to set.
180
+ */
181
+ set_backdrop: (backdrop) => {
182
+ try {
183
+ engine.cutsceneManager.setBackdrop({ backdrop });
184
+ } catch (e) {
185
+ console.warn('Failed to set backdrop', e);
186
+ }
187
+ },
188
+
189
+ /**
190
+ * Show a cutout in the current cutscene.
191
+ * @param {string} sprite - The sprite ID.
192
+ * @param {string} cutout - The cutout label.
193
+ * @param {string} [position='left'] - The position ('left' or 'right').
194
+ */
195
+ show_cutout: (sprite, cutout, position = 'left') => {
196
+ try {
197
+ engine.cutsceneManager.showCutout({ sprite, cutout, position });
198
+ } catch (e) {
199
+ console.warn('Failed to show cutout', e);
200
+ }
201
+ },
202
+
203
+ /**
204
+ * Play a cutscene by name. Supports both:
205
+ * 1. Pre-registered cutscene names (registered via register_cutscene)
206
+ * 2. File paths to .pxc cutscene files
207
+ *
208
+ * Returns a function that can be yielded in a Lua script.
209
+ *
210
+ * Example Lua:
211
+ * pixos.sync({ pixos.play_cutscene('intro') })
212
+ * pixos.sync({ pixos.play_cutscene('cutscenes/opening.pxc') })
213
+ *
214
+ * @param {string} cutscene - Cutscene name or file path
215
+ * @returns {function} Async function that resolves when cutscene completes
216
+ */
217
+ play_cutscene: (cutscene) => {
218
+ return () =>
219
+ new Promise(async (resolve) => {
220
+ try {
221
+ // Check if this is a file path (ends with .pxc) or a registered cutscene name
222
+ if (typeof cutscene === 'string' && cutscene.endsWith('.pxc')) {
223
+ // Load and play .pxc file
224
+ const scriptText = await engine.assetLoader.load(cutscene);
225
+ if (scriptText) {
226
+ const player = new PxcPlayer(engine, {
227
+ onEnd: () => resolve()
228
+ });
229
+ await player.playCutscene(scriptText);
230
+ } else {
231
+ console.warn('[play_cutscene] Failed to load cutscene file:', cutscene);
232
+ resolve();
233
+ }
234
+ } else {
235
+ // Try registered cutscene first
236
+ if (engine.cutsceneManager && engine.cutsceneManager.isRegistered?.(cutscene)) {
237
+ engine.cutsceneManager.start(cutscene);
238
+ // Poll until cutscene finishes
239
+ const poll = () => {
240
+ if (!engine.cutsceneManager.isRunning()) {
241
+ resolve();
242
+ } else {
243
+ setTimeout(poll, 30);
244
+ }
245
+ };
246
+ poll();
247
+ } else if (envScope.zone?.playCutscene) {
248
+ // Fallback to zone's playCutscene method
249
+ await envScope.zone.playCutscene(cutscene);
250
+ resolve();
251
+ } else {
252
+ console.warn('[play_cutscene] Cutscene not found:', cutscene);
253
+ resolve();
254
+ }
255
+ }
256
+ } catch (e) {
257
+ console.warn('[play_cutscene] Error playing cutscene:', e);
258
+ resolve();
259
+ }
260
+ });
261
+ },
262
+
263
+ /**
264
+ * TODO - This is working well - but I need to fix up older cutscene methods,
265
+ * and consolidate everything together. Zones, etc, should all work using the
266
+ * same system.
267
+ *
268
+ * Run an ad-hoc cutscene defined by a Lua table of steps. Returns a
269
+ * function that can be yielded in a Lua script and executed via
270
+ * pixos.sync. The returned function resolves when the cutscene
271
+ * completes. Steps are converted to plain JS objects. A unique
272
+ * temporary name is generated for each call to avoid collisions.
273
+ *
274
+ * Example Lua:
275
+ * local steps = {
276
+ * { type = 'transition', effect = 'cross', direction = 'out', duration = 500 },
277
+ * { type = 'load_zone', zone = 'cave', effect = 'cross', duration = 500 },
278
+ * { type = 'transition', effect = 'cross', direction = 'in', duration = 500 },
279
+ * }
280
+ * pixos.sync({ pixos.run_cutscene(steps) })
281
+ */
282
+ run_cutscene: (steps) => {
283
+ // Return an async function that the Lua runtime will call
284
+ return () =>
285
+ new Promise((resolve) => {
286
+ try {
287
+ const arr = this.pixoscript.utils.ensureArray(steps.toObject());
288
+ const jsSteps = arr.map((item) => {
289
+ return item && typeof item.toObject === 'function' ? item.toObject() : item;
290
+ });
291
+ // Generate a unique name for this temporary cutscene
292
+ const name = '__lua_cutscene_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
293
+ engine.cutsceneManager.register(name, jsSteps);
294
+ engine.cutsceneManager.start(name);
295
+ // Poll until cutscene finishes
296
+ const poll = () => {
297
+ if (!engine.cutsceneManager.isRunning()) {
298
+ resolve();
299
+ } else {
300
+ setTimeout(poll, 30);
301
+ }
302
+ };
303
+ poll();
304
+ } catch (e) {
305
+ console.warn('Failed to run cutscene', e);
306
+ resolve();
307
+ }
308
+ });
309
+ },
310
+ run_transition: (effect = 'fade', direction = 'out', duration = 500) => {
311
+ return () => {
312
+ new Promise((resolve) => {
313
+ const rm = engine.renderManager;
314
+ if (!rm) resolve();
315
+ return rm.startTransition({ effect, direction, duration }).then(() => resolve());
316
+ })
317
+ }
318
+ },
319
+ sprite_dialogue: (spriteId, dialogue, options = {}) => {
320
+ return () =>
321
+ new Promise((resolve) => {
322
+ debug('PixoScript', 'playing dialogue via lua', { zone: envScope.zone, spriteId, dialogue });
323
+ options.onClose = () => resolve();
324
+ return envScope.zone.spriteDialogue(spriteId, dialogue, options).then(() => {
325
+ debug('PixoScript', 'played dialogue via lua', { zone: envScope.zone, spriteId, dialogue });
326
+ });
327
+ });
328
+ },
329
+ move_sprite: (spriteId, location, running) => {
330
+ return () =>
331
+ new Promise((resolve) => {
332
+ debug('PixoScript', 'moving sprite via lua', { zone: envScope.zone, spriteId, location, running });
333
+ return envScope.zone.moveSprite(spriteId, this.pixoscript.utils.ensureArray(location.toObject()), running).then(() => {
334
+ debug('PixoScript', 'moved sprite via lua', { zone: envScope.zone, spriteId, location, running });
335
+ resolve();
336
+ });
337
+ });
338
+ },
339
+ load_scripts: (scripts) => {
340
+ debug('PixoScript', 'loading scripts via lua', { scripts, envScope });
341
+ return envScope.zone.loadScripts(scripts);
342
+ },
343
+
344
+ /**
345
+ * Play a .pxc cutscene file
346
+ * Returns a function that resolves when cutscene completes
347
+ *
348
+ * Example:
349
+ * pixos.sync({ pixos.play_pxc_cutscene('cutscenes/intro.pxc') })
350
+ */
351
+ play_pxc_cutscene: (filePath, options = {}) => {
352
+ return () =>
353
+ new Promise(async (resolve) => {
354
+ try {
355
+ // Load the .pxc file from asset loader
356
+ const scriptText = await engine.assetLoader.load(filePath);
357
+
358
+ if (!scriptText) {
359
+ console.error('[PixoScript] Failed to load cutscene:', filePath);
360
+ resolve();
361
+ return;
362
+ }
363
+
364
+ // Create PxcPlayer instance with callbacks
365
+ const callbacks = {
366
+ onDialogueShow: options.onDialogueShow || ((data) => {
367
+ debug('PxcPlayer', 'Dialogue:', data.actor, data.text);
368
+ }),
369
+ onBackdropChange: options.onBackdropChange || ((url, opts) => {
370
+ debug('PxcPlayer', 'Backdrop:', url);
371
+ }),
372
+ onEnd: () => {
373
+ debug('PxcPlayer', 'Cutscene ended');
374
+ if (options.onEnd) options.onEnd();
375
+ resolve();
376
+ }
377
+ };
378
+
379
+ const player = new PxcPlayer(engine, callbacks);
380
+
381
+ // Play the cutscene
382
+ await player.playCutscene(scriptText);
383
+
384
+ } catch (e) {
385
+ console.error('[PixoScript] Error playing .pxc cutscene:', e);
386
+ resolve();
387
+ }
388
+ });
389
+ },
390
+
391
+ /**
392
+ * Play inline .pxc cutscene script
393
+ * Returns a function that resolves when cutscene completes
394
+ *
395
+ * Example:
396
+ * local script = [[
397
+ * @backdrop textures/room.gif
398
+ * HERO: [expression=smile] Hello!
399
+ * waitInput
400
+ * @end
401
+ * ]]
402
+ * pixos.sync({ pixos.play_pxc_script(script) })
403
+ */
404
+ play_pxc_script: (scriptText, options = {}) => {
405
+ return () =>
406
+ new Promise(async (resolve) => {
407
+ try {
408
+ debug('PixoScript', 'Playing inline .pxc script');
409
+
410
+ // Create PxcPlayer instance with callbacks
411
+ const callbacks = {
412
+ onDialogueShow: options.onDialogueShow || ((data) => {
413
+ debug('PxcPlayer', 'Dialogue:', data.actor, data.text);
414
+ }),
415
+ onBackdropChange: options.onBackdropChange || ((url, opts) => {
416
+ debug('PxcPlayer', 'Backdrop:', url);
417
+ }),
418
+ onEnd: () => {
419
+ debug('PxcPlayer', 'Cutscene ended');
420
+ if (options.onEnd) options.onEnd();
421
+ resolve();
422
+ }
423
+ };
424
+
425
+ const player = new PxcPlayer(engine, callbacks);
426
+
427
+ // Play the cutscene
428
+ await player.playCutscene(scriptText);
429
+
430
+ } catch (e) {
431
+ console.error('[PixoScript] Error playing inline .pxc script:', e);
432
+ resolve();
433
+ }
434
+ });
435
+ },
436
+
437
+ // camera functions
438
+ // ...
439
+ set_camera: () => {
440
+ engine.renderManager.camera.setCamera();
441
+ },
442
+ get_camera_vector: () => {
443
+ return engine.renderManager.camera.cameraTarget;
444
+ },
445
+ look_at: (pos, trgt, up) => {
446
+ let position = this.pixoscript.utils.ensureArray(pos.toObject());
447
+ let target = this.pixoscript.utils.ensureArray(trgt.toObject());
448
+ let upDir = this.pixoscript.utils.ensureArray(up.toObject());
449
+ engine.renderManager.camera.lookAt(position, target, upDir);
450
+ },
451
+ pan_camera: (from, to, duration) => {
452
+ debug('PixoScript', 'panning camera via lua', { from, to, duration });
453
+ return () =>
454
+ new Promise((resolve) => {
455
+ engine.spritz.world.addEvent(
456
+ new EventLoader(
457
+ engine,
458
+ 'camera',
459
+ [
460
+ 'pan',
461
+ {
462
+ from: from,
463
+ to: to,
464
+ duration: duration,
465
+ },
466
+ ],
467
+ engine.spritz.world,
468
+ async () => {
469
+ resolve();
470
+ }
471
+ )
472
+ );
473
+ });
474
+ },
475
+
476
+ _pan: (direction, radians = Math.PI / 4) => {
477
+ if (direction === 'CCW') {
478
+ engine.renderManager.camera.panCCW(this.pixoscript.utils.coerceToNumber(radians));
479
+ } else {
480
+ engine.renderManager.camera.panCW(this.pixoscript.utils.coerceToNumber(radians));
481
+ }
482
+ },
483
+ pitch: (direction, radians = Math.PI / 4) => {
484
+ if (direction === 'CCW') {
485
+ engine.renderManager.camera.pitchCCW(this.pixoscript.utils.coerceToNumber(radians));
486
+ } else {
487
+ engine.renderManager.camera.pitchCW(this.pixoscript.utils.coerceToNumber(radians));
488
+ }
489
+ },
490
+ tilt: (direction, radians = Math.PI / 4) => {
491
+ if (direction === 'CCW') {
492
+ engine.renderManager.camera.tiltCCW(this.pixoscript.utils.coerceToNumber(radians));
493
+ } else {
494
+ engine.renderManager.camera.tiltCW(this.pixoscript.utils.coerceToNumber(radians));
495
+ }
496
+ },
497
+
498
+ // input functions
499
+ bind_action: (action, inputType, inputValue) => {
500
+ try {
501
+ if (engine.inputManager) {
502
+ engine.inputManager.bindAction(action, inputType, inputValue);
503
+ }
504
+ } catch (e) {
505
+ console.warn('bind_action failed', e);
506
+ }
507
+ },
508
+ unbind_action: (action, inputType) => {
509
+ try {
510
+ if (engine.inputManager) {
511
+ engine.inputManager.unbindAction(action, inputType);
512
+ }
513
+ } catch (e) {
514
+ console.warn('unbind_action failed', e);
515
+ }
516
+ },
517
+ register_action_hook: (action, hook) => {
518
+ try {
519
+ if (engine.inputManager) {
520
+ engine.inputManager.registerActionHook(action, hook);
521
+ }
522
+ } catch (e) {
523
+ console.warn('register_action_hook failed', e);
524
+ }
525
+ },
526
+ is_action_active: (action) => {
527
+ try {
528
+ return engine.inputManager ? engine.inputManager.isActionActive(action) : false;
529
+ } catch (e) {
530
+ console.warn('is_action_active failed', e);
531
+ return false;
532
+ }
533
+ },
534
+ get_action_input: (action) => {
535
+ try {
536
+ return engine.inputManager ? engine.inputManager.getActionInput(action) : null;
537
+ } catch (e) {
538
+ console.warn('get_action_input failed', e);
539
+ return null;
540
+ }
541
+ },
542
+
543
+ // audio functions
544
+ // ...
545
+
546
+ // sprite functions
547
+ // ...
548
+
549
+ // math functions
550
+ vector: (tbl) => {
551
+ let [x, y, z] = this.pixoscript.utils.ensureArray(tbl.toObject());
552
+ return new engine.utils.Vector(x, y, z);
553
+ },
554
+ vec_sub: (a, b) => {
555
+ return a.sub(b);
556
+ },
557
+
558
+ // misc utils & functions
559
+ sync: async (p) => {
560
+ for (const a of p.toObject()) {
561
+ await a();
562
+ }
563
+ },
564
+ as_obj: (tbl) => {
565
+ return tbl.toObject();
566
+ },
567
+ as_array: (tbl) => {
568
+ return this.pixoscript.utils.ensureArray(tbl.toObject());
569
+ },
570
+ as_table: (obj) => {
571
+ const table = new this.pixoscript.Table();
572
+ for (const [key, value] of Object.entries(obj)) {
573
+ table.set(key, value);
574
+ }
575
+ return table;
576
+ },
577
+ log: (msg) => {
578
+ debug('PixoScript', msg);
579
+ },
580
+ to: (obj, tbl) => {
581
+ for (const [key, value] of Object.entries(tbl.toObject())) {
582
+ obj[key] = value;
583
+ }
584
+ },
585
+ /** Mode API - allow Lua scripts to change or query current mode */
586
+ set_mode: (name, params) => {
587
+ try {
588
+ debug('PixoScript', 'pixos.set_mode called ->', name, params);
589
+ const world = engine.spritz.world;
590
+ if (world && world.modeManager) {
591
+ // params may be a Lua table - convert if necessary
592
+ const p = params && typeof params.toObject === 'function' ? params.toObject() : params;
593
+ world.modeManager.set(name, p);
594
+ }
595
+ } catch (e) {
596
+ console.warn('set_mode failed', e);
597
+ }
598
+ },
599
+ get_mode: () => {
600
+ try { return engine.spritz.world.modeManager.getMode(); } catch (e) { return null; }
601
+ },
602
+ set_mode_mappings: (name, params) => {
603
+ try {
604
+ debug('PixoScript', 'pixos.set_mode_mappings called ->', name, params);
605
+ if (engine && engine.inputManager) {
606
+ // params may be a Lua table - convert if necessary
607
+ const p = params && typeof params.toObject === 'function' ? params.toObject() : params;
608
+ engine.inputManager.setModeMappings(name, p);
609
+ }
610
+ } catch (e) {
611
+ console.warn('set_mode_mappings failed', e);
612
+ }
613
+ },
614
+ register_mode: (name, handlers) => {
615
+ try {
616
+ if (!name) {
617
+ console.warn('pixos.register_mode called with undefined name');
618
+ return;
619
+ }
620
+ debug('PixoScript', 'pixos.register_mode called ->', name);
621
+ const world = engine.spritz.world;
622
+ if (!world || !world.modeManager) return;
623
+ // handlers may be a Lua table; convert to JS object safely
624
+ const h = {};
625
+ const asObj = handlers && typeof handlers.toObject === 'function' ? handlers.toObject() : handlers || {};
626
+ if (asObj.setup) h.setup = asObj.setup;
627
+ if (asObj.update) h.update = asObj.update;
628
+ if (asObj.teardown) h.teardown = asObj.teardown;
629
+ if (asObj.check_input) h.check_input = asObj.check_input;
630
+ if (asObj.on_select) h.on_select = asObj.on_select;
631
+ if (asObj.picker !== undefined) h.picker = asObj.picker;
632
+ world.modeManager.register(name, h);
633
+ } catch (e) { console.warn('register_mode failed', e); }
634
+ },
635
+ from: (obj, key) => {
636
+ return obj[key];
637
+ },
638
+ length: (tbl) => {
639
+ return tbl.length || 0;
640
+ },
641
+ callback_finish: (success) => {
642
+ debug('PixoScript', 'callback finish', { success });
643
+ if (envScope.finish) {
644
+ envScope.finish(success > 0);
645
+ }
646
+ },
647
+ // skybox shader switching
648
+ set_skybox_shader: async (shaderName) => {
649
+ if (engine.renderManager?.skyboxManager?.setSkyboxShader) {
650
+ await engine.renderManager.skyboxManager.setSkyboxShader(shaderName);
651
+ }
652
+ },
653
+ // particle system
654
+ emit_particles: (posTbl, cfgTbl) => {
655
+ try {
656
+ const pos = posTbl && typeof posTbl.toObject === 'function' ? posTbl.toObject() : posTbl || [0, 0, 0];
657
+ const cfg = cfgTbl && typeof cfgTbl.toObject === 'function' ? cfgTbl.toObject() : cfgTbl || {};
658
+ if (engine.renderManager && engine.renderManager.particleManager) {
659
+ // allow shorthand preset names
660
+ if (cfg.preset) {
661
+ const presetCfg = engine.renderManager.particleManager.preset(cfg.preset);
662
+ if (presetCfg) Object.assign(cfg, presetCfg);
663
+ }
664
+ engine.renderManager.particleManager.emit(pos, cfg);
665
+ }
666
+ } catch (e) {
667
+ console.warn('emit_particles failed', e);
668
+ }
669
+ },
670
+ create_particles: (posTbl, presetName) => {
671
+ try {
672
+ const pos = posTbl && typeof posTbl.toObject === 'function' ? posTbl.toObject() : posTbl || [0, 0, 0];
673
+ const preset = presetName || null;
674
+ if (engine.renderManager && engine.renderManager.particleManager) {
675
+ const cfg = preset ? engine.renderManager.particleManager.preset(preset) : {};
676
+ engine.renderManager.particleManager.emit(pos, cfg || {});
677
+ }
678
+ } catch (e) {
679
+ console.warn('create_particles failed', e);
680
+ }
681
+ },
682
+ clear_particles: () => {
683
+ try {
684
+ if (engine.renderManager && engine.renderManager.particleManager) {
685
+ engine.renderManager.particleManager.particles = [];
686
+ }
687
+ } catch (e) {
688
+ console.warn('clear_particles failed', e);
689
+ }
690
+ },
691
+ get_particle_count: () => {
692
+ try {
693
+ if (engine.renderManager && engine.renderManager.particleManager) {
694
+ return engine.renderManager.particleManager.particles.length;
695
+ }
696
+ return 0;
697
+ } catch (e) {
698
+ console.warn('get_particle_count failed', e);
699
+ return 0;
700
+ }
701
+ },
702
+ });
703
+ };
704
+ }