@umicat/phaser-sdk 1.0.0 → 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/SDK-GUIDE.md CHANGED
@@ -626,10 +626,11 @@ class GameScene extends Phaser.Scene {
626
626
  // ...wire physics + input on `this.player`...
627
627
  }
628
628
  update() {
629
- // Safe: loadWorldScene suspends update() during its await, so any
630
- // class field assigned after `await loadWorldScene(...)` is defined
631
- // by the first time update() ticks. No `if (!this.player) return;`
632
- // guard needed.
629
+ // Safe: an async create() suspends update() until it resolves. The SDK
630
+ // gates this in createUmicatGame (since 1.0.1) for EVERY registered
631
+ // scene, so any class field assigned after ANY `await` in create()
632
+ // loadWorldScene, saves.get(), umicatReady, a fetch — is defined by the
633
+ // first time update() ticks. No `if (!this.player) return;` guard needed.
633
634
  this.player.setVelocityX(0);
634
635
  }
635
636
  }
@@ -6,11 +6,70 @@ import { setupEditorModeListener } from '../scene/EditorMode.js';
6
6
  import { ORIENTATION_DIMENSIONS } from '../orientation.js';
7
7
  import { setRenderScriptRegistry, } from '../scene/renderScripts.js';
8
8
  import { UmicatHudScene } from '../scene/HudRuntime.js';
9
+ /**
10
+ * Make a Promise-returning `create()` safe against Phaser's update loop.
11
+ *
12
+ * Phaser does NOT await an async `create()`. The moment `create()` suspends
13
+ * at its first `await`, Phaser flips the scene to RUNNING and starts calling
14
+ * `update()` every frame — so any object the game builds *after* that await
15
+ * (a HUD text, the player sprite) is `undefined` for those frames. An
16
+ * `update()` that reads it throws on every frame and the game appears frozen
17
+ * on the loading screen. This is a very easy trap to fall into: the docs
18
+ * pattern `const hi = await saves.get('highScore')` at the top of `create()`
19
+ * is enough to trigger it.
20
+ *
21
+ * We give async `create()` the same guarantee `preload()` already has —
22
+ * "update() does not run until setup is finished" — by wrapping each scene so
23
+ * `update()` is a no-op until the `create()` promise settles. Synchronous
24
+ * `create()` is untouched (it never returns a thenable, so the gate is never
25
+ * armed). Re-runs cleanly across `scene.restart()` since the flag lives on
26
+ * the scene instance and is re-armed each create().
27
+ */
28
+ function guardAsyncCreate(SceneClass) {
29
+ const proto = SceneClass.prototype;
30
+ // Patch the prototype at most once, even under HMR / repeated factory calls.
31
+ if (proto.__umicatGuarded)
32
+ return;
33
+ const origCreate = proto.create;
34
+ if (typeof origCreate !== 'function')
35
+ return; // no create() → nothing to gate
36
+ proto.__umicatGuarded = true;
37
+ const PENDING = '__umicatCreatePending';
38
+ proto.create = function (...args) {
39
+ const ret = origCreate.apply(this, args);
40
+ if (ret && typeof ret.then === 'function') {
41
+ this[PENDING] = true;
42
+ Promise.resolve(ret)
43
+ .catch((e) => {
44
+ // Surface the failure instead of hanging update() forever.
45
+ // eslint-disable-next-line no-console
46
+ console.error('[umicat] async create() rejected:', e);
47
+ })
48
+ .finally(() => {
49
+ this[PENDING] = false;
50
+ });
51
+ }
52
+ return ret;
53
+ };
54
+ const origUpdate = proto.update;
55
+ if (typeof origUpdate === 'function') {
56
+ proto.update = function (...args) {
57
+ if (this[PENDING])
58
+ return undefined; // setup still running — skip this frame
59
+ return origUpdate.apply(this, args);
60
+ };
61
+ }
62
+ }
9
63
  /**
10
64
  * Create an Umicat-enhanced Phaser game instance.
11
65
  * Includes built-in integrations: screenshot capture, preserveDrawingBuffer, etc.
12
66
  */
13
67
  export function createUmicatGame(options) {
68
+ // Harden every game scene against the async-create()/update() race before
69
+ // Phaser instantiates them (see guardAsyncCreate). The auto-registered
70
+ // UmicatHudScene is ours and uses a sync create(), so it's left alone.
71
+ for (const SceneClass of options.scenes)
72
+ guardAsyncCreate(SceneClass);
14
73
  const { width, height } = 'orientation' in options && options.orientation
15
74
  ? ORIENTATION_DIMENSIONS[options.orientation]
16
75
  : { width: options.width, height: options.height };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import Phaser from 'phaser';
2
+ // phaser3-rex-plugins (e.g. the NinePatch plugin) were written for Phaser 3's
3
+ // UMD build, which set a global `window.Phaser`. Their module bodies reference a
4
+ // bare `Phaser` at eval time (e.g. `Phaser.Utils.Objects.GetValue`). Phaser 4 is
5
+ // pure ESM and does NOT expose a global, so those plugins throw
6
+ // "Phaser is not defined". Expose Phaser on globalThis here.
7
+ //
8
+ // IMPORTANT: this module must be imported BEFORE any `phaser3-rex-plugins/*`
9
+ // import so the global exists before the plugin's top-level code evaluates.
10
+ globalThis.Phaser = Phaser;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umicat/phaser-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Umicat Phaser 3 SDK — game infrastructure for the Umicat platform",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",