@unboxy/phaser-sdk 0.2.35 → 0.2.37
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/dist/core/UnboxyGame.js +16 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/scene/HudRuntime.js +206 -57
- package/dist/scene/SceneLoader.d.ts +27 -0
- package/dist/scene/SceneLoader.js +55 -4
- package/dist/scene/types.d.ts +56 -1
- package/package.json +4 -2
package/dist/core/UnboxyGame.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Phaser from 'phaser';
|
|
2
|
+
import NinePatchPlugin from 'phaser3-rex-plugins/plugins/ninepatch-plugin.js';
|
|
2
3
|
import { setupScreenshotListener } from '../screenshot/ScreenshotManager.js';
|
|
3
4
|
import { setupRecordingListener } from '../recording/RecordingManager.js';
|
|
4
5
|
import { setupEditorModeListener } from '../scene/EditorMode.js';
|
|
@@ -13,6 +14,20 @@ export function createUnboxyGame(options) {
|
|
|
13
14
|
const { width, height } = 'orientation' in options && options.orientation
|
|
14
15
|
? ORIENTATION_DIMENSIONS[options.orientation]
|
|
15
16
|
: { width: options.width, height: options.height };
|
|
17
|
+
// Auto-register rex-plugins NinePatch so HUD widgets with 9-slice asset
|
|
18
|
+
// metadata (`Asset.ninePatch`) can scale without corner distortion. Plugin
|
|
19
|
+
// exposes `scene.add.rexNinePatch(...)` factory; HudRuntime calls it on
|
|
20
|
+
// demand. Merged with any user-supplied `plugins.global` so existing
|
|
21
|
+
// rexVirtualJoystick / rexUI registrations keep working.
|
|
22
|
+
const ninePatchEntry = {
|
|
23
|
+
key: 'rexNinePatchPlugin',
|
|
24
|
+
plugin: NinePatchPlugin,
|
|
25
|
+
start: true,
|
|
26
|
+
};
|
|
27
|
+
const plugins = {
|
|
28
|
+
...(options.plugins ?? {}),
|
|
29
|
+
global: [ninePatchEntry, ...(options.plugins?.global ?? [])],
|
|
30
|
+
};
|
|
16
31
|
const config = {
|
|
17
32
|
type: Phaser.AUTO,
|
|
18
33
|
backgroundColor: options.backgroundColor ?? '#1a1a2e',
|
|
@@ -30,7 +45,7 @@ export function createUnboxyGame(options) {
|
|
|
30
45
|
default: 'arcade',
|
|
31
46
|
arcade: { gravity: { x: 0, y: 0 }, debug: false },
|
|
32
47
|
},
|
|
33
|
-
|
|
48
|
+
plugins,
|
|
34
49
|
// UnboxyHudScene is auto-registered alongside the game's scenes so
|
|
35
50
|
// `loadWorldScene` can launch it when the active world scene's manifest
|
|
36
51
|
// entry sets `hud: '<id>'`. Registered at the end so the user's scenes
|
package/dist/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type { ChatMessage, ChatMessageKind } from './realtime/UnboxyRoom.js';
|
|
|
16
16
|
export { RpcError } from './core/Transport.js';
|
|
17
17
|
export type { Transport, TransportKind } from './core/Transport.js';
|
|
18
18
|
export type { UnboxyUser } from './protocol.js';
|
|
19
|
-
export { loadWorldScene, preloadManifest, preloadSceneAssets, getManifest, SCENES_BASE, MANIFEST_PATH, } from './scene/SceneLoader.js';
|
|
19
|
+
export { loadWorldScene, preloadManifest, preloadSceneAssets, applyPixelArtFilters, getManifest, SCENES_BASE, MANIFEST_PATH, } from './scene/SceneLoader.js';
|
|
20
20
|
export type { LoadWorldSceneOptions } from './scene/SceneLoader.js';
|
|
21
21
|
export { spawnEntity, parseColor } from './scene/spawnEntity.js';
|
|
22
22
|
export type { SpawnContext, AssetResolver, RenderScriptResolver, } from './scene/spawnEntity.js';
|
|
@@ -28,5 +28,5 @@ export { setupEditorBridge } from './editor/EditorBridge.js';
|
|
|
28
28
|
export { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './editor/EditorOverlayScene.js';
|
|
29
29
|
export type { EditorEntityPatch, EditorEnterMessage, EditorExitMessage, EditorGetSceneMessage, EditorApplyEditMessage, EditorSetSelectionMessage, EditorPanZoomMessage, EditorHostToSdkMessage, EditorSceneLoadedMessage, EditorSelectionPickedMessage, EditorDragEndMessage, EditorShortcutMessage, EditorCreateEntityMessage, EditorDeleteEntityMessage, EditorSetEditModeMessage, EditorSelectionRectMessage, EditorSdkToHostMessage, } from './protocol.js';
|
|
30
30
|
export { SCHEMA_VERSION, } from './scene/types.js';
|
|
31
|
-
export type { Manifest, SceneRef, HudRef, AssetRecord, AssetKind, SceneType, SceneFile, WorldScene, HudScene, WorldSceneConfig, CameraConfig, WorldEntity, WorldEntityKind, NonGroupWorldEntity, SpriteEntity, PrimitiveEntity, CodeRenderedEntity, GroupEntity, TilemapEntity, TriggerEntity, TriggerShape, WorldVisual, SpriteVisual, PrimitiveVisual, PrimitiveRectVisual, PrimitiveCircleVisual, CodeRenderedVisual, HudEntity, HudEntityKind, HudEntityBase, HudTextEntity, HudImageEntity, HudIconButtonEntity, HudProgressBarEntity, HudPanelEntity, HudVisual, HudTextVisual, HudImageVisual, HudIconButtonVisual, HudProgressBarVisual, HudPanelVisual, HudTextSource, HudNumberSource, HudLayer, Transform, Anchor, AnchorSide, } from './scene/types.js';
|
|
31
|
+
export type { Manifest, SceneRef, HudRef, AssetRecord, AssetKind, SceneType, SceneFile, WorldScene, HudScene, WorldSceneConfig, CameraConfig, WorldEntity, WorldEntityKind, NonGroupWorldEntity, SpriteEntity, PrimitiveEntity, CodeRenderedEntity, GroupEntity, TilemapEntity, TriggerEntity, TriggerShape, WorldVisual, SpriteVisual, PrimitiveVisual, PrimitiveRectVisual, PrimitiveCircleVisual, CodeRenderedVisual, HudEntity, HudEntityKind, HudEntityBase, HudTextEntity, HudImageEntity, HudIconButtonEntity, HudProgressBarEntity, HudPanelEntity, HudVisual, HudTextVisual, HudImageVisual, HudIconButtonVisual, HudProgressBarVisual, HudPanelVisual, HudTextSource, HudNumberSource, HudLayer, Transform, Anchor, AnchorSide, NinePatchConfig, } from './scene/types.js';
|
|
32
32
|
export { PROTOCOL_VERSION, type HelloMessage, type InitMessage, type RpcRequestMessage, type RpcResultOk, type RpcResultError, type HostToSdkMessage, type SdkToHostMessage, type RpcErrorPayload, type RpcMethod, type SavesGetParams, type SavesGetResult, type SavesSetParams, type SavesSetResult, type SavesDeleteParams, type SavesDeleteResult, type SavesListResult, type GameDataGetParams, type GameDataGetResult, type GameDataSetParams, type GameDataSetResult, type GameDataDeleteParams, type GameDataDeleteResult, type GameDataListResult, type RealtimeGetTokenParams, type RealtimeGetTokenResult, } from './protocol.js';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { RealtimeModule } from './realtime/RealtimeModule.js';
|
|
|
14
14
|
export { UnboxyRoom, PlayerDataFacade, RoomDataFacade, ChatFacade, MAX_CHAT_TEXT_LEN, } from './realtime/UnboxyRoom.js';
|
|
15
15
|
export { RpcError } from './core/Transport.js';
|
|
16
16
|
// Scene-as-data (visual editor foundation, slice 1)
|
|
17
|
-
export { loadWorldScene, preloadManifest, preloadSceneAssets, getManifest, SCENES_BASE, MANIFEST_PATH, } from './scene/SceneLoader.js';
|
|
17
|
+
export { loadWorldScene, preloadManifest, preloadSceneAssets, applyPixelArtFilters, getManifest, SCENES_BASE, MANIFEST_PATH, } from './scene/SceneLoader.js';
|
|
18
18
|
export { spawnEntity, parseColor } from './scene/spawnEntity.js';
|
|
19
19
|
export { EntityRegistry, attachEntityRegistry, getEntityRegistry, } from './scene/EntityRegistry.js';
|
|
20
20
|
export { setupEditorModeListener, isEditMode } from './scene/EditorMode.js';
|
package/dist/scene/HudRuntime.js
CHANGED
|
@@ -2,7 +2,7 @@ import Phaser from 'phaser';
|
|
|
2
2
|
import { SCHEMA_VERSION, } from './types.js';
|
|
3
3
|
import { attachEntityRegistry, getEntityRegistry, } from './EntityRegistry.js';
|
|
4
4
|
import { parseColor } from './spawnEntity.js';
|
|
5
|
-
import { getManifest, SCENES_BASE } from './SceneLoader.js';
|
|
5
|
+
import { applyPixelArtFilters, getManifest, SCENES_BASE } from './SceneLoader.js';
|
|
6
6
|
/**
|
|
7
7
|
* HUD runtime — slice 5.
|
|
8
8
|
*
|
|
@@ -79,6 +79,34 @@ const LAYER_DEPTH = {
|
|
|
79
79
|
overlay: 200,
|
|
80
80
|
modal: 300,
|
|
81
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* Render an image asset at (x, y) sized to (width, height). When the asset
|
|
84
|
+
* carries `ninePatch` metadata, uses `phaser3-rex-plugins`' NinePatch (corners
|
|
85
|
+
* fixed, edges + center stretch); otherwise falls back to a regular Image
|
|
86
|
+
* scaled with `setDisplaySize`. Caller owns origin/anchor — both Image and
|
|
87
|
+
* NinePatch (which extends RenderTexture) expose `setOrigin`. Returns the
|
|
88
|
+
* created GameObject already added to the scene's display list.
|
|
89
|
+
*/
|
|
90
|
+
function createImageOrNinePatch(scene, x, y, width, height, asset) {
|
|
91
|
+
if (asset.ninePatch) {
|
|
92
|
+
const np = asset.ninePatch;
|
|
93
|
+
const factory = scene.add.rexNinePatch;
|
|
94
|
+
if (factory) {
|
|
95
|
+
// Middle column/row left undefined → that segment stretches to fill
|
|
96
|
+
// the remaining space between the two fixed-width edges.
|
|
97
|
+
return factory.call(scene.add, x, y, width, height, asset.textureKey, [np.leftWidth, undefined, np.rightWidth], [np.topHeight, undefined, np.bottomHeight]);
|
|
98
|
+
}
|
|
99
|
+
// Plugin not registered — degrade to a stretched Image rather than crash
|
|
100
|
+
// an entire HUD render. Symptom is "corners distort on resize", not a
|
|
101
|
+
// missing widget. Likely cause: a host that bypassed createUnboxyGame's
|
|
102
|
+
// auto-registration (e.g. an external Phaser.Game instance).
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.warn(`[unboxy/hud] rexNinePatch plugin not registered; falling back to stretched Image for asset '${asset.id}'`);
|
|
105
|
+
}
|
|
106
|
+
const image = scene.add.image(x, y, asset.textureKey);
|
|
107
|
+
image.setDisplaySize(width, height);
|
|
108
|
+
return image;
|
|
109
|
+
}
|
|
82
110
|
/**
|
|
83
111
|
* Spawn a HUD widget into the scene. The returned GameObject is recorded in
|
|
84
112
|
* the entity registry so the editor + behavior code can find it by id.
|
|
@@ -161,6 +189,23 @@ function createImage(ctx, entity, pos) {
|
|
|
161
189
|
container.setSize(w, h);
|
|
162
190
|
return container;
|
|
163
191
|
}
|
|
192
|
+
// 9-slice path. When the asset has `ninePatch` metadata the SDK renders via
|
|
193
|
+
// rex-plugins NinePatch (corners fixed; edges + center stretch). NinePatch
|
|
194
|
+
// requires explicit dims, so missing width/height falls back to the texture's
|
|
195
|
+
// native source dims — keeps existing data shapes (image-without-dims) working.
|
|
196
|
+
if (asset.ninePatch) {
|
|
197
|
+
const tex = ctx.scene.textures.get(asset.textureKey);
|
|
198
|
+
const src = tex?.getSourceImage();
|
|
199
|
+
const w = entity.visual.width ?? src?.width ?? 64;
|
|
200
|
+
const h = entity.visual.height ?? src?.height ?? 64;
|
|
201
|
+
const np = createImageOrNinePatch(ctx.scene, pos.x, pos.y, w, h, asset);
|
|
202
|
+
if (entity.visual.tint)
|
|
203
|
+
np.setTint?.(parseColor(entity.visual.tint));
|
|
204
|
+
if (typeof entity.visual.alpha === 'number')
|
|
205
|
+
np.setAlpha(entity.visual.alpha);
|
|
206
|
+
applyOriginFromAnchor(np, entity.anchor.side);
|
|
207
|
+
return np;
|
|
208
|
+
}
|
|
164
209
|
const image = entity.visual.frame !== undefined
|
|
165
210
|
? ctx.scene.add.image(pos.x, pos.y, asset.textureKey, entity.visual.frame)
|
|
166
211
|
: ctx.scene.add.image(pos.x, pos.y, asset.textureKey);
|
|
@@ -174,19 +219,16 @@ function createImage(ctx, entity, pos) {
|
|
|
174
219
|
applyOriginFromAnchor(image, entity.anchor.side);
|
|
175
220
|
return image;
|
|
176
221
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const strokeW = v.strokeWidth ?? 0;
|
|
184
|
-
// Container at anchor pos. Children drawn with their own origins.
|
|
185
|
-
const container = ctx.scene.add.container(pos.x, pos.y);
|
|
222
|
+
/**
|
|
223
|
+
* Build the v0 colored-rect background for an icon-button (rectangle or
|
|
224
|
+
* circle). Used when `backgroundAssetId` is unset OR the referenced asset
|
|
225
|
+
* couldn't be resolved.
|
|
226
|
+
*/
|
|
227
|
+
function makeColoredRectBg(scene, v, w, h, fillColor, strokeColor, strokeW) {
|
|
186
228
|
const bg = v.shape === 'circle'
|
|
187
|
-
?
|
|
229
|
+
? scene.add.circle(0, 0, Math.max(w, h) / 2, fillColor)
|
|
188
230
|
: (() => {
|
|
189
|
-
const r =
|
|
231
|
+
const r = scene.add.rectangle(0, 0, w, h, fillColor);
|
|
190
232
|
r.setOrigin(0.5, 0.5);
|
|
191
233
|
return r;
|
|
192
234
|
})();
|
|
@@ -198,6 +240,40 @@ function createIconButton(ctx, entity, pos) {
|
|
|
198
240
|
bg.setStrokeStyle(strokeW, strokeColor);
|
|
199
241
|
}
|
|
200
242
|
}
|
|
243
|
+
return bg;
|
|
244
|
+
}
|
|
245
|
+
function createIconButton(ctx, entity, pos) {
|
|
246
|
+
const v = entity.visual;
|
|
247
|
+
const w = v.width ?? 96;
|
|
248
|
+
const h = v.height ?? 48;
|
|
249
|
+
const fillColor = parseColor(v.fillColor ?? '#3b82f6');
|
|
250
|
+
const strokeColor = v.strokeColor === null ? null : parseColor(v.strokeColor ?? '#1e40af');
|
|
251
|
+
const strokeW = v.strokeWidth ?? 0;
|
|
252
|
+
// Container at anchor pos. Children drawn with their own origins.
|
|
253
|
+
const container = ctx.scene.add.container(pos.x, pos.y);
|
|
254
|
+
// Background source (textured vs. colored-rect). When `backgroundAssetId`
|
|
255
|
+
// is set we render the asset image (NinePatch when the asset has 9-slice
|
|
256
|
+
// metadata; stretched Image otherwise); colored-rect bg is the v0 fallback.
|
|
257
|
+
// The interactive surface is always the `bg` GameObject — POINTER_DOWN/UP
|
|
258
|
+
// events fire on whichever variant is active.
|
|
259
|
+
let bg;
|
|
260
|
+
let texturedBg = false;
|
|
261
|
+
if (v.backgroundAssetId) {
|
|
262
|
+
try {
|
|
263
|
+
const bgAsset = ctx.resolveAsset(v.backgroundAssetId);
|
|
264
|
+
const np = createImageOrNinePatch(ctx.scene, 0, 0, w, h, bgAsset);
|
|
265
|
+
np.setOrigin?.(0.5, 0.5);
|
|
266
|
+
bg = np;
|
|
267
|
+
texturedBg = true;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Background asset missing — fall through to colored-rect.
|
|
271
|
+
bg = makeColoredRectBg(ctx.scene, v, w, h, fillColor, strokeColor, strokeW);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
bg = makeColoredRectBg(ctx.scene, v, w, h, fillColor, strokeColor, strokeW);
|
|
276
|
+
}
|
|
201
277
|
container.add(bg);
|
|
202
278
|
if (v.iconAssetId) {
|
|
203
279
|
try {
|
|
@@ -230,19 +306,23 @@ function createIconButton(ctx, entity, pos) {
|
|
|
230
306
|
// code subscribes by entity id (or role) to wire its onPress action.
|
|
231
307
|
// The editor mode ignores this — input is captured by the EditorOverlay.
|
|
232
308
|
bg.setInteractive({ useHandCursor: true });
|
|
309
|
+
// pressedFillColor only applies to the colored-rect bg (Rectangle/Arc have
|
|
310
|
+
// setFillStyle). Textured backgrounds render the asset's pixels directly —
|
|
311
|
+
// pressed-state visual feedback for those is v2.
|
|
312
|
+
const supportsFillSwap = !texturedBg && !!v.pressedFillColor;
|
|
233
313
|
bg.on(Phaser.Input.Events.POINTER_DOWN, () => {
|
|
234
|
-
if (
|
|
314
|
+
if (supportsFillSwap) {
|
|
235
315
|
bg.setFillStyle?.(parseColor(v.pressedFillColor));
|
|
236
316
|
}
|
|
237
317
|
});
|
|
238
318
|
bg.on(Phaser.Input.Events.POINTER_UP, () => {
|
|
239
|
-
if (
|
|
319
|
+
if (supportsFillSwap) {
|
|
240
320
|
bg.setFillStyle?.(fillColor);
|
|
241
321
|
}
|
|
242
322
|
ctx.scene.events.emit('hud:press', entity.id, entity);
|
|
243
323
|
});
|
|
244
324
|
bg.on(Phaser.Input.Events.POINTER_OUT, () => {
|
|
245
|
-
if (
|
|
325
|
+
if (supportsFillSwap) {
|
|
246
326
|
bg.setFillStyle?.(fillColor);
|
|
247
327
|
}
|
|
248
328
|
});
|
|
@@ -351,35 +431,54 @@ function createPanel(ctx, entity, pos) {
|
|
|
351
431
|
const w = v.width ?? 200;
|
|
352
432
|
const h = v.height ?? 100;
|
|
353
433
|
const container = ctx.scene.add.container(pos.x, pos.y);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
else
|
|
367
|
-
g.fillRect(x0, y0, w, h);
|
|
434
|
+
// Textured-background path: when `backgroundAssetId` is set we render the
|
|
435
|
+
// asset image (NinePatch when it carries 9-slice metadata; stretched Image
|
|
436
|
+
// otherwise). The colored Graphics path below is the fallback when the
|
|
437
|
+
// asset is missing or no background asset is configured.
|
|
438
|
+
let bgRendered = false;
|
|
439
|
+
if (v.backgroundAssetId) {
|
|
440
|
+
try {
|
|
441
|
+
const bgAsset = ctx.resolveAsset(v.backgroundAssetId);
|
|
442
|
+
const np = createImageOrNinePatch(ctx.scene, 0, 0, w, h, bgAsset);
|
|
443
|
+
np.setOrigin?.(0.5, 0.5);
|
|
444
|
+
container.add(np);
|
|
445
|
+
bgRendered = true;
|
|
368
446
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (radius > 0)
|
|
372
|
-
g.strokeRoundedRect(x0, y0, w, h, radius);
|
|
373
|
-
else
|
|
374
|
-
g.strokeRect(x0, y0, w, h);
|
|
447
|
+
catch {
|
|
448
|
+
// Background asset missing — fall through to Graphics rect.
|
|
375
449
|
}
|
|
376
|
-
}
|
|
377
|
-
|
|
450
|
+
}
|
|
451
|
+
if (!bgRendered) {
|
|
452
|
+
const g = ctx.scene.add.graphics();
|
|
453
|
+
container.add(g);
|
|
454
|
+
const draw = () => {
|
|
455
|
+
g.clear();
|
|
456
|
+
const radius = v.borderRadius ?? 0;
|
|
457
|
+
// Centered draw — see createProgressBar for the rationale.
|
|
458
|
+
const x0 = -w / 2;
|
|
459
|
+
const y0 = -h / 2;
|
|
460
|
+
if (v.backgroundColor) {
|
|
461
|
+
g.fillStyle(parseColor(v.backgroundColor), v.backgroundAlpha ?? 1);
|
|
462
|
+
if (radius > 0)
|
|
463
|
+
g.fillRoundedRect(x0, y0, w, h, radius);
|
|
464
|
+
else
|
|
465
|
+
g.fillRect(x0, y0, w, h);
|
|
466
|
+
}
|
|
467
|
+
if (v.borderColor && (v.borderWidth ?? 0) > 0) {
|
|
468
|
+
g.lineStyle(v.borderWidth ?? 1, parseColor(v.borderColor), 1);
|
|
469
|
+
if (radius > 0)
|
|
470
|
+
g.strokeRoundedRect(x0, y0, w, h, radius);
|
|
471
|
+
else
|
|
472
|
+
g.strokeRect(x0, y0, w, h);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
draw();
|
|
476
|
+
container.setData('hudRedraw', draw);
|
|
477
|
+
}
|
|
378
478
|
container.setSize(w, h);
|
|
379
479
|
container.setData('editorHitWidth', w);
|
|
380
480
|
container.setData('editorHitHeight', h);
|
|
381
481
|
applyContainerOriginShift(container, entity.anchor.side, w, h);
|
|
382
|
-
container.setData('hudRedraw', draw);
|
|
383
482
|
return container;
|
|
384
483
|
}
|
|
385
484
|
/** Read a numeric source (static or dynamic from registry). */
|
|
@@ -528,10 +627,18 @@ export function preloadHudAssets(scene, hudScene, manifest) {
|
|
|
528
627
|
function collectHudAssetIds(entities) {
|
|
529
628
|
const ids = new Set();
|
|
530
629
|
for (const e of entities) {
|
|
531
|
-
if (e.kind === 'image')
|
|
630
|
+
if (e.kind === 'image') {
|
|
532
631
|
ids.add(e.visual.assetId);
|
|
533
|
-
|
|
534
|
-
|
|
632
|
+
}
|
|
633
|
+
else if (e.kind === 'icon-button') {
|
|
634
|
+
if (e.visual.iconAssetId)
|
|
635
|
+
ids.add(e.visual.iconAssetId);
|
|
636
|
+
if (e.visual.backgroundAssetId)
|
|
637
|
+
ids.add(e.visual.backgroundAssetId);
|
|
638
|
+
}
|
|
639
|
+
else if (e.kind === 'panel' && e.visual.backgroundAssetId) {
|
|
640
|
+
ids.add(e.visual.backgroundAssetId);
|
|
641
|
+
}
|
|
535
642
|
}
|
|
536
643
|
return Array.from(ids);
|
|
537
644
|
}
|
|
@@ -558,6 +665,11 @@ export async function loadHudScene(scene, hudId) {
|
|
|
558
665
|
}
|
|
559
666
|
preloadHudAssets(scene, hudScene, manifest);
|
|
560
667
|
await runLoader(scene);
|
|
668
|
+
// Mirrors loadWorldScene: NEAREST filter on every `pixelArt: true` asset.
|
|
669
|
+
// HUD scene runs in its own Phaser scene with its own texture references
|
|
670
|
+
// (textures are game-global so this is mostly idempotent with the world
|
|
671
|
+
// pass, but the HUD might load image assets the world doesn't reference).
|
|
672
|
+
applyPixelArtFilters(scene, manifest);
|
|
561
673
|
const registry = attachEntityRegistry(scene);
|
|
562
674
|
const safeArea = hudScene.design?.safeArea ?? DEFAULT_SAFE_AREA;
|
|
563
675
|
const ctx = {
|
|
@@ -783,23 +895,56 @@ export function applyHudPatch(game, entityId, patch) {
|
|
|
783
895
|
if (patch.visual && entity.kind === 'image') {
|
|
784
896
|
const v = entity.visual;
|
|
785
897
|
const p = patch.visual;
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
898
|
+
// 9-slice live objects are RenderTextures, not Images — setDisplaySize /
|
|
899
|
+
// setTint don't behave the same. Mirror the panel/icon-button approach:
|
|
900
|
+
// mutate the entity record + re-spawn in place. Drag-resize from the
|
|
901
|
+
// editor lands here; the rebuild keeps NinePatch geometry correct.
|
|
902
|
+
const manifest = getManifest(hud);
|
|
903
|
+
const asset = manifest.assets?.find((a) => a.id === v.assetId);
|
|
904
|
+
if (asset?.ninePatch) {
|
|
905
|
+
for (const [k, val] of Object.entries(p)) {
|
|
906
|
+
if (val !== undefined)
|
|
907
|
+
v[k] = val;
|
|
908
|
+
}
|
|
909
|
+
const oldDepth = go.depth;
|
|
910
|
+
go.destroy();
|
|
911
|
+
reg.unregister(entityId);
|
|
912
|
+
const ctx = {
|
|
913
|
+
scene: hud,
|
|
914
|
+
registry: reg,
|
|
915
|
+
safeArea,
|
|
916
|
+
resolveAsset: (id) => {
|
|
917
|
+
const m = getManifest(hud);
|
|
918
|
+
const a = m.assets?.find((x) => x.id === id);
|
|
919
|
+
if (!a)
|
|
920
|
+
throw new Error(`[unboxy/hud] manifest has no asset with id '${id}'`);
|
|
921
|
+
return a;
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
const fresh = spawnHudEntity(ctx, entity);
|
|
925
|
+
if (typeof oldDepth === 'number') {
|
|
926
|
+
fresh.setDepth?.(oldDepth);
|
|
927
|
+
}
|
|
798
928
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
929
|
+
else {
|
|
930
|
+
const image = go;
|
|
931
|
+
if (typeof p.tint === 'string') {
|
|
932
|
+
v.tint = p.tint;
|
|
933
|
+
image.setTint(parseColor(v.tint));
|
|
934
|
+
}
|
|
935
|
+
else if (p.tint === null) {
|
|
936
|
+
v.tint = undefined;
|
|
937
|
+
image.clearTint();
|
|
938
|
+
}
|
|
939
|
+
if (typeof p.alpha === 'number') {
|
|
940
|
+
v.alpha = p.alpha;
|
|
941
|
+
image.setAlpha(p.alpha);
|
|
942
|
+
}
|
|
943
|
+
if (typeof p.width === 'number' && typeof p.height === 'number') {
|
|
944
|
+
v.width = p.width;
|
|
945
|
+
v.height = p.height;
|
|
946
|
+
image.setDisplaySize(p.width, p.height);
|
|
947
|
+
}
|
|
803
948
|
}
|
|
804
949
|
}
|
|
805
950
|
if (patch.visual && (entity.kind === 'progress-bar' || entity.kind === 'panel')) {
|
|
@@ -847,6 +992,10 @@ export function applyHudPatch(game, entityId, patch) {
|
|
|
847
992
|
v.fillColor = p.fillColor;
|
|
848
993
|
if (typeof p.iconAssetId === 'string')
|
|
849
994
|
v.iconAssetId = p.iconAssetId;
|
|
995
|
+
if (typeof p.backgroundAssetId === 'string')
|
|
996
|
+
v.backgroundAssetId = p.backgroundAssetId;
|
|
997
|
+
else if (p.backgroundAssetId === null)
|
|
998
|
+
v.backgroundAssetId = undefined;
|
|
850
999
|
// Patches that change the rendered visual deeply (label, colour, icon)
|
|
851
1000
|
// re-render by destroying + re-spawning the container in place.
|
|
852
1001
|
const reg2 = reg;
|
|
@@ -33,6 +33,33 @@ export declare function getManifest(scene: Phaser.Scene): Manifest;
|
|
|
33
33
|
* scene that uses it is free.
|
|
34
34
|
*/
|
|
35
35
|
export declare function preloadSceneAssets(scene: Phaser.Scene, sceneFile: SceneFile, manifest: Manifest): void;
|
|
36
|
+
/**
|
|
37
|
+
* Walk every `kind=spritesheet` asset in the manifest and register each one's
|
|
38
|
+
* `animations[]` as a Phaser animation, so behavior code can call
|
|
39
|
+
* `sprite.play('walk-down')` directly. Idempotent — uses `scene.anims.exists()`
|
|
40
|
+
* to skip duplicates (Phaser's anims.create throws otherwise).
|
|
41
|
+
*
|
|
42
|
+
* <p>Animation keys are scene-global. Two sheets registering the same name
|
|
43
|
+
* (e.g. two characters both with `walk-down`) → first wins, second logs a
|
|
44
|
+
* warning. Sheets that need disambiguation should namespace their keys
|
|
45
|
+
* (e.g. `cow:walk-down`) at metadata time.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Walk every asset in the manifest and apply `Phaser.Textures.FilterMode.NEAREST`
|
|
49
|
+
* to each `pixelArt: true` entry's loaded texture. Without this, pixel-art
|
|
50
|
+
* sprites render bilinear-blurred — visible immediately on imported character
|
|
51
|
+
* sheets when the agent hasn't manually set Phaser game config's `pixelArt`.
|
|
52
|
+
*
|
|
53
|
+
* <p>Idempotent: setFilter is safe to call multiple times. Skip silently when
|
|
54
|
+
* a texture isn't loaded yet (next call after that asset is requested will
|
|
55
|
+
* pick it up). Wrap in try/catch so a single bad asset doesn't block the rest
|
|
56
|
+
* of scene init.
|
|
57
|
+
*
|
|
58
|
+
* <p>Per-asset NEAREST closes the visible blur gap; the game-wide flag
|
|
59
|
+
* (`Phaser.Game.config.pixelArt` + `camera.setRoundPixels`) is a separate
|
|
60
|
+
* concern (framebuffer-stretch crispness, perf) deferred to a follow-up.
|
|
61
|
+
*/
|
|
62
|
+
export declare function applyPixelArtFilters(scene: Phaser.Scene, manifest: Manifest): void;
|
|
36
63
|
export interface LoadWorldSceneOptions {
|
|
37
64
|
/**
|
|
38
65
|
* Optional render-script resolver for `code-rendered` entities. When
|
|
@@ -126,6 +126,37 @@ function collectAssetIds(entities) {
|
|
|
126
126
|
* warning. Sheets that need disambiguation should namespace their keys
|
|
127
127
|
* (e.g. `cow:walk-down`) at metadata time.
|
|
128
128
|
*/
|
|
129
|
+
/**
|
|
130
|
+
* Walk every asset in the manifest and apply `Phaser.Textures.FilterMode.NEAREST`
|
|
131
|
+
* to each `pixelArt: true` entry's loaded texture. Without this, pixel-art
|
|
132
|
+
* sprites render bilinear-blurred — visible immediately on imported character
|
|
133
|
+
* sheets when the agent hasn't manually set Phaser game config's `pixelArt`.
|
|
134
|
+
*
|
|
135
|
+
* <p>Idempotent: setFilter is safe to call multiple times. Skip silently when
|
|
136
|
+
* a texture isn't loaded yet (next call after that asset is requested will
|
|
137
|
+
* pick it up). Wrap in try/catch so a single bad asset doesn't block the rest
|
|
138
|
+
* of scene init.
|
|
139
|
+
*
|
|
140
|
+
* <p>Per-asset NEAREST closes the visible blur gap; the game-wide flag
|
|
141
|
+
* (`Phaser.Game.config.pixelArt` + `camera.setRoundPixels`) is a separate
|
|
142
|
+
* concern (framebuffer-stretch crispness, perf) deferred to a follow-up.
|
|
143
|
+
*/
|
|
144
|
+
export function applyPixelArtFilters(scene, manifest) {
|
|
145
|
+
for (const asset of manifest.assets ?? []) {
|
|
146
|
+
if (!asset.pixelArt)
|
|
147
|
+
continue;
|
|
148
|
+
if (!scene.textures.exists(asset.textureKey))
|
|
149
|
+
continue;
|
|
150
|
+
try {
|
|
151
|
+
const tex = scene.textures.get(asset.textureKey);
|
|
152
|
+
tex.setFilter(Phaser.Textures.FilterMode.NEAREST);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.warn(`[unboxy/scene] failed to apply NEAREST filter to '${asset.id}': ${String(e)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
129
160
|
function registerSheetAnimations(scene, manifest) {
|
|
130
161
|
const assets = manifest.assets ?? [];
|
|
131
162
|
for (const asset of assets) {
|
|
@@ -172,12 +203,29 @@ function queueAssetLoad(scene, asset) {
|
|
|
172
203
|
case 'image':
|
|
173
204
|
scene.load.image(asset.textureKey, asset.path);
|
|
174
205
|
return;
|
|
175
|
-
case 'spritesheet':
|
|
176
|
-
|
|
177
|
-
|
|
206
|
+
case 'spritesheet': {
|
|
207
|
+
// Tolerant config resolution: prefer nested `spriteSheetConfig`, fall
|
|
208
|
+
// back to top-level `frameWidth/frameHeight/margin/spacing` if the
|
|
209
|
+
// agent put them there (common typo — both shapes appear in real
|
|
210
|
+
// workspaces). Soft-fail with warning when neither shape provides
|
|
211
|
+
// frame dims so the rest of the scene still boots.
|
|
212
|
+
const cfg = asset.spriteSheetConfig
|
|
213
|
+
?? (asset.frameWidth && asset.frameHeight
|
|
214
|
+
? {
|
|
215
|
+
frameWidth: asset.frameWidth,
|
|
216
|
+
frameHeight: asset.frameHeight,
|
|
217
|
+
margin: asset.margin,
|
|
218
|
+
spacing: asset.spacing,
|
|
219
|
+
}
|
|
220
|
+
: null);
|
|
221
|
+
if (!cfg) {
|
|
222
|
+
// eslint-disable-next-line no-console
|
|
223
|
+
console.warn(`[unboxy/scene] asset '${asset.id}' kind=spritesheet missing spriteSheetConfig (and no top-level frameWidth/frameHeight); skipping load`);
|
|
224
|
+
return;
|
|
178
225
|
}
|
|
179
|
-
scene.load.spritesheet(asset.textureKey, asset.path,
|
|
226
|
+
scene.load.spritesheet(asset.textureKey, asset.path, cfg);
|
|
180
227
|
return;
|
|
228
|
+
}
|
|
181
229
|
case 'atlas':
|
|
182
230
|
if (!asset.atlasPath || !asset.atlasFormat) {
|
|
183
231
|
throw new Error(`[unboxy/scene] asset '${asset.id}' kind=atlas missing atlasPath/atlasFormat`);
|
|
@@ -232,6 +280,9 @@ export async function loadWorldScene(scene, sceneId, options = {}) {
|
|
|
232
280
|
// Lazy preload of any new assets this scene needs, then await loader.
|
|
233
281
|
preloadSceneAssets(scene, sceneFile, manifest);
|
|
234
282
|
await runLoader(scene);
|
|
283
|
+
// Per-asset NEAREST filter for `pixelArt: true` entries. Must run after
|
|
284
|
+
// textures are loaded — setFilter on a missing texture is a no-op.
|
|
285
|
+
applyPixelArtFilters(scene, manifest);
|
|
235
286
|
// Register Phaser animations declared in `manifest.assets[].animations`.
|
|
236
287
|
// Done after texture load + before entity spawn so behavior code can call
|
|
237
288
|
// `sprite.play('walk-down')` from frame 1 without manual `anims.create()`.
|
package/dist/scene/types.d.ts
CHANGED
|
@@ -42,6 +42,15 @@ export interface AssetRecord {
|
|
|
42
42
|
margin?: number;
|
|
43
43
|
spacing?: number;
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Top-level convenience fields. When the agent writes a spritesheet asset
|
|
47
|
+
* entry, it commonly puts these at the top level instead of nested in
|
|
48
|
+
* `spriteSheetConfig` — both shapes are now accepted by the loader.
|
|
49
|
+
*/
|
|
50
|
+
frameWidth?: number;
|
|
51
|
+
frameHeight?: number;
|
|
52
|
+
margin?: number;
|
|
53
|
+
spacing?: number;
|
|
45
54
|
/** For `atlas`: companion atlas file path (xml or json). */
|
|
46
55
|
atlasPath?: string;
|
|
47
56
|
/** `'xml'` (Starling) or `'json'` (TexturePacker). */
|
|
@@ -63,6 +72,34 @@ export interface AssetRecord {
|
|
|
63
72
|
animations?: SheetAnimation[];
|
|
64
73
|
/** Default playback rate for `animations[]`. Defaults to 8. */
|
|
65
74
|
fps?: number;
|
|
75
|
+
/**
|
|
76
|
+
* For `image`: 9-slice cut-line metadata. When present, HUD widgets that
|
|
77
|
+
* render this asset (image / icon-button background / panel background)
|
|
78
|
+
* scale via `phaser3-rex-plugins` NinePatch instead of plain stretched
|
|
79
|
+
* Image — corners stay fixed, edges + center stretch. Pack-import vision
|
|
80
|
+
* captures these on UI button/panel imports; users can edit in the Adjust
|
|
81
|
+
* modal at import time.
|
|
82
|
+
*
|
|
83
|
+
* <p>Single-image only in v1 (slice 7.5). Per-frame 9-slice on uniform-grid
|
|
84
|
+
* sheets (e.g. `Square Buttons 26x26.png`) is v2 — see
|
|
85
|
+
* `unboxy-design/features/visual-editor/07-asset-pack-import.md` §7.9.
|
|
86
|
+
*/
|
|
87
|
+
ninePatch?: NinePatchConfig;
|
|
88
|
+
/**
|
|
89
|
+
* Vision-detected pixel-art flag. When true, SDK applies
|
|
90
|
+
* `Phaser.Textures.FilterMode.NEAREST` to the texture after load so it
|
|
91
|
+
* renders crisp instead of bilinear-blurred. Population is per-source:
|
|
92
|
+
* pack-import sets it via vision; AI generation / manual upload / library
|
|
93
|
+
* backfill close their own gaps separately (see design doc 07 §8.4).
|
|
94
|
+
*/
|
|
95
|
+
pixelArt?: boolean;
|
|
96
|
+
}
|
|
97
|
+
/** Single-image 9-slice config. Pixels from each edge to the slice line. */
|
|
98
|
+
export interface NinePatchConfig {
|
|
99
|
+
leftWidth: number;
|
|
100
|
+
rightWidth: number;
|
|
101
|
+
topHeight: number;
|
|
102
|
+
bottomHeight: number;
|
|
66
103
|
}
|
|
67
104
|
export interface SheetAnimation {
|
|
68
105
|
/** Phaser animation key — used as `sprite.play('<name>')`. */
|
|
@@ -336,7 +373,16 @@ export interface HudIconButtonVisual {
|
|
|
336
373
|
label?: string;
|
|
337
374
|
/** Optional icon asset shown inside the button. */
|
|
338
375
|
iconAssetId?: string;
|
|
339
|
-
/**
|
|
376
|
+
/**
|
|
377
|
+
* Optional image asset rendered as the button background. When the
|
|
378
|
+
* referenced asset has `ninePatch` metadata, the SDK renders it via
|
|
379
|
+
* `phaser3-rex-plugins` NinePatch so corners stay fixed at any width/height;
|
|
380
|
+
* otherwise the image is stretched to the widget's size. When set, the
|
|
381
|
+
* colored-rect bg below (fillColor / strokeColor / shape) is ignored, and
|
|
382
|
+
* `pressedFillColor` no-ops (no fill swap on a textured bg in v1).
|
|
383
|
+
*/
|
|
384
|
+
backgroundAssetId?: string;
|
|
385
|
+
/** Button shape. Default `rounded-rect`. Ignored when backgroundAssetId is set. */
|
|
340
386
|
shape?: 'rounded-rect' | 'circle';
|
|
341
387
|
width?: number;
|
|
342
388
|
height?: number;
|
|
@@ -380,6 +426,15 @@ export interface HudPanelVisual {
|
|
|
380
426
|
kind: 'panel';
|
|
381
427
|
width?: number;
|
|
382
428
|
height?: number;
|
|
429
|
+
/**
|
|
430
|
+
* Optional image asset rendered as the panel background. When the
|
|
431
|
+
* referenced asset has `ninePatch` metadata, the SDK renders it via
|
|
432
|
+
* `phaser3-rex-plugins` NinePatch so corners stay fixed at any width/height;
|
|
433
|
+
* otherwise the image is stretched to the widget's size. When set,
|
|
434
|
+
* `backgroundColor` / `backgroundAlpha` / `borderColor` / `borderWidth` are
|
|
435
|
+
* ignored — the asset image carries its own pixels.
|
|
436
|
+
*/
|
|
437
|
+
backgroundAssetId?: string;
|
|
383
438
|
/** Solid fill colour. Falls back to a transparent panel if omitted. */
|
|
384
439
|
backgroundColor?: string;
|
|
385
440
|
/** Background alpha (independent of widget-level alpha). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unboxy/phaser-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.37",
|
|
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",
|
|
@@ -18,11 +18,13 @@
|
|
|
18
18
|
"colyseus.js": "^0.16.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"phaser": "^3.60.0"
|
|
21
|
+
"phaser": "^3.60.0",
|
|
22
|
+
"phaser3-rex-plugins": "^1.80.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"jose": "^6.2.2",
|
|
25
26
|
"phaser": "^3.80.0",
|
|
27
|
+
"phaser3-rex-plugins": "^1.80.20",
|
|
26
28
|
"typescript": "^5.5.0"
|
|
27
29
|
},
|
|
28
30
|
"publishConfig": {
|