@unboxy/phaser-sdk 0.2.36 → 0.2.38
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 +3 -3
- package/dist/index.js +2 -2
- package/dist/scene/HudRuntime.js +238 -58
- package/dist/scene/SceneLoader.d.ts +27 -0
- package/dist/scene/SceneLoader.js +34 -0
- package/dist/scene/types.d.ts +86 -1
- package/dist/scene/types.js +7 -0
- 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';
|
|
@@ -27,6 +27,6 @@ export type { RenderScriptModule } from './scene/renderScripts.js';
|
|
|
27
27
|
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
|
-
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';
|
|
30
|
+
export { SCHEMA_VERSION, isPerFrameNinePatch, } 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, NinePatchPerFrame, } 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,12 +14,12 @@ 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';
|
|
21
21
|
export { setRenderScriptRegistry, getRenderScriptRegistry, resolveRenderScript, } from './scene/renderScripts.js';
|
|
22
22
|
export { setupEditorBridge } from './editor/EditorBridge.js';
|
|
23
23
|
export { EditorOverlayScene, EDITOR_OVERLAY_KEY } from './editor/EditorOverlayScene.js';
|
|
24
|
-
export { SCHEMA_VERSION, } from './scene/types.js';
|
|
24
|
+
export { SCHEMA_VERSION, isPerFrameNinePatch, } from './scene/types.js';
|
|
25
25
|
export { PROTOCOL_VERSION, } from './protocol.js';
|
package/dist/scene/HudRuntime.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import Phaser from 'phaser';
|
|
2
|
-
import { SCHEMA_VERSION, } from './types.js';
|
|
2
|
+
import { SCHEMA_VERSION, isPerFrameNinePatch, } 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,61 @@ const LAYER_DEPTH = {
|
|
|
79
79
|
overlay: 200,
|
|
80
80
|
modal: 300,
|
|
81
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* Render an image (or one cell of a uniform-grid spritesheet) at (x, y) sized
|
|
84
|
+
* to (width, height). When the asset carries `ninePatch` metadata, uses
|
|
85
|
+
* `phaser3-rex-plugins`' NinePatch (corners fixed, edges + center stretch);
|
|
86
|
+
* otherwise falls back to a regular Image scaled with `setDisplaySize`.
|
|
87
|
+
*
|
|
88
|
+
* Two ninePatch shapes accepted:
|
|
89
|
+
* - Single image (slice 7.5): `asset.ninePatch` is a `NinePatchConfig`. The
|
|
90
|
+
* whole texture slices into 9 regions.
|
|
91
|
+
* - Per-frame on a uniform-grid sheet (slice 7.6): `asset.ninePatch` is
|
|
92
|
+
* `{ perFrame: NinePatchConfig }`. The shared cuts apply to whichever
|
|
93
|
+
* frame the caller picks via `frame`. Common case: itch.io button packs
|
|
94
|
+
* where every cell is a different button color/state with the same
|
|
95
|
+
* corner radius.
|
|
96
|
+
*
|
|
97
|
+
* Caller owns origin/anchor — both Image and NinePatch (which extends
|
|
98
|
+
* RenderTexture) expose `setOrigin`. Returns the created GameObject already
|
|
99
|
+
* added to the scene's display list.
|
|
100
|
+
*/
|
|
101
|
+
function createImageOrNinePatch(scene, x, y, width, height, asset, frame) {
|
|
102
|
+
if (asset.ninePatch) {
|
|
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}'`);
|
|
128
|
+
}
|
|
129
|
+
// Fallback: plain Image. Frame index threaded through for spritesheet
|
|
130
|
+
// assets so per-cell rendering still works without 9-slice.
|
|
131
|
+
const image = frame !== undefined
|
|
132
|
+
? scene.add.image(x, y, asset.textureKey, frame)
|
|
133
|
+
: scene.add.image(x, y, asset.textureKey);
|
|
134
|
+
image.setDisplaySize(width, height);
|
|
135
|
+
return image;
|
|
136
|
+
}
|
|
82
137
|
/**
|
|
83
138
|
* Spawn a HUD widget into the scene. The returned GameObject is recorded in
|
|
84
139
|
* the entity registry so the editor + behavior code can find it by id.
|
|
@@ -161,6 +216,23 @@ function createImage(ctx, entity, pos) {
|
|
|
161
216
|
container.setSize(w, h);
|
|
162
217
|
return container;
|
|
163
218
|
}
|
|
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.
|
|
223
|
+
if (asset.ninePatch) {
|
|
224
|
+
const tex = ctx.scene.textures.get(asset.textureKey);
|
|
225
|
+
const src = tex?.getSourceImage();
|
|
226
|
+
const w = entity.visual.width ?? src?.width ?? 64;
|
|
227
|
+
const h = entity.visual.height ?? src?.height ?? 64;
|
|
228
|
+
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);
|
|
234
|
+
return np;
|
|
235
|
+
}
|
|
164
236
|
const image = entity.visual.frame !== undefined
|
|
165
237
|
? ctx.scene.add.image(pos.x, pos.y, asset.textureKey, entity.visual.frame)
|
|
166
238
|
: ctx.scene.add.image(pos.x, pos.y, asset.textureKey);
|
|
@@ -174,19 +246,16 @@ function createImage(ctx, entity, pos) {
|
|
|
174
246
|
applyOriginFromAnchor(image, entity.anchor.side);
|
|
175
247
|
return image;
|
|
176
248
|
}
|
|
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);
|
|
249
|
+
/**
|
|
250
|
+
* Build the v0 colored-rect background for an icon-button (rectangle or
|
|
251
|
+
* circle). Used when `backgroundAssetId` is unset OR the referenced asset
|
|
252
|
+
* couldn't be resolved.
|
|
253
|
+
*/
|
|
254
|
+
function makeColoredRectBg(scene, v, w, h, fillColor, strokeColor, strokeW) {
|
|
186
255
|
const bg = v.shape === 'circle'
|
|
187
|
-
?
|
|
256
|
+
? scene.add.circle(0, 0, Math.max(w, h) / 2, fillColor)
|
|
188
257
|
: (() => {
|
|
189
|
-
const r =
|
|
258
|
+
const r = scene.add.rectangle(0, 0, w, h, fillColor);
|
|
190
259
|
r.setOrigin(0.5, 0.5);
|
|
191
260
|
return r;
|
|
192
261
|
})();
|
|
@@ -198,6 +267,40 @@ function createIconButton(ctx, entity, pos) {
|
|
|
198
267
|
bg.setStrokeStyle(strokeW, strokeColor);
|
|
199
268
|
}
|
|
200
269
|
}
|
|
270
|
+
return bg;
|
|
271
|
+
}
|
|
272
|
+
function createIconButton(ctx, entity, pos) {
|
|
273
|
+
const v = entity.visual;
|
|
274
|
+
const w = v.width ?? 96;
|
|
275
|
+
const h = v.height ?? 48;
|
|
276
|
+
const fillColor = parseColor(v.fillColor ?? '#3b82f6');
|
|
277
|
+
const strokeColor = v.strokeColor === null ? null : parseColor(v.strokeColor ?? '#1e40af');
|
|
278
|
+
const strokeW = v.strokeWidth ?? 0;
|
|
279
|
+
// Container at anchor pos. Children drawn with their own origins.
|
|
280
|
+
const container = ctx.scene.add.container(pos.x, pos.y);
|
|
281
|
+
// Background source (textured vs. colored-rect). When `backgroundAssetId`
|
|
282
|
+
// is set we render the asset image (NinePatch when the asset has 9-slice
|
|
283
|
+
// metadata; stretched Image otherwise); colored-rect bg is the v0 fallback.
|
|
284
|
+
// The interactive surface is always the `bg` GameObject — POINTER_DOWN/UP
|
|
285
|
+
// events fire on whichever variant is active.
|
|
286
|
+
let bg;
|
|
287
|
+
let texturedBg = false;
|
|
288
|
+
if (v.backgroundAssetId) {
|
|
289
|
+
try {
|
|
290
|
+
const bgAsset = ctx.resolveAsset(v.backgroundAssetId);
|
|
291
|
+
const np = createImageOrNinePatch(ctx.scene, 0, 0, w, h, bgAsset, v.backgroundFrame);
|
|
292
|
+
np.setOrigin?.(0.5, 0.5);
|
|
293
|
+
bg = np;
|
|
294
|
+
texturedBg = true;
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// Background asset missing — fall through to colored-rect.
|
|
298
|
+
bg = makeColoredRectBg(ctx.scene, v, w, h, fillColor, strokeColor, strokeW);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
bg = makeColoredRectBg(ctx.scene, v, w, h, fillColor, strokeColor, strokeW);
|
|
303
|
+
}
|
|
201
304
|
container.add(bg);
|
|
202
305
|
if (v.iconAssetId) {
|
|
203
306
|
try {
|
|
@@ -230,19 +333,23 @@ function createIconButton(ctx, entity, pos) {
|
|
|
230
333
|
// code subscribes by entity id (or role) to wire its onPress action.
|
|
231
334
|
// The editor mode ignores this — input is captured by the EditorOverlay.
|
|
232
335
|
bg.setInteractive({ useHandCursor: true });
|
|
336
|
+
// pressedFillColor only applies to the colored-rect bg (Rectangle/Arc have
|
|
337
|
+
// setFillStyle). Textured backgrounds render the asset's pixels directly —
|
|
338
|
+
// pressed-state visual feedback for those is v2.
|
|
339
|
+
const supportsFillSwap = !texturedBg && !!v.pressedFillColor;
|
|
233
340
|
bg.on(Phaser.Input.Events.POINTER_DOWN, () => {
|
|
234
|
-
if (
|
|
341
|
+
if (supportsFillSwap) {
|
|
235
342
|
bg.setFillStyle?.(parseColor(v.pressedFillColor));
|
|
236
343
|
}
|
|
237
344
|
});
|
|
238
345
|
bg.on(Phaser.Input.Events.POINTER_UP, () => {
|
|
239
|
-
if (
|
|
346
|
+
if (supportsFillSwap) {
|
|
240
347
|
bg.setFillStyle?.(fillColor);
|
|
241
348
|
}
|
|
242
349
|
ctx.scene.events.emit('hud:press', entity.id, entity);
|
|
243
350
|
});
|
|
244
351
|
bg.on(Phaser.Input.Events.POINTER_OUT, () => {
|
|
245
|
-
if (
|
|
352
|
+
if (supportsFillSwap) {
|
|
246
353
|
bg.setFillStyle?.(fillColor);
|
|
247
354
|
}
|
|
248
355
|
});
|
|
@@ -351,35 +458,54 @@ function createPanel(ctx, entity, pos) {
|
|
|
351
458
|
const w = v.width ?? 200;
|
|
352
459
|
const h = v.height ?? 100;
|
|
353
460
|
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);
|
|
461
|
+
// Textured-background path: when `backgroundAssetId` is set we render the
|
|
462
|
+
// asset image (NinePatch when it carries 9-slice metadata; stretched Image
|
|
463
|
+
// otherwise). The colored Graphics path below is the fallback when the
|
|
464
|
+
// asset is missing or no background asset is configured.
|
|
465
|
+
let bgRendered = false;
|
|
466
|
+
if (v.backgroundAssetId) {
|
|
467
|
+
try {
|
|
468
|
+
const bgAsset = ctx.resolveAsset(v.backgroundAssetId);
|
|
469
|
+
const np = createImageOrNinePatch(ctx.scene, 0, 0, w, h, bgAsset, v.backgroundFrame);
|
|
470
|
+
np.setOrigin?.(0.5, 0.5);
|
|
471
|
+
container.add(np);
|
|
472
|
+
bgRendered = true;
|
|
368
473
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (radius > 0)
|
|
372
|
-
g.strokeRoundedRect(x0, y0, w, h, radius);
|
|
373
|
-
else
|
|
374
|
-
g.strokeRect(x0, y0, w, h);
|
|
474
|
+
catch {
|
|
475
|
+
// Background asset missing — fall through to Graphics rect.
|
|
375
476
|
}
|
|
376
|
-
}
|
|
377
|
-
|
|
477
|
+
}
|
|
478
|
+
if (!bgRendered) {
|
|
479
|
+
const g = ctx.scene.add.graphics();
|
|
480
|
+
container.add(g);
|
|
481
|
+
const draw = () => {
|
|
482
|
+
g.clear();
|
|
483
|
+
const radius = v.borderRadius ?? 0;
|
|
484
|
+
// Centered draw — see createProgressBar for the rationale.
|
|
485
|
+
const x0 = -w / 2;
|
|
486
|
+
const y0 = -h / 2;
|
|
487
|
+
if (v.backgroundColor) {
|
|
488
|
+
g.fillStyle(parseColor(v.backgroundColor), v.backgroundAlpha ?? 1);
|
|
489
|
+
if (radius > 0)
|
|
490
|
+
g.fillRoundedRect(x0, y0, w, h, radius);
|
|
491
|
+
else
|
|
492
|
+
g.fillRect(x0, y0, w, h);
|
|
493
|
+
}
|
|
494
|
+
if (v.borderColor && (v.borderWidth ?? 0) > 0) {
|
|
495
|
+
g.lineStyle(v.borderWidth ?? 1, parseColor(v.borderColor), 1);
|
|
496
|
+
if (radius > 0)
|
|
497
|
+
g.strokeRoundedRect(x0, y0, w, h, radius);
|
|
498
|
+
else
|
|
499
|
+
g.strokeRect(x0, y0, w, h);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
draw();
|
|
503
|
+
container.setData('hudRedraw', draw);
|
|
504
|
+
}
|
|
378
505
|
container.setSize(w, h);
|
|
379
506
|
container.setData('editorHitWidth', w);
|
|
380
507
|
container.setData('editorHitHeight', h);
|
|
381
508
|
applyContainerOriginShift(container, entity.anchor.side, w, h);
|
|
382
|
-
container.setData('hudRedraw', draw);
|
|
383
509
|
return container;
|
|
384
510
|
}
|
|
385
511
|
/** Read a numeric source (static or dynamic from registry). */
|
|
@@ -528,10 +654,18 @@ export function preloadHudAssets(scene, hudScene, manifest) {
|
|
|
528
654
|
function collectHudAssetIds(entities) {
|
|
529
655
|
const ids = new Set();
|
|
530
656
|
for (const e of entities) {
|
|
531
|
-
if (e.kind === 'image')
|
|
657
|
+
if (e.kind === 'image') {
|
|
532
658
|
ids.add(e.visual.assetId);
|
|
533
|
-
|
|
534
|
-
|
|
659
|
+
}
|
|
660
|
+
else if (e.kind === 'icon-button') {
|
|
661
|
+
if (e.visual.iconAssetId)
|
|
662
|
+
ids.add(e.visual.iconAssetId);
|
|
663
|
+
if (e.visual.backgroundAssetId)
|
|
664
|
+
ids.add(e.visual.backgroundAssetId);
|
|
665
|
+
}
|
|
666
|
+
else if (e.kind === 'panel' && e.visual.backgroundAssetId) {
|
|
667
|
+
ids.add(e.visual.backgroundAssetId);
|
|
668
|
+
}
|
|
535
669
|
}
|
|
536
670
|
return Array.from(ids);
|
|
537
671
|
}
|
|
@@ -558,6 +692,11 @@ export async function loadHudScene(scene, hudId) {
|
|
|
558
692
|
}
|
|
559
693
|
preloadHudAssets(scene, hudScene, manifest);
|
|
560
694
|
await runLoader(scene);
|
|
695
|
+
// Mirrors loadWorldScene: NEAREST filter on every `pixelArt: true` asset.
|
|
696
|
+
// HUD scene runs in its own Phaser scene with its own texture references
|
|
697
|
+
// (textures are game-global so this is mostly idempotent with the world
|
|
698
|
+
// pass, but the HUD might load image assets the world doesn't reference).
|
|
699
|
+
applyPixelArtFilters(scene, manifest);
|
|
561
700
|
const registry = attachEntityRegistry(scene);
|
|
562
701
|
const safeArea = hudScene.design?.safeArea ?? DEFAULT_SAFE_AREA;
|
|
563
702
|
const ctx = {
|
|
@@ -783,23 +922,56 @@ export function applyHudPatch(game, entityId, patch) {
|
|
|
783
922
|
if (patch.visual && entity.kind === 'image') {
|
|
784
923
|
const v = entity.visual;
|
|
785
924
|
const p = patch.visual;
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
925
|
+
// 9-slice live objects are RenderTextures, not Images — setDisplaySize /
|
|
926
|
+
// setTint don't behave the same. Mirror the panel/icon-button approach:
|
|
927
|
+
// mutate the entity record + re-spawn in place. Drag-resize from the
|
|
928
|
+
// editor lands here; the rebuild keeps NinePatch geometry correct.
|
|
929
|
+
const manifest = getManifest(hud);
|
|
930
|
+
const asset = manifest.assets?.find((a) => a.id === v.assetId);
|
|
931
|
+
if (asset?.ninePatch) {
|
|
932
|
+
for (const [k, val] of Object.entries(p)) {
|
|
933
|
+
if (val !== undefined)
|
|
934
|
+
v[k] = val;
|
|
935
|
+
}
|
|
936
|
+
const oldDepth = go.depth;
|
|
937
|
+
go.destroy();
|
|
938
|
+
reg.unregister(entityId);
|
|
939
|
+
const ctx = {
|
|
940
|
+
scene: hud,
|
|
941
|
+
registry: reg,
|
|
942
|
+
safeArea,
|
|
943
|
+
resolveAsset: (id) => {
|
|
944
|
+
const m = getManifest(hud);
|
|
945
|
+
const a = m.assets?.find((x) => x.id === id);
|
|
946
|
+
if (!a)
|
|
947
|
+
throw new Error(`[unboxy/hud] manifest has no asset with id '${id}'`);
|
|
948
|
+
return a;
|
|
949
|
+
},
|
|
950
|
+
};
|
|
951
|
+
const fresh = spawnHudEntity(ctx, entity);
|
|
952
|
+
if (typeof oldDepth === 'number') {
|
|
953
|
+
fresh.setDepth?.(oldDepth);
|
|
954
|
+
}
|
|
798
955
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
956
|
+
else {
|
|
957
|
+
const image = go;
|
|
958
|
+
if (typeof p.tint === 'string') {
|
|
959
|
+
v.tint = p.tint;
|
|
960
|
+
image.setTint(parseColor(v.tint));
|
|
961
|
+
}
|
|
962
|
+
else if (p.tint === null) {
|
|
963
|
+
v.tint = undefined;
|
|
964
|
+
image.clearTint();
|
|
965
|
+
}
|
|
966
|
+
if (typeof p.alpha === 'number') {
|
|
967
|
+
v.alpha = p.alpha;
|
|
968
|
+
image.setAlpha(p.alpha);
|
|
969
|
+
}
|
|
970
|
+
if (typeof p.width === 'number' && typeof p.height === 'number') {
|
|
971
|
+
v.width = p.width;
|
|
972
|
+
v.height = p.height;
|
|
973
|
+
image.setDisplaySize(p.width, p.height);
|
|
974
|
+
}
|
|
803
975
|
}
|
|
804
976
|
}
|
|
805
977
|
if (patch.visual && (entity.kind === 'progress-bar' || entity.kind === 'panel')) {
|
|
@@ -847,6 +1019,14 @@ export function applyHudPatch(game, entityId, patch) {
|
|
|
847
1019
|
v.fillColor = p.fillColor;
|
|
848
1020
|
if (typeof p.iconAssetId === 'string')
|
|
849
1021
|
v.iconAssetId = p.iconAssetId;
|
|
1022
|
+
if (typeof p.backgroundAssetId === 'string')
|
|
1023
|
+
v.backgroundAssetId = p.backgroundAssetId;
|
|
1024
|
+
else if (p.backgroundAssetId === null)
|
|
1025
|
+
v.backgroundAssetId = undefined;
|
|
1026
|
+
if (typeof p.backgroundFrame === 'number')
|
|
1027
|
+
v.backgroundFrame = p.backgroundFrame;
|
|
1028
|
+
else if (p.backgroundFrame === null)
|
|
1029
|
+
v.backgroundFrame = undefined;
|
|
850
1030
|
// Patches that change the rendered visual deeply (label, colour, icon)
|
|
851
1031
|
// re-render by destroying + re-spawning the container in place.
|
|
852
1032
|
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) {
|
|
@@ -249,6 +280,9 @@ export async function loadWorldScene(scene, sceneId, options = {}) {
|
|
|
249
280
|
// Lazy preload of any new assets this scene needs, then await loader.
|
|
250
281
|
preloadSceneAssets(scene, sceneFile, manifest);
|
|
251
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);
|
|
252
286
|
// Register Phaser animations declared in `manifest.assets[].animations`.
|
|
253
287
|
// Done after texture load + before entity spawn so behavior code can call
|
|
254
288
|
// `sprite.play('walk-down')` from frame 1 without manual `anims.create()`.
|
package/dist/scene/types.d.ts
CHANGED
|
@@ -72,7 +72,53 @@ export interface AssetRecord {
|
|
|
72
72
|
animations?: SheetAnimation[];
|
|
73
73
|
/** Default playback rate for `animations[]`. Defaults to 8. */
|
|
74
74
|
fps?: number;
|
|
75
|
+
/**
|
|
76
|
+
* 9-slice cut-line metadata. Two shapes accepted:
|
|
77
|
+
*
|
|
78
|
+
* - **Single image** (slice 7.5) — `NinePatchConfig` directly. Used on
|
|
79
|
+
* `kind: 'image'` assets like a single button/panel background. All four
|
|
80
|
+
* corner widths apply to the image as a whole.
|
|
81
|
+
* - **Per-frame on a uniform grid** (slice 7.6) — `{ perFrame: NinePatchConfig }`.
|
|
82
|
+
* Used on `kind: 'spritesheet'` assets where every cell is its own
|
|
83
|
+
* stretchable button/panel sharing the same corner radius (typical
|
|
84
|
+
* itch.io UI button packs, e.g. `Square Buttons 26x26.png`). The HUD
|
|
85
|
+
* widget references which cell to render via `backgroundFrame`.
|
|
86
|
+
*
|
|
87
|
+
* Per-cell keyed config (`{ perFrame: Record<number, NinePatchConfig> }`)
|
|
88
|
+
* is deferred to v3 per design 07 §7.9.5 — promote only when a real pack
|
|
89
|
+
* needs different corner radii on different cells.
|
|
90
|
+
*/
|
|
91
|
+
ninePatch?: NinePatchConfig | NinePatchPerFrame;
|
|
92
|
+
/**
|
|
93
|
+
* Vision-detected pixel-art flag. When true, SDK applies
|
|
94
|
+
* `Phaser.Textures.FilterMode.NEAREST` to the texture after load so it
|
|
95
|
+
* renders crisp instead of bilinear-blurred. Population is per-source:
|
|
96
|
+
* pack-import sets it via vision; AI generation / manual upload / library
|
|
97
|
+
* backfill close their own gaps separately (see design doc 07 §8.4).
|
|
98
|
+
*/
|
|
99
|
+
pixelArt?: boolean;
|
|
75
100
|
}
|
|
101
|
+
/** Single-image 9-slice config. Pixels from each edge to the slice line. */
|
|
102
|
+
export interface NinePatchConfig {
|
|
103
|
+
leftWidth: number;
|
|
104
|
+
rightWidth: number;
|
|
105
|
+
topHeight: number;
|
|
106
|
+
bottomHeight: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Per-frame 9-slice config (slice 7.6). One shared `NinePatchConfig` applied
|
|
110
|
+
* to every frame of a uniform-grid spritesheet. Covers ~95% of itch.io UI
|
|
111
|
+
* button packs, where each cell is a different button color/state but all
|
|
112
|
+
* share the same corner radius.
|
|
113
|
+
*/
|
|
114
|
+
export interface NinePatchPerFrame {
|
|
115
|
+
perFrame: NinePatchConfig;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Type guard — true when `ninePatch` is the per-frame variant. Lets call
|
|
119
|
+
* sites that only need to handle one shape stay narrow.
|
|
120
|
+
*/
|
|
121
|
+
export declare function isPerFrameNinePatch(np: NinePatchConfig | NinePatchPerFrame | undefined): np is NinePatchPerFrame;
|
|
76
122
|
export interface SheetAnimation {
|
|
77
123
|
/** Phaser animation key — used as `sprite.play('<name>')`. */
|
|
78
124
|
name: string;
|
|
@@ -345,7 +391,27 @@ export interface HudIconButtonVisual {
|
|
|
345
391
|
label?: string;
|
|
346
392
|
/** Optional icon asset shown inside the button. */
|
|
347
393
|
iconAssetId?: string;
|
|
348
|
-
/**
|
|
394
|
+
/**
|
|
395
|
+
* Optional image asset rendered as the button background. When the
|
|
396
|
+
* referenced asset has `ninePatch` metadata, the SDK renders it via
|
|
397
|
+
* `phaser3-rex-plugins` NinePatch so corners stay fixed at any width/height;
|
|
398
|
+
* otherwise the image is stretched to the widget's size. When set, the
|
|
399
|
+
* colored-rect bg below (fillColor / strokeColor / shape) is ignored, and
|
|
400
|
+
* `pressedFillColor` no-ops (no fill swap on a textured bg in v1).
|
|
401
|
+
*
|
|
402
|
+
* For uniform-grid spritesheet assets with `ninePatch.perFrame` set
|
|
403
|
+
* (slice 7.6), pair this with `backgroundFrame` to pick which cell to
|
|
404
|
+
* render — common case is a button-pack sheet where every cell is a
|
|
405
|
+
* different button color/state.
|
|
406
|
+
*/
|
|
407
|
+
backgroundAssetId?: string;
|
|
408
|
+
/**
|
|
409
|
+
* For uniform-grid spritesheet backgrounds (slice 7.6): index of the cell
|
|
410
|
+
* to render. Defaults to 0 when omitted. Ignored when the background asset
|
|
411
|
+
* is a single image.
|
|
412
|
+
*/
|
|
413
|
+
backgroundFrame?: number;
|
|
414
|
+
/** Button shape. Default `rounded-rect`. Ignored when backgroundAssetId is set. */
|
|
349
415
|
shape?: 'rounded-rect' | 'circle';
|
|
350
416
|
width?: number;
|
|
351
417
|
height?: number;
|
|
@@ -389,6 +455,25 @@ export interface HudPanelVisual {
|
|
|
389
455
|
kind: 'panel';
|
|
390
456
|
width?: number;
|
|
391
457
|
height?: number;
|
|
458
|
+
/**
|
|
459
|
+
* Optional image asset rendered as the panel background. When the
|
|
460
|
+
* referenced asset has `ninePatch` metadata, the SDK renders it via
|
|
461
|
+
* `phaser3-rex-plugins` NinePatch so corners stay fixed at any width/height;
|
|
462
|
+
* otherwise the image is stretched to the widget's size. When set,
|
|
463
|
+
* `backgroundColor` / `backgroundAlpha` / `borderColor` / `borderWidth` are
|
|
464
|
+
* ignored — the asset image carries its own pixels.
|
|
465
|
+
*
|
|
466
|
+
* For uniform-grid spritesheet assets with `ninePatch.perFrame` set
|
|
467
|
+
* (slice 7.6), pair this with `backgroundFrame` to pick which cell to
|
|
468
|
+
* render.
|
|
469
|
+
*/
|
|
470
|
+
backgroundAssetId?: string;
|
|
471
|
+
/**
|
|
472
|
+
* For uniform-grid spritesheet backgrounds (slice 7.6): index of the cell
|
|
473
|
+
* to render. Defaults to 0 when omitted. Ignored when the background asset
|
|
474
|
+
* is a single image.
|
|
475
|
+
*/
|
|
476
|
+
backgroundFrame?: number;
|
|
392
477
|
/** Solid fill colour. Falls back to a transparent panel if omitted. */
|
|
393
478
|
backgroundColor?: string;
|
|
394
479
|
/** Background alpha (independent of widget-level alpha). */
|
package/dist/scene/types.js
CHANGED
|
@@ -8,3 +8,10 @@
|
|
|
8
8
|
* code-rendered visuals will land in later slices.
|
|
9
9
|
*/
|
|
10
10
|
export const SCHEMA_VERSION = 1;
|
|
11
|
+
/**
|
|
12
|
+
* Type guard — true when `ninePatch` is the per-frame variant. Lets call
|
|
13
|
+
* sites that only need to handle one shape stay narrow.
|
|
14
|
+
*/
|
|
15
|
+
export function isPerFrameNinePatch(np) {
|
|
16
|
+
return !!np && 'perFrame' in np;
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unboxy/phaser-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.38",
|
|
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": {
|