castle-web-cli 0.4.81 → 0.4.82

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.
@@ -100,13 +100,14 @@ This inline shape is fine to keep writing by hand — see `## Blueprints` for wh
100
100
  Every actor placed through the editor is an **instance** of a **blueprint**: a `.scene` file under `blueprints/` whose single actor (`actors[0]`, no `id`) is the template. An instance actor looks like:
101
101
 
102
102
  ```json
103
- { "id": "abc123", "blueprint": "blueprints/blueprint-1.scene", "components": { "Layout": { "x": 100, "y": 200 } } }
103
+ { "id": "abc123", "blueprint": "blueprints/enemy.scene", "components": { "Layout": { "x": 100, "y": 200 } } }
104
104
  ```
105
105
 
106
106
  `components` on an instance holds ONLY the props that differ from the blueprint (an override), merged on top of the blueprint's template per-property. Editing the blueprint file changes every instance that doesn't override that prop; editing an instance's own `Layout.x`/`y`/`rotation` never touches the blueprint (position/rotation are always instance-local — see `Layout.jsx`'s `propertyMeta`). This is entirely optional for you to manage directly:
107
107
 
108
108
  - **Plain inline actors (no `blueprint` field) still just work.** Write them the normal way; the editor mints a blueprint for each one automatically the first time a human opens that scene in the browser, and rewrites the actor into an instance. You never have to do this migration yourself.
109
109
  - **Reference a blueprint directly when it saves real duplication** — e.g. 10 identical enemy actors: write one `blueprints/enemy.scene` (`{ "name": "Enemy", "actors": [{ "components": { "Sprite": {...}, "Collider": {...}, "Enemy": {...} } }] }`) and 10 scene actors that each just set `"blueprint": "blueprints/enemy.scene"` plus their own `Layout.x`/`y`. Don't bother doing this for a one-off actor — plain inline is simpler and migrates fine later.
110
+ - **When an actor is a distinct kind of thing** (not a one-off), author its blueprint yourself — `blueprints/<meaningful>.scene` with a real `"name"` — instead of leaving it inline for auto-migration to name; migration derives a reasonable name/file from the actor's `id` when it can, but a blueprint you write intentionally will always be clearer than one it infers.
110
111
  - Never invent your own `blueprint` path pointing at a file you didn't also create — a dangling reference resolves to "no template" (the instance's own sparse `components` render alone, missing whatever it expected to inherit).
111
112
 
112
113
  ## Built-in behaviors
@@ -54,7 +54,11 @@ export function ScenePlayer({ sceneData, sprites, files, behaviorClasses, onFirs
54
54
  canvas.addEventListener('pointermove', onPointerMove);
55
55
  canvas.addEventListener('pointerup', onPointerUp);
56
56
  canvas.addEventListener('pointercancel', onPointerUp);
57
- canvas.focus();
57
+ // Don't steal focus from the embedding shell (e.g. the chat composer) on
58
+ // reload/remount. A real standalone/top-level play, or an iframe that's
59
+ // already focused, still auto-focuses; a click into the game always
60
+ // focuses via onPointerDown above.
61
+ if (document.hasFocus() || window.parent === window) canvas.focus();
58
62
  const stopLoop = startPlayerLoop(canvas, ctx, runtime, onFirstFrame);
59
63
  return () => {
60
64
  stopLoop();
@@ -126,6 +126,69 @@ function mintBlueprintPath(existingPaths) {
126
126
  return `${BLUEPRINTS_DIR}/blueprint-${max + 1}.scene`;
127
127
  }
128
128
 
129
+ // "auto-tetris" -> "Auto Tetris". Splits on non-alphanumeric runs (covers the
130
+ // hyphen/underscore ids actors typically use) and title-cases each word.
131
+ function humanizeActorId(id) {
132
+ return id
133
+ .split(/[^a-zA-Z0-9]+/)
134
+ .filter(Boolean)
135
+ .map((word) => word[0].toUpperCase() + word.slice(1))
136
+ .join(' ');
137
+ }
138
+
139
+ // "Auto Tetris" -> "auto-tetris" for the filename slug. Kept independent of
140
+ // the actor id's own formatting (an id could already be any casing/shape).
141
+ function slugifyActorId(id) {
142
+ return id
143
+ .trim()
144
+ .toLowerCase()
145
+ .replace(/[^a-z0-9]+/g, '-')
146
+ .replace(/^-+|-+$/g, '');
147
+ }
148
+
149
+ // Derive a migration blueprint's { name, path } from the orphan actor's own
150
+ // `id` -- the one meaningful signal available at migration time (vs. the
151
+ // anonymous "Blueprint N" counter), so e.g. an actor `id: "logo"` mints
152
+ // `blueprints/logo.scene` named "Logo" instead of "Blueprint 1". Returns null
153
+ // when the id doesn't yield a usable name/slug, so the caller falls back to
154
+ // the counter-based mint functions.
155
+ //
156
+ // Collision safety is load-bearing: `knownBlueprints` includes every
157
+ // already-migrated blueprint in this same run (see the caller's loop), so two
158
+ // orphan actors that humanize to the same name, or that collide with a
159
+ // pre-existing blueprint, must never mint the same path or duplicate name.
160
+ // Deterministically disambiguate with a numeric suffix instead.
161
+ function deriveBlueprintName(actorId, knownBlueprints) {
162
+ if (typeof actorId !== 'string') return null;
163
+ const baseSlug = slugifyActorId(actorId);
164
+ const baseName = humanizeActorId(actorId.trim());
165
+ if (!baseSlug || !baseName) return null;
166
+ const existingNames = new Set(knownBlueprints.map((blueprint) => blueprint.name));
167
+ const existingPaths = new Set(knownBlueprints.map((blueprint) => blueprint.path));
168
+ const MAX_ATTEMPTS = 1000;
169
+ for (let suffix = 1; suffix <= MAX_ATTEMPTS; suffix += 1) {
170
+ const name = suffix === 1 ? baseName : `${baseName} ${suffix}`;
171
+ const path = suffix === 1 ? `${BLUEPRINTS_DIR}/${baseSlug}.scene` : `${BLUEPRINTS_DIR}/${baseSlug}-${suffix}.scene`;
172
+ if (!existingNames.has(name) && !existingPaths.has(path)) return { name, path };
173
+ }
174
+ // Pathological: 1000 colliding suffixes. Let the caller fall back to the
175
+ // counter-based mint functions rather than looping forever.
176
+ return null;
177
+ }
178
+
179
+ // { name, path } for a freshly minted migration blueprint: the actor id's
180
+ // derived name/slug when usable and collision-free, else the anonymous
181
+ // counter (see `deriveBlueprintName`). Factored out so the branching here
182
+ // doesn't add to `migrateOrphanActors`'s own complexity.
183
+ function mintMigrationBlueprintName(actorId, knownBlueprints) {
184
+ const derived = deriveBlueprintName(actorId, knownBlueprints);
185
+ if (derived) return derived;
186
+ return {
187
+ name: mintBlueprintName(knownBlueprints.map((blueprint) => blueprint.name)),
188
+ path: mintBlueprintPath(knownBlueprints.map((blueprint) => blueprint.path)),
189
+ };
190
+ }
191
+
129
192
  export function formatBlueprintFileText(name, components) {
130
193
  return formatJson({ name, actors: [{ components }] });
131
194
  }
@@ -408,11 +471,19 @@ export function migrateOrphanActors(files, behaviors, sceneData) {
408
471
  if (!props) continue;
409
472
  const Behavior = findBehavior(behaviors, behaviorName);
410
473
  const { inherited, overridden } = splitByInherit(Behavior, props);
411
- if (Object.keys(inherited).length > 0) templateComponents[behaviorName] = inherited;
474
+ // A behavior attached with no props at all (e.g. `AutoTetris: {}`) has
475
+ // both `inherited` and `overridden` empty. It must still land somewhere,
476
+ // or the attachment itself -- not just some prop of it -- silently
477
+ // vanishes from the resolved actor. Fall back to the template side (as
478
+ // `{}`) so the instance keeps referencing the behavior exactly once.
479
+ if (Object.keys(inherited).length > 0 || Object.keys(overridden).length === 0) {
480
+ templateComponents[behaviorName] = inherited;
481
+ }
412
482
  if (Object.keys(overridden).length > 0) instanceOverrides[behaviorName] = overridden;
413
483
  }
414
- const name = mintBlueprintName(knownBlueprints.map((blueprint) => blueprint.name));
415
- const path = mintBlueprintPath(knownBlueprints.map((blueprint) => blueprint.path));
484
+ // The actor's own id is the one meaningful signal available here (vs. an
485
+ // anonymous "Blueprint N") -- see `mintMigrationBlueprintName`.
486
+ const { name, path } = mintMigrationBlueprintName(actor.id, knownBlueprints);
416
487
  const text = formatBlueprintFileText(name, templateComponents);
417
488
  newBlueprintFiles.push({ path, text });
418
489
  knownBlueprints = [...knownBlueprints, { path, name, components: templateComponents }];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.81",
3
+ "version": "0.4.82",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"
@@ -1,3 +0,0 @@
1
- allowBuilds:
2
- '@fortawesome/fontawesome-common-types': set this to true or false
3
- '@fortawesome/free-solid-svg-icons': set this to true or false