@unboxy/phaser-sdk 0.2.29 → 0.2.30

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/index.d.ts CHANGED
@@ -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, HudVisual, HudTextVisual, HudImageVisual, HudIconButtonVisual, HudTextSource, 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, } 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';
@@ -96,6 +96,12 @@ export function spawnHudEntity(ctx, entity) {
96
96
  case 'icon-button':
97
97
  go = createIconButton(ctx, entity, pos);
98
98
  break;
99
+ case 'progress-bar':
100
+ go = createProgressBar(ctx, entity, pos);
101
+ break;
102
+ case 'panel':
103
+ go = createPanel(ctx, entity, pos);
104
+ break;
99
105
  default: {
100
106
  const exhaustive = entity;
101
107
  throw new Error(`[unboxy/hud] unknown HUD entity kind: ${JSON.stringify(exhaustive)}`);
@@ -227,6 +233,135 @@ function createIconButton(ctx, entity, pos) {
227
233
  container.setData('editorHitHeight', h);
228
234
  return container;
229
235
  }
236
+ /**
237
+ * Progress bar — Graphics-rendered (no Phaser primitive matches "fill that
238
+ * shrinks/grows from one edge"). Subscribes to value + max bindings if
239
+ * either is dynamic; redraws whenever a relevant registry key changes.
240
+ */
241
+ function createProgressBar(ctx, entity, pos) {
242
+ const v = entity.visual;
243
+ const w = v.width ?? 200;
244
+ const h = v.height ?? 16;
245
+ const container = ctx.scene.add.container(pos.x, pos.y);
246
+ // Background + fill drawn into a Graphics so width-stretching doesn't
247
+ // distort corners (no setDisplaySize stretching, no extra dependencies).
248
+ const g = ctx.scene.add.graphics();
249
+ container.add(g);
250
+ const draw = (frac) => {
251
+ const f = Math.max(0, Math.min(1, frac));
252
+ g.clear();
253
+ const radius = v.borderRadius ?? 0;
254
+ // Background.
255
+ if (v.backgroundColor) {
256
+ g.fillStyle(parseColor(v.backgroundColor), 1);
257
+ if (radius > 0)
258
+ g.fillRoundedRect(0, 0, w, h, radius);
259
+ else
260
+ g.fillRect(0, 0, w, h);
261
+ }
262
+ // Fill.
263
+ const fillColor = v.fillColor ?? '#22c55e';
264
+ g.fillStyle(parseColor(fillColor), 1);
265
+ const fillW = w * f;
266
+ if (fillW > 0) {
267
+ if (radius > 0)
268
+ g.fillRoundedRect(0, 0, fillW, h, Math.min(radius, fillW / 2));
269
+ else
270
+ g.fillRect(0, 0, fillW, h);
271
+ }
272
+ // Border.
273
+ if (v.borderColor && (v.borderWidth ?? 0) > 0) {
274
+ g.lineStyle(v.borderWidth ?? 1, parseColor(v.borderColor), 1);
275
+ if (radius > 0)
276
+ g.strokeRoundedRect(0, 0, w, h, radius);
277
+ else
278
+ g.strokeRect(0, 0, w, h);
279
+ }
280
+ };
281
+ const computeFrac = () => {
282
+ const value = readNumberSource(v.value, ctx.scene);
283
+ const max = readNumberSource(v.max, ctx.scene);
284
+ if (max <= 0)
285
+ return 0;
286
+ return value / max;
287
+ };
288
+ draw(computeFrac());
289
+ // Subscribe to dynamic bindings. Either value or max can be dynamic;
290
+ // each pushes a redraw on registry change.
291
+ const cleanups = [];
292
+ if (v.value.mode === 'dynamic') {
293
+ const key = `changedata-${v.value.binding}`;
294
+ const handler = () => draw(computeFrac());
295
+ ctx.scene.game.registry.events.on(key, handler);
296
+ cleanups.push(() => ctx.scene.game.registry.events.off(key, handler));
297
+ }
298
+ if (v.max.mode === 'dynamic') {
299
+ const key = `changedata-${v.max.binding}`;
300
+ const handler = () => draw(computeFrac());
301
+ ctx.scene.game.registry.events.on(key, handler);
302
+ cleanups.push(() => ctx.scene.game.registry.events.off(key, handler));
303
+ }
304
+ // setSize so the editor's bounds-based hit-test works on the container.
305
+ container.setSize(w, h);
306
+ container.setData('editorHitWidth', w);
307
+ container.setData('editorHitHeight', h);
308
+ applyContainerOriginShift(container, entity.anchor.side, w, h);
309
+ // Persist redraw + cleanup on the GameObject for applyHudPatch to reach.
310
+ container.setData('hudRedraw', () => draw(computeFrac()));
311
+ container.setData('hudCleanup', () => cleanups.forEach((fn) => fn()));
312
+ container.once(Phaser.GameObjects.Events.DESTROY, () => {
313
+ const fn = container.getData('hudCleanup');
314
+ fn?.();
315
+ });
316
+ return container;
317
+ }
318
+ /**
319
+ * Panel — a colored rectangle with optional border + rounded corners. Used
320
+ * as a visual frame behind other HUD widgets (no children layout in v1;
321
+ * children live alongside the panel as separate entities).
322
+ */
323
+ function createPanel(ctx, entity, pos) {
324
+ const v = entity.visual;
325
+ const w = v.width ?? 200;
326
+ const h = v.height ?? 100;
327
+ const container = ctx.scene.add.container(pos.x, pos.y);
328
+ const g = ctx.scene.add.graphics();
329
+ container.add(g);
330
+ const draw = () => {
331
+ g.clear();
332
+ const radius = v.borderRadius ?? 0;
333
+ if (v.backgroundColor) {
334
+ g.fillStyle(parseColor(v.backgroundColor), v.backgroundAlpha ?? 1);
335
+ if (radius > 0)
336
+ g.fillRoundedRect(0, 0, w, h, radius);
337
+ else
338
+ g.fillRect(0, 0, w, h);
339
+ }
340
+ if (v.borderColor && (v.borderWidth ?? 0) > 0) {
341
+ g.lineStyle(v.borderWidth ?? 1, parseColor(v.borderColor), 1);
342
+ if (radius > 0)
343
+ g.strokeRoundedRect(0, 0, w, h, radius);
344
+ else
345
+ g.strokeRect(0, 0, w, h);
346
+ }
347
+ };
348
+ draw();
349
+ container.setSize(w, h);
350
+ container.setData('editorHitWidth', w);
351
+ container.setData('editorHitHeight', h);
352
+ applyContainerOriginShift(container, entity.anchor.side, w, h);
353
+ container.setData('hudRedraw', draw);
354
+ return container;
355
+ }
356
+ /** Read a numeric source (static or dynamic from registry). */
357
+ function readNumberSource(source, scene) {
358
+ if (source.mode === 'static')
359
+ return source.value;
360
+ const raw = scene.game.registry.get(source.binding);
361
+ if (typeof raw === 'number' && Number.isFinite(raw))
362
+ return raw;
363
+ return source.fallback ?? 0;
364
+ }
230
365
  /**
231
366
  * Map a 9-grid anchor side to a Phaser origin pair so the rendered widget
232
367
  * is positioned correctly relative to the resolved anchor coordinate.
@@ -596,6 +731,39 @@ export function applyHudPatch(game, entityId, patch) {
596
731
  image.setDisplaySize(p.width, p.height);
597
732
  }
598
733
  }
734
+ if (patch.visual && (entity.kind === 'progress-bar' || entity.kind === 'panel')) {
735
+ // Both kinds are container + Graphics. Patches that change geometry
736
+ // (width/height) or styling are handled by mutating the entity record
737
+ // then re-spawning the widget in place — same pattern as icon-button.
738
+ // For binding-source changes on progress-bar, also re-spawn so the
739
+ // registry subscriptions get rewired cleanly.
740
+ const v = entity.visual;
741
+ for (const [k, val] of Object.entries(patch.visual)) {
742
+ if (val !== undefined)
743
+ v[k] = val;
744
+ }
745
+ const reg2 = reg;
746
+ const safeArea2 = safeArea;
747
+ const oldDepth = go.depth;
748
+ go.destroy();
749
+ reg2.unregister(entityId);
750
+ const ctx = {
751
+ scene: hud,
752
+ registry: reg2,
753
+ safeArea: safeArea2,
754
+ resolveAsset: (id) => {
755
+ const m = getManifest(hud);
756
+ const a = m.assets?.find((x) => x.id === id);
757
+ if (!a)
758
+ throw new Error(`[unboxy/hud] manifest has no asset with id '${id}'`);
759
+ return a;
760
+ },
761
+ };
762
+ const fresh = spawnHudEntity(ctx, entity);
763
+ if (typeof oldDepth === 'number') {
764
+ fresh.setDepth?.(oldDepth);
765
+ }
766
+ }
599
767
  if (patch.visual && entity.kind === 'icon-button') {
600
768
  // Icon-button is a Container — visual patches require a small refresh.
601
769
  // For v1 we just patch the entity record + label/colour fields cheaply;
@@ -318,8 +318,50 @@ export interface HudIconButtonVisual {
318
318
  textColor?: string;
319
319
  fontSize?: number;
320
320
  }
321
- export type HudVisual = HudTextVisual | HudImageVisual | HudIconButtonVisual;
322
- export type HudEntityKind = 'text' | 'image' | 'icon-button';
321
+ /**
322
+ * A progress bar's numeric input. Static = literal number; dynamic = read
323
+ * from `unboxy.gameData` / Phaser's game registry by key. Mirrors
324
+ * `HudTextSource` but for numbers (no prefix/suffix — bar is visual).
325
+ */
326
+ export type HudNumberSource = {
327
+ mode: 'static';
328
+ value: number;
329
+ } | {
330
+ mode: 'dynamic';
331
+ binding: string;
332
+ /** Used when the binding is unset at runtime. Defaults to 0. */
333
+ fallback?: number;
334
+ };
335
+ export interface HudProgressBarVisual {
336
+ kind: 'progress-bar';
337
+ /** Current value source (e.g. player's HP). */
338
+ value: HudNumberSource;
339
+ /** Max value source (e.g. player's max HP). */
340
+ max: HudNumberSource;
341
+ width?: number;
342
+ height?: number;
343
+ fillColor?: string;
344
+ backgroundColor?: string;
345
+ borderColor?: string | null;
346
+ borderWidth?: number;
347
+ /** Border radius for rounded rectangles. Defaults to 0 (sharp corners). */
348
+ borderRadius?: number;
349
+ }
350
+ export interface HudPanelVisual {
351
+ kind: 'panel';
352
+ width?: number;
353
+ height?: number;
354
+ /** Solid fill colour. Falls back to a transparent panel if omitted. */
355
+ backgroundColor?: string;
356
+ /** Background alpha (independent of widget-level alpha). */
357
+ backgroundAlpha?: number;
358
+ borderColor?: string | null;
359
+ borderWidth?: number;
360
+ /** Border radius for the rect. Defaults to 0. */
361
+ borderRadius?: number;
362
+ }
363
+ export type HudVisual = HudTextVisual | HudImageVisual | HudIconButtonVisual | HudProgressBarVisual | HudPanelVisual;
364
+ export type HudEntityKind = 'text' | 'image' | 'icon-button' | 'progress-bar' | 'panel';
323
365
  export interface HudEntityBase {
324
366
  id: string;
325
367
  /** Optional semantic tag. Behavior code keys off this. */
@@ -358,7 +400,15 @@ export interface HudIconButtonEntity extends HudEntityBase {
358
400
  kind: 'icon-button';
359
401
  visual: HudIconButtonVisual;
360
402
  }
361
- export type HudEntity = HudTextEntity | HudImageEntity | HudIconButtonEntity;
403
+ export interface HudProgressBarEntity extends HudEntityBase {
404
+ kind: 'progress-bar';
405
+ visual: HudProgressBarVisual;
406
+ }
407
+ export interface HudPanelEntity extends HudEntityBase {
408
+ kind: 'panel';
409
+ visual: HudPanelVisual;
410
+ }
411
+ export type HudEntity = HudTextEntity | HudImageEntity | HudIconButtonEntity | HudProgressBarEntity | HudPanelEntity;
362
412
  export interface HudScene {
363
413
  schemaVersion: number;
364
414
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboxy/phaser-sdk",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
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",