goudengine 0.0.827 → 0.0.830

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 (76) hide show
  1. package/README.md +116 -0
  2. package/dist/generated/diagnostic.g.d.ts +15 -0
  3. package/dist/generated/diagnostic.g.d.ts.map +1 -0
  4. package/dist/generated/diagnostic.g.js +50 -0
  5. package/dist/generated/diagnostic.g.js.map +1 -0
  6. package/dist/generated/errors.g.d.ts +37 -0
  7. package/dist/generated/errors.g.d.ts.map +1 -0
  8. package/dist/generated/errors.g.js +148 -0
  9. package/dist/generated/errors.g.js.map +1 -0
  10. package/dist/generated/index.g.d.ts +5 -2
  11. package/dist/generated/index.g.d.ts.map +1 -1
  12. package/dist/generated/index.g.js +20 -1
  13. package/dist/generated/index.g.js.map +1 -1
  14. package/dist/generated/node/index.g.d.ts +477 -3
  15. package/dist/generated/node/index.g.d.ts.map +1 -1
  16. package/dist/generated/node/index.g.js +1056 -15
  17. package/dist/generated/node/index.g.js.map +1 -1
  18. package/dist/generated/types/engine.g.d.ts +580 -0
  19. package/dist/generated/types/engine.g.d.ts.map +1 -1
  20. package/dist/generated/types/input.g.d.ts +77 -0
  21. package/dist/generated/types/input.g.d.ts.map +1 -1
  22. package/dist/generated/types/input.g.js +91 -1
  23. package/dist/generated/types/input.g.js.map +1 -1
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +28 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/node/index.d.ts +6 -0
  29. package/dist/node/index.d.ts.map +1 -0
  30. package/dist/node/index.js +28 -0
  31. package/dist/node/index.js.map +1 -0
  32. package/dist/shared/debugger.d.ts +9 -0
  33. package/dist/shared/debugger.d.ts.map +1 -0
  34. package/dist/shared/debugger.js +11 -0
  35. package/dist/shared/debugger.js.map +1 -0
  36. package/dist/shared/network.d.ts +39 -0
  37. package/dist/shared/network.d.ts.map +1 -0
  38. package/dist/shared/network.js +66 -0
  39. package/dist/shared/network.js.map +1 -0
  40. package/dist/web/generated/types/engine.g.d.ts +580 -0
  41. package/dist/web/generated/types/engine.g.d.ts.map +1 -1
  42. package/dist/web/generated/types/input.g.d.ts +77 -0
  43. package/dist/web/generated/types/input.g.d.ts.map +1 -1
  44. package/dist/web/generated/types/input.g.js +90 -0
  45. package/dist/web/generated/types/input.g.js.map +1 -1
  46. package/dist/web/generated/web/errors.g.d.ts +37 -0
  47. package/dist/web/generated/web/errors.g.d.ts.map +1 -0
  48. package/dist/web/generated/web/errors.g.js +136 -0
  49. package/dist/web/generated/web/errors.g.js.map +1 -0
  50. package/dist/web/generated/web/index.g.d.ts +198 -3
  51. package/dist/web/generated/web/index.g.d.ts.map +1 -1
  52. package/dist/web/generated/web/index.g.js +533 -5
  53. package/dist/web/generated/web/index.g.js.map +1 -1
  54. package/dist/web/shared/debugger.d.ts +9 -0
  55. package/dist/web/shared/debugger.d.ts.map +1 -0
  56. package/dist/web/shared/debugger.js +7 -0
  57. package/dist/web/shared/debugger.js.map +1 -0
  58. package/dist/web/shared/network.d.ts +39 -0
  59. package/dist/web/shared/network.d.ts.map +1 -0
  60. package/dist/web/shared/network.js +61 -0
  61. package/dist/web/shared/network.js.map +1 -0
  62. package/dist/web/web/index.d.ts +6 -0
  63. package/dist/web/web/index.d.ts.map +1 -0
  64. package/dist/web/web/index.js +6 -0
  65. package/dist/web/web/index.js.map +1 -0
  66. package/goud-engine-node.darwin-arm64.node +0 -0
  67. package/goud-engine-node.darwin-x64.node +0 -0
  68. package/goud-engine-node.linux-x64-gnu.node +0 -0
  69. package/goud-engine-node.win32-x64-msvc.node +0 -0
  70. package/index.d.ts +405 -0
  71. package/index.js +57 -52
  72. package/package.json +25 -19
  73. package/wasm/goud_engine.d.ts +719 -73
  74. package/wasm/goud_engine.js +1401 -20
  75. package/wasm/goud_engine_bg.wasm +0 -0
  76. package/wasm/goud_engine_bg.wasm.d.ts +183 -73
@@ -2,8 +2,28 @@
2
2
  import { Color } from '../types/math.g.js';
3
3
  import { attachInputHandlers } from './input.g.js';
4
4
  export { Color, Vec2, Vec3 } from '../types/math.g.js';
5
- export { Key, MouseButton } from '../types/input.g.js';
5
+ export { Key, MouseButton, PhysicsBackend2D } from '../types/input.g.js';
6
6
  export { Rect } from '../types/math.g.js';
7
+ const PRELOAD_TEXTURE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'tga', 'dds']);
8
+ const PRELOAD_FONT_EXTENSIONS = new Set(['ttf', 'otf', 'woff', 'woff2', 'fnt']);
9
+ function detectPreloadKind(path) {
10
+ const ext = path.split('.').pop()?.toLowerCase() ?? '';
11
+ if (PRELOAD_TEXTURE_EXTENSIONS.has(ext))
12
+ return 'texture';
13
+ if (PRELOAD_FONT_EXTENSIONS.has(ext))
14
+ return 'font';
15
+ throw new Error(`Unsupported preload asset type for path: ${path}`);
16
+ }
17
+ function normalizePreloadAsset(asset) {
18
+ if (typeof asset === 'string') {
19
+ return { path: asset, kind: detectPreloadKind(asset) };
20
+ }
21
+ return { path: asset.path, kind: asset.kind ?? detectPreloadKind(asset.path) };
22
+ }
23
+ const WEB_RENDER_CAPABILITIES = { maxTextureUnits: 0, maxTextureSize: 0, supportsInstancing: false, supportsCompute: false, supportsMsaa: false };
24
+ const WEB_PHYSICS_CAPABILITIES = { supportsContinuousCollision: false, supportsJoints: false, maxBodies: 0 };
25
+ const WEB_AUDIO_CAPABILITIES = { supportsSpatial: true, maxChannels: 32 };
26
+ const WEB_INPUT_CAPABILITIES = { supportsGamepad: false, supportsTouch: true, maxGamepads: 0 };
7
27
  let _wasmModule = null;
8
28
  export async function initWasm(wasmUrl) {
9
29
  if (_wasmModule)
@@ -18,6 +38,44 @@ function getWasm() {
18
38
  throw new Error('Wasm not loaded. Call initWasm() first.');
19
39
  return _wasmModule;
20
40
  }
41
+ function toWasmUiNodeId(nodeId) {
42
+ return typeof nodeId === 'bigint' ? nodeId : BigInt(nodeId);
43
+ }
44
+ export class UiManager {
45
+ constructor(handle) {
46
+ this.handle = handle;
47
+ }
48
+ static async create(wasmUrl) {
49
+ await initWasm(wasmUrl);
50
+ const wasm = getWasm();
51
+ return new UiManager(new wasm.WasmUiManager());
52
+ }
53
+ destroy() { this.handle.free(); }
54
+ update() { this.handle.update(); }
55
+ render() { this.handle.render(); }
56
+ nodeCount() { return this.handle.node_count(); }
57
+ createNode(componentType) { return this.handle.create_node(componentType); }
58
+ removeNode(nodeId) { return this.handle.remove_node(toWasmUiNodeId(nodeId)); }
59
+ setParent(childId, parentId) { return this.handle.set_parent(toWasmUiNodeId(childId), toWasmUiNodeId(parentId)); }
60
+ getParent(nodeId) { return this.handle.get_parent(toWasmUiNodeId(nodeId)); }
61
+ getChildCount(nodeId) { return this.handle.get_child_count(toWasmUiNodeId(nodeId)); }
62
+ getChildAt(nodeId, index) { return this.handle.get_child_at(toWasmUiNodeId(nodeId), index); }
63
+ setWidget(nodeId, widgetKind) { return this.handle.set_widget(toWasmUiNodeId(nodeId), widgetKind); }
64
+ setStyle(nodeId, style) {
65
+ return this.handle.set_style(toWasmUiNodeId(nodeId), style.backgroundColor?.r, style.backgroundColor?.g, style.backgroundColor?.b, style.backgroundColor?.a, style.foregroundColor?.r, style.foregroundColor?.g, style.foregroundColor?.b, style.foregroundColor?.a, style.borderColor?.r, style.borderColor?.g, style.borderColor?.b, style.borderColor?.a, style.borderWidth, style.fontFamily, style.fontSize, style.texturePath, style.widgetSpacing);
66
+ }
67
+ setLabelText(nodeId, text) { return this.handle.set_label_text(toWasmUiNodeId(nodeId), text); }
68
+ setButtonEnabled(nodeId, enabled) { return this.handle.set_button_enabled(toWasmUiNodeId(nodeId), enabled); }
69
+ setImageTexturePath(nodeId, path) { return this.handle.set_image_texture_path(toWasmUiNodeId(nodeId), path); }
70
+ setSlider(nodeId, min, max, value, enabled) { return this.handle.set_slider(toWasmUiNodeId(nodeId), min, max, value, enabled); }
71
+ eventCount() { return this.handle.event_count(); }
72
+ eventRead(index) { return this.handle.event_read(index) ?? null; }
73
+ createPanel() { return this.createNode(0); }
74
+ createLabel(text) { const nodeId = this.createNode(2); this.setLabelText(nodeId, text); return nodeId; }
75
+ createButton(enabled = true) { const nodeId = this.createNode(1); this.setButtonEnabled(nodeId, enabled); return nodeId; }
76
+ createImage(path) { const nodeId = this.createNode(3); this.setImageTexturePath(nodeId, path); return nodeId; }
77
+ createSlider(min, max, value, enabled = true) { const nodeId = this.createNode(4); this.setSlider(nodeId, min, max, value, enabled); return nodeId; }
78
+ }
21
79
  class WebEntity {
22
80
  constructor(_bits) {
23
81
  this._bits = _bits;
@@ -36,6 +94,14 @@ export class GoudGame {
36
94
  this.lastTs = 0;
37
95
  this._shouldClose = false;
38
96
  this._updateFn = null;
97
+ this._audioGlobalVolume = 1;
98
+ this._audioChannelVolumes = new Map();
99
+ this._activeAudioPlayers = new Set();
100
+ this.preloadedTextures = new Map();
101
+ this.preloadedFonts = new Map();
102
+ this.texturePathByHandle = new Map();
103
+ this.fontPathByHandle = new Map();
104
+ this.preloadInFlight = false;
39
105
  this.handle = handle;
40
106
  this.canvas = canvas;
41
107
  }
@@ -93,7 +159,11 @@ export class GoudGame {
93
159
  /** Signals the window to close */
94
160
  close() { this._shouldClose = true; this.stop(); }
95
161
  /** Releases all engine resources */
96
- destroy() { this.stop(); this.handle.free(); }
162
+ destroy() {
163
+ this.stop();
164
+ this._activeAudioPlayers.clear();
165
+ this.handle.free();
166
+ }
97
167
  /** Starts a new render frame with the given clear color */
98
168
  beginFrame(r = 0, g = 0, b = 0, a = 1) {
99
169
  this.handle.set_clear_color(r, g, b, a);
@@ -109,6 +179,9 @@ export class GoudGame {
109
179
  run(update) {
110
180
  if (this.running)
111
181
  return;
182
+ if (this.preloadInFlight) {
183
+ throw new Error('game.preload(...) must finish before game.run() starts.');
184
+ }
112
185
  if (update.constructor.name === 'AsyncFunction') {
113
186
  console.warn('GoudEngine: game.run() callback should be synchronous. Async callbacks may cause borrow conflicts in WASM.');
114
187
  }
@@ -118,7 +191,9 @@ export class GoudGame {
118
191
  this.detachInput = attachInputHandlers(this.canvas, this.handle);
119
192
  this._startLoop(update);
120
193
  }
121
- stop() {
194
+ stop(entity) {
195
+ if (entity !== undefined)
196
+ return 0;
122
197
  this.running = false;
123
198
  if (this.rafId) {
124
199
  cancelAnimationFrame(this.rafId);
@@ -162,14 +237,89 @@ export class GoudGame {
162
237
  }
163
238
  /** Loads a texture from a file path and returns its handle */
164
239
  async loadTexture(path) {
240
+ const cached = this.preloadedTextures.get(path);
241
+ if (cached !== undefined) {
242
+ return cached;
243
+ }
165
244
  const resp = await fetch(path);
166
245
  if (!resp.ok)
167
246
  throw new Error(`Failed to load texture: ${path} (HTTP ${resp.status})`);
168
247
  const bytes = new Uint8Array(await resp.arrayBuffer());
169
- return this.handle.register_texture_from_bytes(bytes);
248
+ const handle = this.handle.register_texture_from_bytes(bytes);
249
+ this.preloadedTextures.set(path, handle);
250
+ this.texturePathByHandle.set(handle, path);
251
+ return handle;
170
252
  }
171
253
  /** Destroys a previously loaded texture */
172
- destroyTexture(handle) { this.handle.destroy_texture(handle); }
254
+ destroyTexture(handle) {
255
+ const path = this.texturePathByHandle.get(handle);
256
+ if (path !== undefined) {
257
+ this.texturePathByHandle.delete(handle);
258
+ this.preloadedTextures.delete(path);
259
+ }
260
+ this.handle.destroy_texture(handle);
261
+ }
262
+ /** Loads a font from a file path and returns its handle */
263
+ async loadFont(path) {
264
+ const cached = this.preloadedFonts.get(path);
265
+ if (cached !== undefined) {
266
+ return cached;
267
+ }
268
+ const resp = await fetch(path);
269
+ if (!resp.ok)
270
+ throw new Error(`Failed to load font: ${path} (HTTP ${resp.status})`);
271
+ const bytes = new Uint8Array(await resp.arrayBuffer());
272
+ const handle = this.handle.register_font_from_bytes(bytes);
273
+ this.preloadedFonts.set(path, handle);
274
+ this.fontPathByHandle.set(handle, path);
275
+ return handle;
276
+ }
277
+ /** Destroys a previously loaded font */
278
+ destroyFont(handle) {
279
+ const path = this.fontPathByHandle.get(handle);
280
+ if (path !== undefined) {
281
+ this.fontPathByHandle.delete(handle);
282
+ this.preloadedFonts.delete(path);
283
+ }
284
+ return this.handle.destroy_font(handle);
285
+ }
286
+ async preload(assets, options = {}) {
287
+ if (this.preloadInFlight) {
288
+ throw new Error('game.preload(...) is already in progress.');
289
+ }
290
+ this.preloadInFlight = true;
291
+ const handles = {};
292
+ try {
293
+ const normalized = assets.map(normalizePreloadAsset);
294
+ const total = normalized.length;
295
+ let loaded = 0;
296
+ for (const asset of normalized) {
297
+ const handle = asset.kind === 'font'
298
+ ? await this.loadFont(asset.path)
299
+ : await this.loadTexture(asset.path);
300
+ handles[asset.path] = handle;
301
+ loaded += 1;
302
+ const update = {
303
+ loaded,
304
+ total,
305
+ progress: total === 0 ? 1 : loaded / total,
306
+ path: asset.path,
307
+ kind: asset.kind,
308
+ handle,
309
+ };
310
+ options.onProgress?.(update);
311
+ }
312
+ return handles;
313
+ }
314
+ finally {
315
+ this.preloadInFlight = false;
316
+ }
317
+ }
318
+ /** Draws text using a loaded font */
319
+ drawText(fontHandle, text, x, y, fontSize = 16, alignment = 0, maxWidth = 0, lineSpacing = 1, direction = 0, color) {
320
+ const c = color ?? Color.white();
321
+ return this.handle.draw_text(fontHandle, text, x, y, fontSize, alignment, maxWidth, lineSpacing, direction, c.r, c.g, c.b, c.a);
322
+ }
173
323
  /** Draws a textured sprite */
174
324
  drawSprite(texture, x, y, width, height, rotation = 0, color) {
175
325
  const c = color ?? Color.white();
@@ -185,6 +335,164 @@ export class GoudGame {
185
335
  const c = color ?? Color.white();
186
336
  this.handle.draw_quad(x, y, width, height, c.r, c.g, c.b, c.a);
187
337
  }
338
+ /** Plays audio from raw bytes on the default channel */
339
+ audioPlay(data) {
340
+ const playerId = this.handle.audio_play(data);
341
+ if (playerId >= 0)
342
+ this._activeAudioPlayers.add(playerId);
343
+ return playerId;
344
+ }
345
+ /** Plays audio from raw bytes on the given channel */
346
+ audioPlayOnChannel(data, channel) {
347
+ const fn = this.handle.audio_play_on_channel;
348
+ const playerId = fn ? fn.call(this.handle, data, channel) : this.handle.audio_play(data);
349
+ if (playerId >= 0)
350
+ this._activeAudioPlayers.add(playerId);
351
+ return playerId;
352
+ }
353
+ /** Plays audio with explicit volume, speed, looping, and channel settings */
354
+ audioPlayWithSettings(data, volume, speed, looping, channel) {
355
+ const fn = this.handle.audio_play_with_settings;
356
+ const playerId = fn ? fn.call(this.handle, data, volume, speed, looping, channel) : this.handle.audio_play(data);
357
+ if (playerId < 0)
358
+ return playerId;
359
+ this._activeAudioPlayers.add(playerId);
360
+ if (!fn) {
361
+ this.handle.audio_set_player_volume(playerId, volume);
362
+ this.handle.audio_set_player_speed(playerId, speed);
363
+ }
364
+ return playerId;
365
+ }
366
+ /** Stops a playing audio player */
367
+ audioStop(playerId) {
368
+ const rc = this.handle.audio_stop(playerId);
369
+ if (rc === 0)
370
+ this._activeAudioPlayers.delete(playerId);
371
+ return rc;
372
+ }
373
+ /** Pauses a playing audio player */
374
+ audioPause(playerId) { return this.handle.audio_pause(playerId); }
375
+ /** Resumes a paused audio player */
376
+ audioResume(playerId) { return this.handle.audio_resume(playerId); }
377
+ /** Stops all active audio players */
378
+ audioStopAll() {
379
+ const rc = this.handle.audio_stop_all();
380
+ if (rc === 0)
381
+ this._activeAudioPlayers.clear();
382
+ return rc;
383
+ }
384
+ /** Sets the global audio volume */
385
+ audioSetGlobalVolume(volume) {
386
+ const fn = this.handle.audio_set_global_volume;
387
+ if (fn) {
388
+ const rc = fn.call(this.handle, volume);
389
+ if (rc === 0)
390
+ this._audioGlobalVolume = volume;
391
+ return rc;
392
+ }
393
+ this._audioGlobalVolume = volume;
394
+ return 0;
395
+ }
396
+ /** Gets the global audio volume */
397
+ audioGetGlobalVolume() {
398
+ const fn = this.handle.audio_get_global_volume;
399
+ return fn ? fn.call(this.handle) : this._audioGlobalVolume;
400
+ }
401
+ /** Sets the volume for a specific channel */
402
+ audioSetChannelVolume(channel, volume) {
403
+ const fn = this.handle.audio_set_channel_volume;
404
+ if (fn)
405
+ return fn.call(this.handle, channel, volume);
406
+ this._audioChannelVolumes.set(channel, volume);
407
+ return 0;
408
+ }
409
+ /** Gets the volume for a specific channel */
410
+ audioGetChannelVolume(channel) {
411
+ const fn = this.handle.audio_get_channel_volume;
412
+ if (fn)
413
+ return fn.call(this.handle, channel);
414
+ return this._audioChannelVolumes.get(channel) ?? this._audioGlobalVolume;
415
+ }
416
+ /** Returns non-zero when the player is still playing */
417
+ audioIsPlaying(playerId) {
418
+ const fn = this.handle.audio_is_playing;
419
+ if (fn)
420
+ return fn.call(this.handle, playerId);
421
+ return this._activeAudioPlayers.has(playerId) ? 1 : 0;
422
+ }
423
+ /** Returns the number of active audio players */
424
+ audioActiveCount() {
425
+ const fn = this.handle.audio_active_count;
426
+ return fn ? fn.call(this.handle) : this._activeAudioPlayers.size;
427
+ }
428
+ /** Cleans up finished audio players */
429
+ audioCleanupFinished() {
430
+ const fn = this.handle.audio_cleanup_finished;
431
+ return fn ? fn.call(this.handle) : 0;
432
+ }
433
+ /** Plays audio with 3D spatial attenuation */
434
+ audioPlaySpatial3d(data, sourceX, sourceY, sourceZ, listenerX, listenerY, listenerZ, maxDistance, rolloff) {
435
+ const playerId = this.handle.audio_play_spatial_3d(data, sourceX, sourceY, sourceZ, listenerX, listenerY, listenerZ, maxDistance, rolloff);
436
+ if (playerId >= 0)
437
+ this._activeAudioPlayers.add(playerId);
438
+ return playerId;
439
+ }
440
+ /** Updates 3D spatial attenuation for an active player */
441
+ audioUpdateSpatial3d(playerId, sourceX, sourceY, sourceZ, listenerX, listenerY, listenerZ, maxDistance, rolloff) {
442
+ return this.handle.audio_update_spatial_volume_3d(playerId, sourceX, sourceY, sourceZ, listenerX, listenerY, listenerZ, maxDistance, rolloff);
443
+ }
444
+ /** Sets the 3D listener position */
445
+ audioSetListenerPosition3d(x, y, z) { return this.handle.audio_set_listener_position_3d(x, y, z); }
446
+ /** Sets the 3D source position for an active player */
447
+ audioSetSourcePosition3d(playerId, x, y, z, maxDistance, rolloff) {
448
+ return this.handle.audio_set_source_position_3d(playerId, x, y, z, maxDistance, rolloff);
449
+ }
450
+ /** Sets the volume for an active player */
451
+ audioSetPlayerVolume(playerId, volume) { return this.handle.audio_set_player_volume(playerId, volume); }
452
+ /** Sets the playback speed for an active player */
453
+ audioSetPlayerSpeed(playerId, speed) { return this.handle.audio_set_player_speed(playerId, speed); }
454
+ /** Applies an immediate crossfade mix between two active players */
455
+ audioCrossfade(fromPlayerId, toPlayerId, mix) {
456
+ return this.handle.audio_crossfade(fromPlayerId, toPlayerId, mix);
457
+ }
458
+ /** Starts a timed crossfade from one player to a new audio asset */
459
+ audioCrossfadeTo(fromPlayerId, data, durationSec, channel) {
460
+ const fn = this.handle.audio_crossfade_to;
461
+ const playerId = fn ? fn.call(this.handle, fromPlayerId, data, durationSec, channel) : this.handle.audio_play(data);
462
+ if (playerId >= 0)
463
+ this._activeAudioPlayers.add(playerId);
464
+ if (!fn && playerId >= 0)
465
+ this.handle.audio_crossfade(fromPlayerId, playerId, 1.0);
466
+ return playerId;
467
+ }
468
+ /** Mixes a secondary audio asset with a primary player */
469
+ audioMixWith(primaryPlayerId, data, secondaryVolume, secondaryChannel) {
470
+ const fn = this.handle.audio_mix_with;
471
+ const playerId = fn ? fn.call(this.handle, primaryPlayerId, data, secondaryVolume, secondaryChannel) : this.handle.audio_play(data);
472
+ if (playerId >= 0)
473
+ this._activeAudioPlayers.add(playerId);
474
+ if (!fn && playerId >= 0) {
475
+ this.handle.audio_set_player_volume(playerId, secondaryVolume);
476
+ const mix = Math.max(0, Math.min(1, secondaryVolume));
477
+ this.handle.audio_crossfade(primaryPlayerId, playerId, mix);
478
+ }
479
+ return playerId;
480
+ }
481
+ /** Advances all active timed crossfades */
482
+ audioUpdateCrossfades(deltaSec) {
483
+ const fn = this.handle.audio_update_crossfades;
484
+ return fn ? fn.call(this.handle, deltaSec) : 0;
485
+ }
486
+ /** Returns the number of active timed crossfades */
487
+ audioActiveCrossfadeCount() {
488
+ const fn = this.handle.audio_active_crossfade_count;
489
+ return fn ? fn.call(this.handle) : 0;
490
+ }
491
+ /** Activates audio playback on platforms that require user gesture initialization */
492
+ audioActivate() {
493
+ const fn = this.handle.audio_activate;
494
+ return fn ? fn.call(this.handle) : 0;
495
+ }
188
496
  /** Returns true if the key is currently held down */
189
497
  isKeyPressed(key) { return this.handle.is_key_pressed(key); }
190
498
  /** Returns true if the key was pressed this frame */
@@ -236,6 +544,10 @@ export class GoudGame {
236
544
  entityCount() { return this.handle.entity_count(); }
237
545
  /** Returns true if the entity still exists */
238
546
  isAlive(entity) { return this.handle.is_alive(entity.toBits()); }
547
+ /** Clones an entity, creating a new entity with copies of all cloneable components */
548
+ cloneEntity(entity) { return new WebEntity(this.handle.clone_entity(entity.toBits())); }
549
+ /** Clones an entity and all its descendants recursively */
550
+ cloneEntityRecursive(entity) { return new WebEntity(this.handle.clone_entity_recursive(entity.toBits())); }
239
551
  /** Attaches a Transform2D component to the entity */
240
552
  addTransform2d(entity, t) { this.handle.add_transform2d(entity.toBits(), t.positionX, t.positionY, t.rotation, t.scaleX, t.scaleY); }
241
553
  /** Returns the entity's Transform2D, or null if absent */
@@ -274,6 +586,73 @@ export class GoudGame {
274
586
  hasName(entity) { return this.handle.has_name(entity.toBits()); }
275
587
  /** Removes the Name from the entity */
276
588
  removeName(entity) { return this.handle.remove_name(entity.toBits()); }
589
+ loadScene(_name, _json) {
590
+ throw new Error('Not supported in WASM mode');
591
+ }
592
+ unloadScene(_name) {
593
+ throw new Error('Not supported in WASM mode');
594
+ }
595
+ setActiveScene(_sceneId, _active) {
596
+ throw new Error('Not supported in WASM mode');
597
+ }
598
+ networkHost(protocol, port) {
599
+ return this.handle.network_host(protocol, port);
600
+ }
601
+ networkConnect(protocol, address, port) {
602
+ return this.handle.network_connect(protocol, address, port);
603
+ }
604
+ networkConnectWithPeer(protocol, address, port) {
605
+ const result = this.handle.network_connect_with_peer(protocol, address, port);
606
+ return { handle: result.handle, peerId: Number(result.peer_id) };
607
+ }
608
+ networkDisconnect(handle) {
609
+ return this.handle.network_disconnect(handle);
610
+ }
611
+ networkSend(handle, peerId, data, channel) {
612
+ return this.handle.network_send(handle, BigInt(peerId), data, channel);
613
+ }
614
+ networkReceive(handle) {
615
+ return this.handle.network_receive(handle);
616
+ }
617
+ networkReceivePacket(handle) {
618
+ const packet = this.handle.network_receive_packet(handle);
619
+ if (!packet)
620
+ return null;
621
+ return { peerId: Number(packet.peer_id), data: packet.data };
622
+ }
623
+ networkPoll(handle) {
624
+ return this.handle.network_poll(handle);
625
+ }
626
+ getNetworkStats(handle) {
627
+ const stats = this.handle.get_network_stats(handle);
628
+ return {
629
+ bytesSent: Number(stats.bytes_sent),
630
+ bytesReceived: Number(stats.bytes_received),
631
+ packetsSent: Number(stats.packets_sent),
632
+ packetsReceived: Number(stats.packets_received),
633
+ packetsLost: Number(stats.packets_lost),
634
+ rttMs: stats.rtt_ms,
635
+ sendBandwidthBytesPerSec: stats.send_bandwidth_bytes_per_sec,
636
+ receiveBandwidthBytesPerSec: stats.receive_bandwidth_bytes_per_sec,
637
+ packetLossPercent: stats.packet_loss_percent,
638
+ jitterMs: stats.jitter_ms,
639
+ };
640
+ }
641
+ networkPeerCount(handle) {
642
+ return this.handle.network_peer_count(handle);
643
+ }
644
+ setNetworkSimulation(handle, config) {
645
+ return this.handle.set_network_simulation(handle, config.oneWayLatencyMs, config.jitterMs, config.packetLossPercent);
646
+ }
647
+ clearNetworkSimulation(handle) {
648
+ return this.handle.clear_network_simulation(handle);
649
+ }
650
+ setNetworkOverlayHandle(handle) {
651
+ return this.handle.set_network_overlay_handle(handle);
652
+ }
653
+ clearNetworkOverlayHandle() {
654
+ return this.handle.clear_network_overlay_handle();
655
+ }
277
656
  /** AABB vs AABB collision test with contact */
278
657
  collisionAabbAabb(centerAx, centerAy, halfWa, halfHa, centerBx, centerBy, halfWb, halfHb) {
279
658
  const c = this.handle.collision_aabb_aabb(centerAx, centerAy, halfWa, halfHa, centerBx, centerBy, halfWb, halfHb);
@@ -332,5 +711,154 @@ export class GoudGame {
332
711
  disableDepthTest() { }
333
712
  clearDepth() { }
334
713
  disableBlending() { }
714
+ // TODO: wasm FPS overlay -- these stub methods satisfy the IGoudGame interface
715
+ getFpsStats() { return { currentFps: this.handle.fps, minFps: 0, maxFps: 0, avgFps: 0, frameTimeMs: 0 }; }
716
+ setFpsOverlayEnabled(_enabled) { }
717
+ setFpsUpdateInterval(_interval) { }
718
+ setFpsOverlayCorner(_corner) { }
719
+ getDebuggerSnapshotJson() { return this.handle.getDebuggerSnapshotJson(); }
720
+ getDebuggerManifestJson() { return '{}'; }
721
+ setDebuggerPaused(paused) { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'set_paused', paused })); }
722
+ stepDebugger(kind, count) { this.handle.dispatchDebuggerRequest(JSON.stringify(kind === 0 ? { verb: 'step', frames: count } : { verb: 'step', frames: 0, ticks: count })); }
723
+ setDebuggerTimeScale(scale) { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'set_time_scale', time_scale: scale })); }
724
+ setDebuggerDebugDrawEnabled(enabled) { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'set_debug_draw_enabled', enabled })); }
725
+ injectDebuggerKeyEvent(key, pressed) { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'inject_input', events: [{ device: 'keyboard', action: pressed ? 'press' : 'release', key: String(key) }] })); }
726
+ injectDebuggerMouseButton(button, pressed) { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'inject_input', events: [{ device: 'mouse', action: pressed ? 'press' : 'release', button: String(button) }] })); }
727
+ injectDebuggerMousePosition(position) { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'inject_input', events: [{ device: 'mouse', action: 'move', position: [position.x, position.y] }] })); }
728
+ injectDebuggerScroll(delta) { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'inject_input', events: [{ device: 'mouse', action: 'scroll', delta: [delta.x, delta.y] }] })); }
729
+ setDebuggerProfilingEnabled(enabled) { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'set_profiling_enabled', enabled })); }
730
+ setDebuggerSelectedEntity(entityId) { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'set_selected_entity', entity_id: entityId })); }
731
+ clearDebuggerSelectedEntity() { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'clear_selected_entity' })); }
732
+ getMemorySummary() { return { rendering: { currentBytes: 0, peakBytes: 0 }, assets: { currentBytes: 0, peakBytes: 0 }, ecs: { currentBytes: 0, peakBytes: 0 }, ui: { currentBytes: 0, peakBytes: 0 }, audio: { currentBytes: 0, peakBytes: 0 }, network: { currentBytes: 0, peakBytes: 0 }, debugger: { currentBytes: 0, peakBytes: 0 }, other: { currentBytes: 0, peakBytes: 0 }, totalCurrentBytes: 0, totalPeakBytes: 0 }; }
733
+ captureDebuggerFrame() {
734
+ const snapshot = this.handle.getDebuggerSnapshotJson();
735
+ let imagePng = new Uint8Array(0);
736
+ try {
737
+ const dataUrl = this.canvas.toDataURL('image/png');
738
+ const base64 = dataUrl.split(',')[1] ?? '';
739
+ const binary = atob(base64);
740
+ imagePng = new Uint8Array(binary.length);
741
+ for (let i = 0; i < binary.length; i++)
742
+ imagePng[i] = binary.charCodeAt(i);
743
+ }
744
+ catch { }
745
+ return { imagePng, metadataJson: '{}', snapshotJson: snapshot, metricsTraceJson: '{}' };
746
+ }
747
+ startDebuggerRecording() { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'start_recording' })); }
748
+ stopDebuggerRecording() { const r = this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'stop_recording' })); const parsed = JSON.parse(r); return { manifestJson: parsed?.result?.manifest_json ?? '{}', data: new Uint8Array(parsed?.result?.data ?? []) }; }
749
+ startDebuggerReplay(recording) { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'start_replay', data: Array.from(recording) })); }
750
+ stopDebuggerReplay() { this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'stop_replay' })); }
751
+ getDebuggerReplayStatusJson() { return this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'get_replay_status' })); }
752
+ getDebuggerMetricsTraceJson() { return this.handle.dispatchDebuggerRequest(JSON.stringify({ verb: 'get_metrics_trace' })); }
753
+ // TODO: wasm animation -- these stub methods satisfy the IGoudGame interface
754
+ play(_entity) { return 0; }
755
+ setState(_entity, _stateName) { return 0; }
756
+ setParameterBool(_entity, _name, _value) { return 0; }
757
+ setParameterFloat(_entity, _name, _value) { return 0; }
758
+ animationLayerStackCreate(_entity) { return 0; }
759
+ animationLayerAdd(_entity, _name, _blendMode) { return 0; }
760
+ animationLayerSetWeight(_entity, _layerIndex, _weight) { return 0; }
761
+ animationLayerPlay(_entity, _layerIndex) { return 0; }
762
+ animationLayerSetClip(_entity, _layerIndex, _frameCount, _frameDuration, _mode) { return 0; }
763
+ animationLayerAddFrame(_entity, _layerIndex, _x, _y, _w, _h) { return 0; }
764
+ animationLayerReset(_entity, _layerIndex) { return 0; }
765
+ animationClipAddEvent(_entity, _frameIndex, _name, _payloadType, _payloadInt, _payloadFloat, _payloadString) { return 0; }
766
+ animationEventsCount() { return 0; }
767
+ animationEventsRead(_index) { return { entity: 0, name: '', frameIndex: 0, payloadType: 0, payloadInt: 0, payloadFloat: 0, payloadString: '' }; }
768
+ /** Casts a filtered ray and returns the full hit payload, or null when no hit is found (FFI: goud_physics_raycast_ex) */
769
+ physicsRaycastEx(_originX, _originY, _dirX, _dirY, _maxDist, _layerMask) { return null; }
770
+ /** Returns the number of queued physics collision events available to read (FFI: goud_physics_collision_events_count) */
771
+ physicsCollisionEventsCount() { throw new Error('Not supported in WASM mode'); }
772
+ /** Reads one queued physics collision event by index, or null when the index is out of range (FFI: goud_physics_collision_events_read) */
773
+ physicsCollisionEventsRead(_index) { return null; }
774
+ /** Registers or clears a physics collision callback function pointer. C# callers must keep the callback delegate alive for the full registration lifetime. Python and TypeScript wrappers support clear-only (`callbackPtr = 0`, `userData = 0`) for safety. (FFI: goud_physics_set_collision_callback) */
775
+ physicsSetCollisionCallback(_callbackPtr, _userData) { throw new Error('Not supported in WASM mode'); }
776
+ // Provider capability queries
777
+ getRenderCapabilities() { return WEB_RENDER_CAPABILITIES; }
778
+ getPhysicsCapabilities() { return WEB_PHYSICS_CAPABILITIES; }
779
+ getAudioCapabilities() { return WEB_AUDIO_CAPABILITIES; }
780
+ getInputCapabilities() { return WEB_INPUT_CAPABILITIES; }
781
+ getNetworkCapabilities() {
782
+ const caps = this.handle.get_network_capabilities();
783
+ return {
784
+ supportsHosting: caps.supports_hosting,
785
+ maxConnections: caps.max_connections,
786
+ maxChannels: caps.max_channels,
787
+ maxMessageSize: caps.max_message_size,
788
+ };
789
+ }
790
+ /** Checks if the hot-swap keyboard shortcut (F5) was pressed and cycles the render provider to null. Debug builds only. Returns true if a swap occurred. */
791
+ checkHotSwapShortcut() { throw new Error('Not supported in WASM mode'); }
792
+ /** Connect to a WebSocket debugger relay for MCP tool support. */
793
+ connectDebugger(wsUrl) {
794
+ const url = wsUrl ?? 'ws://127.0.0.1:9229';
795
+ this.handle.initDebugger?.('web-game');
796
+ const ws = new WebSocket(url);
797
+ ws.onopen = () => {
798
+ ws.send(JSON.stringify({ route_label: 'web-game', surface_kind: 'wasm_browser', capabilities: ['capture', 'control_plane', 'replay'] }));
799
+ };
800
+ ws.onmessage = (event) => {
801
+ const request = JSON.parse(event.data);
802
+ if (request.type === 'registration_ack')
803
+ return;
804
+ if (request.verb === 'capture_frame') {
805
+ const snapshot = this.handle.getDebuggerSnapshotJson?.() ?? '{}';
806
+ let imagePngBase64 = '';
807
+ try {
808
+ imagePngBase64 = this.canvas.toDataURL('image/png').split(',')[1] ?? '';
809
+ }
810
+ catch { }
811
+ ws.send(JSON.stringify({ ok: true, result: { image_png_base64: imagePngBase64, snapshot_json: snapshot, metadata_json: '{}', metrics_trace_json: '{}' } }));
812
+ return;
813
+ }
814
+ const response = this.handle.dispatchDebuggerRequest?.(JSON.stringify(request)) ?? '{}';
815
+ ws.send(response);
816
+ };
817
+ }
818
+ }
819
+ /** Builder for configuring and creating a GoudGame instance with provider selection. */
820
+ export class EngineConfig {
821
+ constructor() {
822
+ this._config = {};
823
+ }
824
+ /** Sets the window title */
825
+ setTitle(title) {
826
+ this._config.title = title;
827
+ return this;
828
+ }
829
+ /** Sets the window size in pixels */
830
+ setSize(width, height) {
831
+ this._config.width = width;
832
+ this._config.height = height;
833
+ return this;
834
+ }
835
+ /** Enables or disables the FPS debug overlay */
836
+ setFpsOverlay(_enabled) {
837
+ // FPS overlay is not yet supported in WASM; accepted for API parity.
838
+ return this;
839
+ }
840
+ /** Enables or disables physics debug visualization */
841
+ setPhysicsDebug(_enabled) {
842
+ return this;
843
+ }
844
+ /** Selects the 2D physics backend used by the built game */
845
+ setPhysicsBackend2D(_backend) {
846
+ return this;
847
+ }
848
+ /** Configures debugger runtime startup for the created game. */
849
+ setDebugger(debuggerConfig) {
850
+ this._config.debugger = debuggerConfig;
851
+ return this;
852
+ }
853
+ /** Consumes the config and creates a windowed GoudGame instance */
854
+ async build() {
855
+ const game = await GoudGame.create(this._config);
856
+ if (this._config.debugger?.enabled) {
857
+ game.handle.initDebugger?.(this._config.debugger.routeLabel ?? 'web');
858
+ }
859
+ return game;
860
+ }
861
+ /** Frees the config without building */
862
+ destroy() { }
335
863
  }
336
864
  //# sourceMappingURL=index.g.js.map