@unboxy/phaser-sdk 0.2.16 → 0.2.18

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.
@@ -0,0 +1,30 @@
1
+ import Phaser from 'phaser';
2
+ import { AssetRecord, WorldEntity } from './types.js';
3
+ import { EntityRegistry } from './EntityRegistry.js';
4
+ /**
5
+ * Resolves an asset id to its AssetRecord, throwing a clear error if the
6
+ * scene file references something the manifest doesn't know about. We
7
+ * surface this as an error (not a silent miss) because a missing asset
8
+ * means either the manifest is stale or the scene file was hand-edited
9
+ * — both worth catching at runtime, not skipping silently.
10
+ */
11
+ export type AssetResolver = (assetId: string) => AssetRecord;
12
+ /** Optional hook for `code-rendered` visuals. v1 throws if not provided. */
13
+ export type RenderScriptResolver = (scriptPath: string) => ((g: Phaser.GameObjects.Graphics, params: Record<string, unknown>) => void) | undefined;
14
+ export interface SpawnContext {
15
+ scene: Phaser.Scene;
16
+ registry: EntityRegistry;
17
+ resolveAsset: AssetResolver;
18
+ resolveRenderScript?: RenderScriptResolver;
19
+ }
20
+ /**
21
+ * Spawn one entity into the scene and register it. Returns the created
22
+ * GameObject so callers (notably `spawnEntity` itself, recursing on a
23
+ * group's children) can attach it to a parent container.
24
+ */
25
+ export declare function spawnEntity(ctx: SpawnContext, entity: WorldEntity): Phaser.GameObjects.GameObject;
26
+ /**
27
+ * Accepts `'#rrggbb'`, `'rrggbb'`, or `'0xrrggbb'` and returns a number
28
+ * suitable for Phaser's color APIs.
29
+ */
30
+ export declare function parseColor(input: string): number;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Spawn one entity into the scene and register it. Returns the created
3
+ * GameObject so callers (notably `spawnEntity` itself, recursing on a
4
+ * group's children) can attach it to a parent container.
5
+ */
6
+ export function spawnEntity(ctx, entity) {
7
+ let go;
8
+ switch (entity.kind) {
9
+ case 'sprite':
10
+ go = createSprite(ctx, entity);
11
+ break;
12
+ case 'primitive':
13
+ go = createPrimitive(ctx, entity);
14
+ break;
15
+ case 'group':
16
+ go = createGroup(ctx, entity);
17
+ break;
18
+ case 'code-rendered':
19
+ go = createCodeRendered(ctx, entity);
20
+ break;
21
+ case 'tilemap':
22
+ throw new Error(`[unboxy/scene] tilemap entity ('${entity.id}') not supported in slice 1 — see 06-tilemap.md`);
23
+ case 'trigger':
24
+ throw new Error(`[unboxy/scene] trigger entity ('${entity.id}') not supported in slice 1 — see 03-world-editor.md`);
25
+ default: {
26
+ const exhaustive = entity;
27
+ throw new Error(`[unboxy/scene] unknown entity kind: ${JSON.stringify(exhaustive)}`);
28
+ }
29
+ }
30
+ applyTransform(go, entity.transform);
31
+ tagGameObject(go, entity);
32
+ ctx.registry.register(entity.id, entity.role, go);
33
+ return go;
34
+ }
35
+ function createSprite(ctx, entity) {
36
+ const v = entity.visual;
37
+ const asset = ctx.resolveAsset(v.assetId);
38
+ // Phaser's add.sprite handles both plain images and atlas/spritesheet
39
+ // textures uniformly when given (key, frame). For plain images, frame
40
+ // is undefined and Phaser uses the default frame.
41
+ const sprite = ctx.scene.add.sprite(0, 0, asset.textureKey, v.frame);
42
+ if (v.tint)
43
+ sprite.setTint(parseColor(v.tint));
44
+ if (typeof v.alpha === 'number')
45
+ sprite.setAlpha(v.alpha);
46
+ if (v.flipX)
47
+ sprite.setFlipX(true);
48
+ if (v.flipY)
49
+ sprite.setFlipY(true);
50
+ return sprite;
51
+ }
52
+ function createPrimitive(ctx, entity) {
53
+ const v = entity.visual;
54
+ if (v.kind === 'rect')
55
+ return createRect(ctx, v);
56
+ if (v.kind === 'circle')
57
+ return createCircle(ctx, v);
58
+ const exhaustive = v;
59
+ throw new Error(`[unboxy/scene] unknown primitive kind: ${JSON.stringify(exhaustive)}`);
60
+ }
61
+ function createRect(ctx, v) {
62
+ const fill = v.fillColor ? parseColor(v.fillColor) : 0xffffff;
63
+ const rect = ctx.scene.add.rectangle(0, 0, v.width, v.height, fill);
64
+ if (v.strokeColor && v.strokeWidth) {
65
+ rect.setStrokeStyle(v.strokeWidth, parseColor(v.strokeColor));
66
+ }
67
+ if (typeof v.alpha === 'number')
68
+ rect.setAlpha(v.alpha);
69
+ return rect;
70
+ }
71
+ function createCircle(ctx, v) {
72
+ const fill = v.fillColor ? parseColor(v.fillColor) : 0xffffff;
73
+ const arc = ctx.scene.add.circle(0, 0, v.radius, fill);
74
+ if (v.strokeColor && v.strokeWidth) {
75
+ arc.setStrokeStyle(v.strokeWidth, parseColor(v.strokeColor));
76
+ }
77
+ if (typeof v.alpha === 'number')
78
+ arc.setAlpha(v.alpha);
79
+ return arc;
80
+ }
81
+ function createGroup(ctx, entity) {
82
+ const container = ctx.scene.add.container(0, 0);
83
+ for (const child of entity.children) {
84
+ if (child.kind === 'group') {
85
+ // v1 cap: groups can't nest. Surface a clear error rather than
86
+ // silently producing a half-attached hierarchy.
87
+ throw new Error(`[unboxy/scene] group '${entity.id}' has a nested group child '${child.id}' — v1 supports one level of nesting only`);
88
+ }
89
+ const childGo = spawnEntity(ctx, child);
90
+ container.add(childGo);
91
+ }
92
+ return container;
93
+ }
94
+ function createCodeRendered(ctx, entity) {
95
+ const render = ctx.resolveRenderScript?.(entity.visual.script);
96
+ if (!render) {
97
+ throw new Error(`[unboxy/scene] code-rendered entity '${entity.id}' references '${entity.visual.script}' but no render-script resolver was provided — render scripts land in a later slice (see 02-render-scripts.md)`);
98
+ }
99
+ const g = ctx.scene.add.graphics();
100
+ render(g, entity.visual.params ?? {});
101
+ return g;
102
+ }
103
+ function applyTransform(go, t) {
104
+ // Most game objects implement Transform; Container does too. Cast through
105
+ // a permissive shape so this works for sprite/rect/circle/container alike.
106
+ const target = go;
107
+ target.x = t.x;
108
+ target.y = t.y;
109
+ if (typeof t.rotation === 'number')
110
+ target.rotation = t.rotation;
111
+ if (typeof t.scaleX === 'number')
112
+ target.scaleX = t.scaleX;
113
+ if (typeof t.scaleY === 'number')
114
+ target.scaleY = t.scaleY;
115
+ if (typeof t.depth === 'number' && target.setDepth)
116
+ target.setDepth(t.depth);
117
+ }
118
+ function tagGameObject(go, entity) {
119
+ go.setData('entityId', entity.id);
120
+ if (entity.role)
121
+ go.setData('entityRole', entity.role);
122
+ if (entity.properties)
123
+ go.setData('entityProperties', entity.properties);
124
+ }
125
+ /**
126
+ * Accepts `'#rrggbb'`, `'rrggbb'`, or `'0xrrggbb'` and returns a number
127
+ * suitable for Phaser's color APIs.
128
+ */
129
+ export function parseColor(input) {
130
+ const trimmed = input.trim();
131
+ if (trimmed.startsWith('#'))
132
+ return parseInt(trimmed.slice(1), 16);
133
+ if (trimmed.startsWith('0x'))
134
+ return parseInt(trimmed.slice(2), 16);
135
+ return parseInt(trimmed, 16);
136
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Scene-as-data types — visual editor foundation (slice 1).
3
+ *
4
+ * Spec: unboxy-design/features/visual-editor/01-scene-data-foundation.md
5
+ *
6
+ * v1 schema (`schemaVersion: 1`) supports the entity kinds needed for the
7
+ * read-only scene viewer. HUD entity kinds, tilemap, trigger, and
8
+ * code-rendered visuals will land in later slices.
9
+ */
10
+ export declare const SCHEMA_VERSION = 1;
11
+ export type SceneType = 'world' | 'hud';
12
+ export interface SceneRef {
13
+ id: string;
14
+ type: SceneType;
15
+ /** Path under `public/scenes/`, e.g. `"world/level-1-1.json"`. */
16
+ file: string;
17
+ /** Optional HUD scene id to overlay (world scenes only). Null = no HUD. */
18
+ hud?: string | null;
19
+ }
20
+ export interface HudRef {
21
+ id: string;
22
+ /** Path under `public/scenes/`, e.g. `"hud/game-hud.json"`. */
23
+ file: string;
24
+ }
25
+ /**
26
+ * Per-game asset table. Source of truth for asset id → Phaser textureKey
27
+ * resolution. The editor and runtime both read from this.
28
+ */
29
+ export type AssetKind = 'image' | 'spritesheet' | 'atlas' | 'audio' | 'json';
30
+ export interface AssetRecord {
31
+ /** Stable asset id used in scene files (`visual.assetId`). */
32
+ id: string;
33
+ /** Phaser texture / cache key. May equal `id` for simple cases. */
34
+ textureKey: string;
35
+ /** Path relative to `public/`. */
36
+ path: string;
37
+ kind: AssetKind;
38
+ /** For `spritesheet`: Phaser SpriteSheetConfig. */
39
+ spriteSheetConfig?: {
40
+ frameWidth: number;
41
+ frameHeight: number;
42
+ margin?: number;
43
+ spacing?: number;
44
+ };
45
+ /** For `atlas`: companion atlas file path (xml or json). */
46
+ atlasPath?: string;
47
+ /** `'xml'` (Starling) or `'json'` (TexturePacker). */
48
+ atlasFormat?: 'xml' | 'json';
49
+ }
50
+ export interface Manifest {
51
+ schemaVersion: number;
52
+ id: string;
53
+ title: string;
54
+ version: string;
55
+ initialScene: string;
56
+ scenes: SceneRef[];
57
+ huds: HudRef[];
58
+ assets: AssetRecord[];
59
+ globals?: {
60
+ physics?: {
61
+ gravity?: {
62
+ x?: number;
63
+ y?: number;
64
+ };
65
+ };
66
+ world?: {
67
+ pixelArt?: boolean;
68
+ };
69
+ };
70
+ }
71
+ export interface Transform {
72
+ x: number;
73
+ y: number;
74
+ /** Radians. */
75
+ rotation?: number;
76
+ scaleX?: number;
77
+ scaleY?: number;
78
+ depth?: number;
79
+ }
80
+ export type AnchorSide = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right';
81
+ export interface Anchor {
82
+ side: AnchorSide;
83
+ offsetX?: number;
84
+ offsetY?: number;
85
+ }
86
+ export interface SpriteVisual {
87
+ kind: 'sprite';
88
+ assetId: string;
89
+ /** Atlas frame name OR sprite-sheet frame index. */
90
+ frame?: string | number;
91
+ tint?: string;
92
+ alpha?: number;
93
+ flipX?: boolean;
94
+ flipY?: boolean;
95
+ }
96
+ export interface PrimitiveRectVisual {
97
+ kind: 'rect';
98
+ width: number;
99
+ height: number;
100
+ fillColor?: string;
101
+ strokeColor?: string | null;
102
+ strokeWidth?: number;
103
+ alpha?: number;
104
+ }
105
+ export interface PrimitiveCircleVisual {
106
+ kind: 'circle';
107
+ radius: number;
108
+ fillColor?: string;
109
+ strokeColor?: string | null;
110
+ strokeWidth?: number;
111
+ alpha?: number;
112
+ }
113
+ export type PrimitiveVisual = PrimitiveRectVisual | PrimitiveCircleVisual;
114
+ export interface CodeRenderedVisual {
115
+ kind: 'code-rendered';
116
+ /** Path to render script, e.g. `"src/visuals/boss-renderer.ts"`. */
117
+ script: string;
118
+ params?: Record<string, unknown>;
119
+ }
120
+ export type WorldVisual = SpriteVisual | PrimitiveVisual | CodeRenderedVisual;
121
+ export type WorldEntityKind = 'sprite' | 'primitive' | 'code-rendered' | 'group' | 'tilemap' | 'trigger';
122
+ export interface WorldEntityBase {
123
+ id: string;
124
+ /** Optional semantic tag. Behavior code keys off this. */
125
+ role?: string;
126
+ /** Free-form per-entity data the agent's behavior code reads. */
127
+ properties?: Record<string, unknown>;
128
+ transform: Transform;
129
+ }
130
+ export interface SpriteEntity extends WorldEntityBase {
131
+ kind: 'sprite';
132
+ visual: SpriteVisual;
133
+ }
134
+ export interface PrimitiveEntity extends WorldEntityBase {
135
+ kind: 'primitive';
136
+ visual: PrimitiveVisual;
137
+ }
138
+ export interface CodeRenderedEntity extends WorldEntityBase {
139
+ kind: 'code-rendered';
140
+ visual: CodeRenderedVisual;
141
+ }
142
+ export interface GroupEntity extends WorldEntityBase {
143
+ kind: 'group';
144
+ /**
145
+ * One level of nesting in v1: a child cannot itself be a `group`.
146
+ * If you need deeper hierarchy, the agent should flatten via the
147
+ * existing transform and add a role tag for grouping queries.
148
+ */
149
+ children: NonGroupWorldEntity[];
150
+ }
151
+ /**
152
+ * v1 placeholder — schema slot reserved so future slices can layer in
153
+ * tilemap/trigger without a schemaVersion bump. Loader currently throws
154
+ * on these kinds so an unmigrated game can't silently render nothing.
155
+ */
156
+ export interface TilemapEntity extends WorldEntityBase {
157
+ kind: 'tilemap';
158
+ /** Opaque tilemap payload — defined in `06-tilemap.md`. */
159
+ data: unknown;
160
+ }
161
+ export interface TriggerEntity extends WorldEntityBase {
162
+ kind: 'trigger';
163
+ /** Opaque trigger payload — defined in `03-world-editor.md`. */
164
+ data: unknown;
165
+ }
166
+ export type NonGroupWorldEntity = SpriteEntity | PrimitiveEntity | CodeRenderedEntity | TilemapEntity | TriggerEntity;
167
+ export type WorldEntity = NonGroupWorldEntity | GroupEntity;
168
+ export interface CameraConfig {
169
+ /** Entity id to follow. Null = static camera. */
170
+ follow?: string | null;
171
+ smoothing?: number;
172
+ deadzone?: {
173
+ x: number;
174
+ y: number;
175
+ };
176
+ lookahead?: number;
177
+ bounds?: {
178
+ x: number;
179
+ y: number;
180
+ width: number;
181
+ height: number;
182
+ };
183
+ zoom?: number;
184
+ pixelPerfect?: boolean;
185
+ shakeDecay?: number;
186
+ }
187
+ export interface WorldSceneConfig {
188
+ width: number;
189
+ height: number;
190
+ background?: {
191
+ color?: string;
192
+ };
193
+ physics?: {
194
+ gravity?: {
195
+ x?: number;
196
+ y?: number;
197
+ };
198
+ };
199
+ }
200
+ export interface WorldScene {
201
+ schemaVersion: number;
202
+ id: string;
203
+ name: string;
204
+ type: 'world';
205
+ world: WorldSceneConfig;
206
+ camera?: CameraConfig;
207
+ entities: WorldEntity[];
208
+ metadata?: {
209
+ tags?: string[];
210
+ author?: string;
211
+ createdAt?: string;
212
+ updatedAt?: string;
213
+ };
214
+ }
215
+ /**
216
+ * HUD scene schema slot — v1 reserves the shape for slice 5. The loader
217
+ * throws if it encounters one in slice 1; the type lives here so the
218
+ * manifest's `huds[]` list is well-typed.
219
+ */
220
+ export interface HudScene {
221
+ schemaVersion: number;
222
+ id: string;
223
+ name: string;
224
+ type: 'hud';
225
+ design?: {
226
+ designAspectRatio?: string;
227
+ safeArea?: {
228
+ top: number;
229
+ right: number;
230
+ bottom: number;
231
+ left: number;
232
+ };
233
+ };
234
+ entities: unknown[];
235
+ metadata?: WorldScene['metadata'];
236
+ }
237
+ export type SceneFile = WorldScene | HudScene;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Scene-as-data types — visual editor foundation (slice 1).
3
+ *
4
+ * Spec: unboxy-design/features/visual-editor/01-scene-data-foundation.md
5
+ *
6
+ * v1 schema (`schemaVersion: 1`) supports the entity kinds needed for the
7
+ * read-only scene viewer. HUD entity kinds, tilemap, trigger, and
8
+ * code-rendered visuals will land in later slices.
9
+ */
10
+ export const SCHEMA_VERSION = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboxy/phaser-sdk",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
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",
@@ -11,7 +11,8 @@
11
11
  "scripts": {
12
12
  "build": "tsc",
13
13
  "test": "npm run build && node --test scripts/test-chat-facade.mjs",
14
- "prepublishOnly": "npm run build"
14
+ "prepublishOnly": "npm run build && [ \"$UNBOXY_PUBLISH_VIA_SCRIPT\" = \"1\" ] || (echo '\\nERROR: do not run `npm publish` directly. Use ./publish.sh instead — it gates on the template-sync checklist.\\nSee CLAUDE.md \"Template sync — do not skip\".\\n' >&2 && exit 1)",
15
+ "release": "./publish.sh"
15
16
  },
16
17
  "dependencies": {
17
18
  "colyseus.js": "^0.16.0"