@unboxy/phaser-sdk 0.2.38 → 0.2.40

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.
@@ -101,30 +101,8 @@ const LAYER_DEPTH = {
101
101
  function createImageOrNinePatch(scene, x, y, width, height, asset, frame) {
102
102
  if (asset.ninePatch) {
103
103
  const np = asset.ninePatch;
104
- const isPerFrame = isPerFrameNinePatch(np);
105
- const cuts = isPerFrame ? np.perFrame : np;
106
- const factory = scene.add.rexNinePatch;
107
- if (factory) {
108
- const columns = [cuts.leftWidth, undefined, cuts.rightWidth];
109
- const rows = [cuts.topHeight, undefined, cuts.bottomHeight];
110
- // Per-frame uses rex's 9-arg `(scene, x, y, w, h, key, baseFrame, cols, rows)`
111
- // form — baseFrame scopes the slicing to that one cell of the spritesheet.
112
- // rex types baseFrame as string; Phaser accepts either string or number
113
- // for spritesheet frame access, but passing String() satisfies the typed
114
- // overload on rex's side.
115
- if (isPerFrame) {
116
- return factory.call(scene.add, x, y, width, height, asset.textureKey, String(frame ?? 0), columns, rows);
117
- }
118
- // Middle column/row left undefined → that segment stretches to fill
119
- // the remaining space between the two fixed-width edges.
120
- return factory.call(scene.add, x, y, width, height, asset.textureKey, columns, rows);
121
- }
122
- // Plugin not registered — degrade to a stretched Image rather than crash
123
- // an entire HUD render. Symptom is "corners distort on resize", not a
124
- // missing widget. Likely cause: a host that bypassed createUnboxyGame's
125
- // auto-registration (e.g. an external Phaser.Game instance).
126
- // eslint-disable-next-line no-console
127
- console.warn(`[unboxy/hud] rexNinePatch plugin not registered; falling back to stretched Image for asset '${asset.id}'`);
104
+ const cuts = isPerFrameNinePatch(np) ? np.perFrame : np;
105
+ return createCustomNinePatch(scene, x, y, width, height, asset, cuts, frame);
128
106
  }
129
107
  // Fallback: plain Image. Frame index threaded through for spritesheet
130
108
  // assets so per-cell rendering still works without 9-slice.
@@ -134,6 +112,105 @@ function createImageOrNinePatch(scene, x, y, width, height, asset, frame) {
134
112
  image.setDisplaySize(width, height);
135
113
  return image;
136
114
  }
115
+ /**
116
+ * 9-slice renderer built directly on Phaser primitives. We tried
117
+ * `phaser3-rex-plugins`' NinePatch first (slice 7.5/7.6) but ran into a
118
+ * RenderTexture sizing bug in Phaser 3.60+ that's hard to work around
119
+ * without monkey-patching: rex's constructor calls `super(scene)` with no
120
+ * dims (defaulting the DynamicTexture to 32×32), then `setSize(w, h)` —
121
+ * which in Phaser 3.60+ only sets display dimensions, not the underlying
122
+ * texture. The 9-slice gets drawn into the small default canvas and
123
+ * displayed stretched. Reproducing this without rex is faster than the
124
+ * monkey-patch dance.
125
+ *
126
+ * <p>Algorithm: split the source texture into 9 sub-frames via
127
+ * `texture.add()` (idempotent — Phaser ignores re-adds), spawn one Phaser
128
+ * Image per sub-frame, position + stretch them inside a Container. Corners
129
+ * keep their native dimensions; edges stretch in one axis; the middle
130
+ * stretches in both. Returns the container, sized to the target dims with
131
+ * an explicit hit area so `setInteractive` works as expected on the
132
+ * icon-button widget.
133
+ *
134
+ * <p>Source layout: `(sx, sy)` is the source-image origin within the
135
+ * texture; for plain Images it's (0,0). For per-frame on a uniform-grid
136
+ * sheet, `frame` selects which cell — the corresponding frame's `cutX/Y`
137
+ * gives the origin.
138
+ */
139
+ function createCustomNinePatch(scene, x, y, width, height, asset, cuts, frame) {
140
+ const texKey = asset.textureKey;
141
+ const texture = scene.textures.get(texKey);
142
+ // Per-frame variant: look up the specified cell's source rect. Single
143
+ // image: the texture's `__BASE` frame covers the entire image.
144
+ const baseFrame = frame !== undefined
145
+ ? texture.get(frame)
146
+ : texture.get('__BASE');
147
+ const sx = baseFrame.cutX;
148
+ const sy = baseFrame.cutY;
149
+ const sw = baseFrame.width;
150
+ const sh = baseFrame.height;
151
+ const L = cuts.leftWidth;
152
+ const R = cuts.rightWidth;
153
+ const T = cuts.topHeight;
154
+ const B = cuts.bottomHeight;
155
+ // Source middle widths/heights (the part that stretches).
156
+ const midSrcW = Math.max(0, sw - L - R);
157
+ const midSrcH = Math.max(0, sh - T - B);
158
+ // Target middle widths/heights — what remains after the four corners are
159
+ // placed at their fixed sizes. When the widget is sized smaller than
160
+ // L+R / T+B, the corners overlap; rare but handled by clamping to 0
161
+ // rather than producing negative-size sprites.
162
+ const midDstW = Math.max(0, width - L - R);
163
+ const midDstH = Math.max(0, height - T - B);
164
+ const container = scene.add.container(x, y);
165
+ // Register a sub-frame on the source texture (idempotent) and add a
166
+ // Phaser Image rendering that frame at the target position + size.
167
+ // Position is relative to container's origin; Image origin (0, 0) makes
168
+ // (dx, dy) the top-left of the sub-region.
169
+ const frameSuffix = frame !== undefined ? `__f${frame}` : '';
170
+ const addSubFrame = (suffix, fx, fy, fw, fh, dx, dy, dw, dh) => {
171
+ if (fw <= 0 || fh <= 0 || dw <= 0 || dh <= 0)
172
+ return;
173
+ const subKey = `${texKey}${frameSuffix}_${suffix}`;
174
+ if (!texture.has(subKey)) {
175
+ texture.add(subKey, 0, sx + fx, sy + fy, fw, fh);
176
+ }
177
+ const img = scene.add.image(dx, dy, texKey, subKey);
178
+ img.setOrigin(0, 0);
179
+ img.setDisplaySize(dw, dh);
180
+ container.add(img);
181
+ };
182
+ // Layout offsets — container's local origin is at its center, so the
183
+ // 9-slice patch spans (-width/2, -height/2) to (width/2, height/2).
184
+ const x0 = -width / 2;
185
+ const y0 = -height / 2;
186
+ const xL = x0 + L;
187
+ const xR = x0 + width - R;
188
+ const yT = y0 + T;
189
+ const yB = y0 + height - B;
190
+ // Source frame offsets within the base frame.
191
+ const fxL = 0;
192
+ const fxM = L;
193
+ const fxR = sw - R;
194
+ const fyT = 0;
195
+ const fyM = T;
196
+ const fyB = sh - B;
197
+ // Top row
198
+ addSubFrame('tl', fxL, fyT, L, T, x0, y0, L, T);
199
+ addSubFrame('tm', fxM, fyT, midSrcW, T, xL, y0, midDstW, T);
200
+ addSubFrame('tr', fxR, fyT, R, T, xR, y0, R, T);
201
+ // Middle row
202
+ addSubFrame('ml', fxL, fyM, L, midSrcH, x0, yT, L, midDstH);
203
+ addSubFrame('mm', fxM, fyM, midSrcW, midSrcH, xL, yT, midDstW, midDstH);
204
+ addSubFrame('mr', fxR, fyM, R, midSrcH, xR, yT, R, midDstH);
205
+ // Bottom row
206
+ addSubFrame('bl', fxL, fyB, L, B, x0, yB, L, B);
207
+ addSubFrame('bm', fxM, fyB, midSrcW, B, xL, yB, midDstW, B);
208
+ addSubFrame('br', fxR, fyB, R, B, xR, yB, R, B);
209
+ // Explicit hit area so setInteractive works (Phaser containers have no
210
+ // intrinsic bounds; the icon-button widget calls setInteractive on this).
211
+ container.setSize(width, height);
212
+ return container;
213
+ }
137
214
  /**
138
215
  * Spawn a HUD widget into the scene. The returned GameObject is recorded in
139
216
  * the entity registry so the editor + behavior code can find it by id.
@@ -216,21 +293,29 @@ function createImage(ctx, entity, pos) {
216
293
  container.setSize(w, h);
217
294
  return container;
218
295
  }
219
- // 9-slice path. When the asset has `ninePatch` metadata the SDK renders via
220
- // rex-plugins NinePatch (corners fixed; edges + center stretch). NinePatch
221
- // requires explicit dims, so missing width/height falls back to the texture's
222
- // native source dims keeps existing data shapes (image-without-dims) working.
296
+ // 9-slice path. When the asset has `ninePatch` metadata, the SDK renders
297
+ // via our custom Container-based NinePatch (corners fixed; edges + center
298
+ // stretch). Missing width/height falls back to the texture's native source
299
+ // dims so existing image-without-dims data still works.
223
300
  if (asset.ninePatch) {
224
301
  const tex = ctx.scene.textures.get(asset.textureKey);
225
302
  const src = tex?.getSourceImage();
226
303
  const w = entity.visual.width ?? src?.width ?? 64;
227
304
  const h = entity.visual.height ?? src?.height ?? 64;
228
305
  const np = createImageOrNinePatch(ctx.scene, pos.x, pos.y, w, h, asset);
229
- if (entity.visual.tint)
230
- np.setTint?.(parseColor(entity.visual.tint));
231
- if (typeof entity.visual.alpha === 'number')
232
- np.setAlpha(entity.visual.alpha);
233
- applyOriginFromAnchor(np, entity.anchor.side);
306
+ if (typeof entity.visual.alpha === 'number') {
307
+ np.setAlpha?.(entity.visual.alpha);
308
+ }
309
+ // The custom NinePatch is a Container with children centered around
310
+ // (0, 0). Use the same origin-shift trick the container widgets use so
311
+ // the anchor side resolves correctly. Tint isn't supported on
312
+ // Container; image widgets needing tint should bypass ninePatch.
313
+ if (np instanceof Phaser.GameObjects.Container) {
314
+ applyContainerOriginShift(np, entity.anchor.side, w, h);
315
+ }
316
+ else {
317
+ applyOriginFromAnchor(np, entity.anchor.side);
318
+ }
234
319
  return np;
235
320
  }
236
321
  const image = entity.visual.frame !== undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboxy/phaser-sdk",
3
- "version": "0.2.38",
3
+ "version": "0.2.40",
4
4
  "description": "Unboxy Phaser 3 SDK — game infrastructure for the Unboxy platform",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",