@waica/cli 0.6.1 → 0.8.0

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.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/dist/editor/assets/{cssMode-D3SZSKEA.js → cssMode-DwPoqXFO.js} +1 -1
  3. package/dist/editor/assets/{freemarker2-BH1pa3TU.js → freemarker2-gl5y25bH.js} +1 -1
  4. package/dist/editor/assets/{handlebars-Cf7meNHo.js → handlebars-DHUb8mb3.js} +1 -1
  5. package/dist/editor/assets/{html-6hqYzl5P.js → html-C8W-psr4.js} +1 -1
  6. package/dist/editor/assets/{htmlMode-D_IiyUdi.js → htmlMode-BiF5TH2_.js} +1 -1
  7. package/dist/editor/assets/{index-Cy5pmBhN.css → index-CzarIYLW.css} +1 -1
  8. package/dist/editor/assets/{index-CJyB1cBF.js → index-DtMwWp8R.js} +6380 -2495
  9. package/dist/editor/assets/{javascript-CTeTqLj8.js → javascript-D_t2jyWi.js} +1 -1
  10. package/dist/editor/assets/{jsonMode-t8AC-38H.js → jsonMode-CZ0SltDj.js} +1 -1
  11. package/dist/editor/assets/{liquid-Dc6NoKas.js → liquid-DUxURZui.js} +1 -1
  12. package/dist/editor/assets/{mdx-DCPtX-bs.js → mdx-CDIQ7hmo.js} +1 -1
  13. package/dist/editor/assets/{python-DXAju_oJ.js → python-Cra2N916.js} +1 -1
  14. package/dist/editor/assets/{razor-DqQldpv6.js → razor-B3qV-mNp.js} +1 -1
  15. package/dist/editor/assets/{tsMode-B60EhHlZ.js → tsMode-Ci457z4v.js} +1 -1
  16. package/dist/editor/assets/{typescript-C4LyYAns.js → typescript-BXjh3GLT.js} +1 -1
  17. package/dist/editor/assets/{xml-B0QcT5_Z.js → xml-DQuEpqk5.js} +1 -1
  18. package/dist/editor/assets/{yaml-CEpyO2Qc.js → yaml-CQA7WIT-.js} +1 -1
  19. package/dist/editor/index.html +2 -2
  20. package/dist/mcp/archetypes.js +6 -2
  21. package/dist/mcp/create-project.js +12 -6
  22. package/dist/mcp/known-archetypes.js +24 -0
  23. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/ATTRIBUTION.md +72 -0
  24. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-crate.png +0 -0
  25. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-ground.png +0 -0
  26. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-hero.png +0 -0
  27. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-orc.png +0 -0
  28. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-rock.png +0 -0
  29. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-tree.png +0 -0
  30. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-villager.png +0 -0
  31. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/animation.d.ts +3 -0
  32. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/animation.js +10 -0
  33. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/art.d.ts +3 -0
  34. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/art.js +10 -0
  35. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/bundle.d.ts +2 -0
  36. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/bundle.js +9 -0
  37. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/controls.d.ts +3 -0
  38. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/controls.js +14 -0
  39. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/index.d.ts +24 -0
  40. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/index.js +15 -0
  41. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/manifest.d.ts +16 -0
  42. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/manifest.js +29 -0
  43. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/prefabs.d.ts +18 -0
  44. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/prefabs.js +196 -0
  45. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry-data.d.ts +3 -0
  46. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry-data.js +46 -0
  47. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry.d.ts +4 -0
  48. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry.js +17 -0
  49. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/scene-default.d.ts +5 -0
  50. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/scene-default.js +49 -0
  51. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/ui.d.ts +1 -0
  52. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/ui.js +17 -0
  53. package/dist/mcp/node_modules/@waica/archetype-isometric/package.json +30 -0
  54. package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
  55. package/dist/mcp/node_modules/@waica/archetype-topdown/assets/ATTRIBUTION.md +28 -0
  56. package/dist/mcp/node_modules/@waica/archetype-topdown/assets/waica-blob.png +0 -0
  57. package/dist/mcp/node_modules/@waica/archetype-topdown/assets/waica-fence.png +0 -0
  58. package/dist/mcp/node_modules/@waica/archetype-topdown/assets/waica-grass.png +0 -0
  59. package/dist/mcp/node_modules/@waica/archetype-topdown/assets/waica-hero.png +0 -0
  60. package/dist/mcp/node_modules/@waica/archetype-topdown/assets/waica-npc.png +0 -0
  61. package/dist/mcp/node_modules/@waica/archetype-topdown/assets/waica-path.png +0 -0
  62. package/dist/mcp/node_modules/@waica/archetype-topdown/assets/waica-potion.png +0 -0
  63. package/dist/mcp/node_modules/@waica/archetype-topdown/assets/waica-tree.png +0 -0
  64. package/dist/mcp/node_modules/@waica/archetype-topdown/assets/waica-water.png +0 -0
  65. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/animation.d.ts +7 -0
  66. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/animation.js +10 -0
  67. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/art.d.ts +10 -0
  68. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/art.js +19 -0
  69. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/bundle.d.ts +3 -0
  70. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/bundle.js +10 -0
  71. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/controls.d.ts +5 -0
  72. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/controls.js +16 -0
  73. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/index.d.ts +25 -0
  74. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/index.js +16 -0
  75. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/manifest.d.ts +16 -0
  76. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/manifest.js +29 -0
  77. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/prefabs.d.ts +11 -0
  78. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/prefabs.js +178 -0
  79. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/registry-data.d.ts +9 -0
  80. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/registry-data.js +55 -0
  81. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/registry.d.ts +6 -0
  82. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/registry.js +21 -0
  83. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/scene-default.d.ts +57 -0
  84. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/scene-default.js +110 -0
  85. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/ui.d.ts +10 -0
  86. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/ui.js +26 -0
  87. package/dist/mcp/node_modules/@waica/archetype-topdown/package.json +30 -0
  88. package/dist/mcp/node_modules/@waica/behaviors/dist/grid-motor.d.ts +56 -0
  89. package/dist/mcp/node_modules/@waica/behaviors/dist/grid-motor.js +74 -0
  90. package/dist/mcp/node_modules/@waica/behaviors/dist/grid-player-role.d.ts +15 -0
  91. package/dist/mcp/node_modules/@waica/behaviors/dist/grid-player-role.js +71 -0
  92. package/dist/mcp/node_modules/@waica/behaviors/dist/index.d.ts +5 -0
  93. package/dist/mcp/node_modules/@waica/behaviors/dist/index.js +5 -0
  94. package/dist/mcp/node_modules/@waica/behaviors/dist/interactable.d.ts +34 -0
  95. package/dist/mcp/node_modules/@waica/behaviors/dist/interactable.js +72 -0
  96. package/dist/mcp/node_modules/@waica/behaviors/dist/iso-motor.d.ts +14 -0
  97. package/dist/mcp/node_modules/@waica/behaviors/dist/iso-motor.js +36 -0
  98. package/dist/mcp/node_modules/@waica/behaviors/dist/iso-player-states.d.ts +5 -0
  99. package/dist/mcp/node_modules/@waica/behaviors/dist/iso-player-states.js +9 -0
  100. package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.d.ts +4 -2
  101. package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.js +5 -1
  102. package/dist/mcp/node_modules/@waica/behaviors/dist/topdown-motor.d.ts +18 -0
  103. package/dist/mcp/node_modules/@waica/behaviors/dist/topdown-motor.js +31 -0
  104. package/dist/mcp/node_modules/@waica/behaviors/dist/topdown-player-states.d.ts +5 -0
  105. package/dist/mcp/node_modules/@waica/behaviors/dist/topdown-player-states.js +14 -0
  106. package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
  107. package/dist/mcp/node_modules/@waica/engine/dist/animation/directional.d.ts +48 -0
  108. package/dist/mcp/node_modules/@waica/engine/dist/animation/directional.js +68 -0
  109. package/dist/mcp/node_modules/@waica/engine/dist/archetype.d.ts +3 -0
  110. package/dist/mcp/node_modules/@waica/engine/dist/camera.d.ts +19 -0
  111. package/dist/mcp/node_modules/@waica/engine/dist/camera.js +10 -0
  112. package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +2 -0
  113. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +27 -2
  114. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +50 -5
  115. package/dist/mcp/node_modules/@waica/engine/dist/components/dynamic-body.js +2 -4
  116. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.d.ts +23 -1
  117. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.js +49 -8
  118. package/dist/mcp/node_modules/@waica/engine/dist/components/tilemap.d.ts +113 -0
  119. package/dist/mcp/node_modules/@waica/engine/dist/components/tilemap.js +316 -0
  120. package/dist/mcp/node_modules/@waica/engine/dist/entity.d.ts +3 -0
  121. package/dist/mcp/node_modules/@waica/engine/dist/entity.js +13 -1
  122. package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +13 -1
  123. package/dist/mcp/node_modules/@waica/engine/dist/game.js +70 -9
  124. package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +16 -3
  125. package/dist/mcp/node_modules/@waica/engine/dist/index.js +8 -1
  126. package/dist/mcp/node_modules/@waica/engine/dist/projection.d.ts +14 -0
  127. package/dist/mcp/node_modules/@waica/engine/dist/projection.js +30 -0
  128. package/dist/mcp/node_modules/@waica/engine/dist/render-sort.d.ts +31 -0
  129. package/dist/mcp/node_modules/@waica/engine/dist/render-sort.js +39 -0
  130. package/dist/mcp/node_modules/@waica/engine/dist/scene-solids.d.ts +16 -0
  131. package/dist/mcp/node_modules/@waica/engine/dist/scene-solids.js +28 -0
  132. package/dist/mcp/node_modules/@waica/engine/dist/scene.d.ts +12 -0
  133. package/dist/mcp/node_modules/@waica/engine/dist/scene.js +1 -0
  134. package/dist/mcp/node_modules/@waica/engine/dist/solid-axis.js +2 -4
  135. package/dist/mcp/node_modules/@waica/engine/dist/sprite-placement.d.ts +22 -0
  136. package/dist/mcp/node_modules/@waica/engine/dist/sprite-placement.js +14 -0
  137. package/dist/mcp/node_modules/@waica/engine/dist/state/hooks.d.ts +1 -1
  138. package/dist/mcp/node_modules/@waica/engine/dist/state/hooks.js +3 -1
  139. package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.d.ts +18 -0
  140. package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.js +57 -1
  141. package/dist/mcp/node_modules/@waica/engine/dist/tilemap-grid.d.ts +26 -0
  142. package/dist/mcp/node_modules/@waica/engine/dist/tilemap-grid.js +41 -0
  143. package/dist/mcp/node_modules/@waica/engine/package.json +1 -1
  144. package/dist/mcp/package-resolver.js +14 -4
  145. package/dist/mcp/project-component-loader.js +2 -1
  146. package/dist/mcp/project-component-runner.js +5 -0
  147. package/dist/mcp/server.js +18 -1
  148. package/dist/mcp/template/package.json.tpl +1 -1
  149. package/dist/mcp/template/src/main.ts +8 -4
  150. package/dist/mcp/workspace-runtime.js +41 -10
  151. package/package.json +1 -1
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Y-sort draw ordering: an opt-in render mode (scene JSON `render.sort: 'y'`)
3
+ * for top-down scenes where "lower on screen" means "closer to the camera".
4
+ */
5
+ export interface YSortEntry {
6
+ /** The sprite's layer — the primary draw-order band, exactly as without y-sort. */
7
+ layer: number;
8
+ /** The owning entity's world Y — the sort key. Sprite offsets don't shift it. */
9
+ y: number;
10
+ }
11
+ /**
12
+ * The explicit seam a component opts into to participate in y-sort: it
13
+ * exposes its draw-order layer and accepts the per-frame z the pass derives.
14
+ * Both stock sprite classes implement it; a custom renderable can too.
15
+ */
16
+ export interface YSortParticipant {
17
+ readonly layer: number;
18
+ /** Y-sort pass hook: overrides the layer-derived z for this frame. */
19
+ setSortZ(z: number): void;
20
+ }
21
+ export declare function isYSortParticipant(value: unknown): value is YSortParticipant;
22
+ /**
23
+ * Z per entry under y-sort. Each layer keeps its 0.01 band; within a band,
24
+ * lower Y gets a higher z (renders in front), and exact Y ties keep input
25
+ * order. Offsets stay strictly inside (layer, layer + 1) × 0.01 for integer
26
+ * layers — but a fractional layer (e.g. 0.5) can sit closer than that to the
27
+ * next one present, so each band is capped at the gap to the next distinct
28
+ * layer above it, never wider than 0.01. Integer layers are always >= 1
29
+ * apart, so their band is exactly the old fixed 0.01.
30
+ */
31
+ export declare function ySortZ(entries: readonly YSortEntry[]): number[];
@@ -0,0 +1,39 @@
1
+ export function isYSortParticipant(value) {
2
+ return (typeof value === 'object' &&
3
+ value !== null &&
4
+ typeof value.layer === 'number' &&
5
+ typeof value.setSortZ === 'function');
6
+ }
7
+ /**
8
+ * Z per entry under y-sort. Each layer keeps its 0.01 band; within a band,
9
+ * lower Y gets a higher z (renders in front), and exact Y ties keep input
10
+ * order. Offsets stay strictly inside (layer, layer + 1) × 0.01 for integer
11
+ * layers — but a fractional layer (e.g. 0.5) can sit closer than that to the
12
+ * next one present, so each band is capped at the gap to the next distinct
13
+ * layer above it, never wider than 0.01. Integer layers are always >= 1
14
+ * apart, so their band is exactly the old fixed 0.01.
15
+ */
16
+ export function ySortZ(entries) {
17
+ const byLayer = new Map();
18
+ for (const [index, entry] of entries.entries()) {
19
+ const group = byLayer.get(entry.layer);
20
+ if (group)
21
+ group.push(index);
22
+ else
23
+ byLayer.set(entry.layer, [index]);
24
+ }
25
+ const layers = [...byLayer.keys()].sort((a, b) => a - b);
26
+ const z = new Array(entries.length);
27
+ for (const [i, layer] of layers.entries()) {
28
+ const indices = byLayer.get(layer);
29
+ const next = layers[i + 1];
30
+ const width = next === undefined ? 0.01 : Math.min(0.01, (next - layer) * 0.01);
31
+ // Stable sort: back-to-front is descending Y, ties keep input order.
32
+ const ordered = [...indices].sort((a, b) => entries[b].y - entries[a].y);
33
+ const step = width / (ordered.length + 1);
34
+ for (const [rank, index] of ordered.entries()) {
35
+ z[index] = layer * 0.01 + (rank + 1) * step;
36
+ }
37
+ }
38
+ return z;
39
+ }
@@ -0,0 +1,16 @@
1
+ import { Solid } from './components/solid.js';
2
+ import type { Entity } from './entity.js';
3
+ import type { Game } from './game.js';
4
+ /** Explicit opt-in brand for components that derive collision Solids. */
5
+ export declare const SOLID_SOURCE_SYMBOL: unique symbol;
6
+ /** Component seam for geometry that derives one or more collision Solids. */
7
+ export interface SolidSource {
8
+ readonly [SOLID_SOURCE_SYMBOL]: true;
9
+ solids(): readonly Solid[];
10
+ }
11
+ export declare function isSolidSource(value: unknown): value is SolidSource;
12
+ /**
13
+ * All static collision geometry in stable entity/component order. Without a
14
+ * SolidSource this is exactly one direct Solid per entity, as before.
15
+ */
16
+ export declare function sceneSolids(game: Game, except?: Entity): Solid[];
@@ -0,0 +1,28 @@
1
+ import { Solid } from './components/solid.js';
2
+ /** Explicit opt-in brand for components that derive collision Solids. */
3
+ export const SOLID_SOURCE_SYMBOL = Symbol('waica.solidSource');
4
+ export function isSolidSource(value) {
5
+ if (value === null || typeof value !== 'object')
6
+ return false;
7
+ const candidate = value;
8
+ return candidate[SOLID_SOURCE_SYMBOL] === true && typeof candidate.solids === 'function';
9
+ }
10
+ /**
11
+ * All static collision geometry in stable entity/component order. Without a
12
+ * SolidSource this is exactly one direct Solid per entity, as before.
13
+ */
14
+ export function sceneSolids(game, except) {
15
+ const result = [];
16
+ for (const entity of game.entities) {
17
+ if (entity === except)
18
+ continue;
19
+ const direct = entity.get(Solid);
20
+ if (direct)
21
+ result.push(direct);
22
+ for (const component of entity.components ?? []) {
23
+ if (isSolidSource(component))
24
+ result.push(...component.solids());
25
+ }
26
+ }
27
+ return result;
28
+ }
@@ -28,10 +28,22 @@ export interface PrefabJson {
28
28
  type: 'character' | 'object' | 'tile';
29
29
  components: SceneComponentJson[];
30
30
  }
31
+ /** Scene-wide render options (v3). */
32
+ export interface SceneRenderJson {
33
+ /**
34
+ * 'y' orders same-layer sprites by their render-space Y — lower Y renders
35
+ * in front (top-down depth). Absent: spawn order breaks same-layer ties.
36
+ */
37
+ sort?: 'y';
38
+ /** Logical positions are projected to the fixed 2:1 render lattice. */
39
+ projection?: 'isometric';
40
+ }
31
41
  export interface SceneJson {
32
42
  waicaScene: 1 | 2 | 3;
33
43
  /** The scene's built-in camera (v3); absent = the host keeps control. */
34
44
  camera?: SceneCameraJson;
45
+ /** Draw-order policy (v3); absent = layer bands with spawn-order ties. */
46
+ render?: SceneRenderJson;
35
47
  entities: SceneEntityJson[];
36
48
  /** UI pieces (src/ui/*.html) mounted visible when the scene loads. */
37
49
  ui?: string[];
@@ -82,6 +82,7 @@ export function spawnFromJson(game, json, registry) {
82
82
  /** Loads a full scene into the game. */
83
83
  export function loadScene(game, scene, registry) {
84
84
  game.registry = registry;
85
+ game.setSceneRender(scene.render);
85
86
  for (const entityJson of scene.entities)
86
87
  spawnFromJson(game, entityJson, registry);
87
88
  // After the spawns: with a follow target, the camera starts centered on it.
@@ -1,5 +1,6 @@
1
1
  import { collisionBounds, collisionOverlap } from './collision-shape.js';
2
2
  import { Solid } from './components/solid.js';
3
+ import { sceneSolids } from './scene-solids.js';
3
4
  const CONTACT_TOLERANCE = 1e-7;
4
5
  function bodyFor(solid) {
5
6
  return {
@@ -19,10 +20,7 @@ function bodyFor(solid) {
19
20
  export function resolveSolidAxis({ entity, axis, previous, body, onContact, }) {
20
21
  const position = entity.position;
21
22
  const target = position[axis];
22
- const solids = entity.game.entities
23
- .filter((other) => other !== entity)
24
- .map((other) => other.get(Solid))
25
- .filter((solid) => solid !== undefined);
23
+ const solids = sceneSolids(entity.game, entity);
26
24
  position[axis] = previous;
27
25
  // Preserve the established spawn-inside-wall bail: pre-existing overlaps
28
26
  // are ignored for this move rather than becoming arbitrary push-out rules.
@@ -0,0 +1,22 @@
1
+ export interface SpritePlacementInput {
2
+ width: number;
3
+ height: number;
4
+ offsetX: number;
5
+ offsetY: number;
6
+ anchorX: number;
7
+ anchorY: number;
8
+ flipX: boolean;
9
+ frameScaleX: number;
10
+ frameScaleY: number;
11
+ }
12
+ export interface SpritePlacement {
13
+ x: number;
14
+ y: number;
15
+ scaleX: number;
16
+ scaleY: number;
17
+ }
18
+ /**
19
+ * Places a displayed frame inside its full-size sprite box. The declared
20
+ * anchor belongs to that box; smaller animation frames retain its floor.
21
+ */
22
+ export declare function spritePlacement(input: SpritePlacementInput): SpritePlacement;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Places a displayed frame inside its full-size sprite box. The declared
3
+ * anchor belongs to that box; smaller animation frames retain its floor.
4
+ */
5
+ export function spritePlacement(input) {
6
+ const boxX = input.offsetX + (0.5 - input.anchorX) * input.width;
7
+ const floorY = input.offsetY - input.anchorY * input.height;
8
+ return {
9
+ x: input.flipX ? -boxX : boxX,
10
+ y: floorY + (input.height * input.frameScaleY) / 2,
11
+ scaleX: input.width * input.frameScaleX * (input.flipX ? -1 : 1),
12
+ scaleY: input.height * input.frameScaleY,
13
+ };
14
+ }
@@ -63,7 +63,7 @@ export interface ArchetypeBundle {
63
63
  /** Extra named logic sets that are not themselves roles. */
64
64
  logicSets?: Readonly<Record<string, StateLogic>>;
65
65
  }
66
- /** Clears role definitions and every named logic set. */
66
+ /** Clears role definitions, every named logic set and the directional contract. */
67
67
  export declare function resetRegistries(): void;
68
68
  /**
69
69
  * Replaces the active registry contents with one archetype's complete bundle.
@@ -1,3 +1,4 @@
1
+ import { installDirectionalAnimation } from '../animation/directional.js';
1
2
  const sets = new Map();
2
3
  /**
3
4
  * Registers state code under a logic-set name. A role's name is also its
@@ -8,10 +9,11 @@ export function defineStates(name, states) {
8
9
  sets.set(name, { ...sets.get(name), ...states });
9
10
  }
10
11
  const roles = new Map();
11
- /** Clears role definitions and every named logic set. */
12
+ /** Clears role definitions, every named logic set and the directional contract. */
12
13
  export function resetRegistries() {
13
14
  roles.clear();
14
15
  sets.clear();
16
+ installDirectionalAnimation(null);
15
17
  }
16
18
  /**
17
19
  * Replaces the active registry contents with one archetype's complete bundle.
@@ -66,6 +66,8 @@ export declare class StateMachine extends Component {
66
66
  private readonly instanceHooks;
67
67
  private readonly signals;
68
68
  private readonly warnedClips;
69
+ /** Facing last used to resolve a directional clip — detects a turn mid-state. */
70
+ private lastFacing;
69
71
  onReady(): void;
70
72
  /** Adds instance-level hooks on top of the logic set — the escape hatch. */
71
73
  on(state: string, hooks: StateHooks): void;
@@ -81,4 +83,20 @@ export declare class StateMachine extends Component {
81
83
  private runCollision;
82
84
  private enter;
83
85
  private playClip;
86
+ /**
87
+ * Directional resolution: with an installed contract AND a sibling that
88
+ * reports facing, plays state × facing (mirroring included). Returns false
89
+ * to keep the name-based path — no contract, no facing, no playable clip.
90
+ */
91
+ private playDirectionalClip;
92
+ private facingProvider;
93
+ /**
94
+ * playDirectionalClip only runs at enter() time (via playClip), so a
95
+ * facing change that doesn't cross a state boundary — turning while
96
+ * still walking, say — never re-resolves the clip on its own. Catch that
97
+ * here every frame: same state, new facing, re-run the resolution.
98
+ * AnimatedSprite.play() no-ops on an unchanged clip name, so this is
99
+ * cheap when facing hasn't moved the resolved clip.
100
+ */
101
+ private reresolveOnFacingChange;
84
102
  }
@@ -1,3 +1,4 @@
1
+ import { installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from '../animation/directional.js';
1
2
  import { Component } from '../component.js';
2
3
  import { AnimatedSprite } from '../components/animated-sprite.js';
3
4
  import { closestLogicSet, logicSet, registeredLogicSets, } from './hooks.js';
@@ -47,7 +48,14 @@ export class StateMachine extends Component {
47
48
  static params = {
48
49
  role: { label: 'Role' },
49
50
  };
50
- static transient = ['current', 'elapsed', 'instanceHooks', 'signals', 'warnedClips'];
51
+ static transient = [
52
+ 'current',
53
+ 'elapsed',
54
+ 'instanceHooks',
55
+ 'signals',
56
+ 'warnedClips',
57
+ 'lastFacing',
58
+ ];
51
59
  /** The character's role — names the logic set providing its state code. */
52
60
  role = '';
53
61
  /** Starting state; defaults to the first declared state. */
@@ -60,6 +68,8 @@ export class StateMachine extends Component {
60
68
  instanceHooks = new Map();
61
69
  signals = new Set();
62
70
  warnedClips = new Set();
71
+ /** Facing last used to resolve a directional clip — detects a turn mid-state. */
72
+ lastFacing;
63
73
  onReady() {
64
74
  if (this.role && !logicSet(this.role)) {
65
75
  const sets = registeredLogicSets();
@@ -111,6 +121,7 @@ export class StateMachine extends Component {
111
121
  this.enter(edge.to);
112
122
  }
113
123
  this.signals.clear();
124
+ this.reresolveOnFacingChange();
114
125
  }
115
126
  onCollide(other) {
116
127
  // The state that was active when the contact happened owns it: a
@@ -167,6 +178,8 @@ export class StateMachine extends Component {
167
178
  if (!sprite)
168
179
  return;
169
180
  const clip = this.states[state]?.clip ?? state;
181
+ if (this.playDirectionalClip(sprite, clip))
182
+ return;
170
183
  if (sprite.clips[clip]) {
171
184
  sprite.play(clip);
172
185
  }
@@ -176,4 +189,47 @@ export class StateMachine extends Component {
176
189
  `keeping "${sprite.current ?? 'none'}"`);
177
190
  }
178
191
  }
192
+ /**
193
+ * Directional resolution: with an installed contract AND a sibling that
194
+ * reports facing, plays state × facing (mirroring included). Returns false
195
+ * to keep the name-based path — no contract, no facing, no playable clip.
196
+ */
197
+ playDirectionalClip(sprite, clip) {
198
+ const animation = installedDirectionalAnimation();
199
+ if (!animation)
200
+ return false;
201
+ const provider = this.facingProvider();
202
+ if (!provider)
203
+ return false;
204
+ const facing = provider.getAnimationFacing();
205
+ this.lastFacing = facing;
206
+ const resolved = resolveDirectionalClip(animation, Object.keys(sprite.clips), clip, facing);
207
+ if (!resolved.clip || !sprite.clips[resolved.clip])
208
+ return false;
209
+ sprite.setFlipX(resolved.flip);
210
+ sprite.play(resolved.clip);
211
+ return true;
212
+ }
213
+ facingProvider() {
214
+ return this.entity.components.find((c) => isAnimationFacingProvider(c));
215
+ }
216
+ /**
217
+ * playDirectionalClip only runs at enter() time (via playClip), so a
218
+ * facing change that doesn't cross a state boundary — turning while
219
+ * still walking, say — never re-resolves the clip on its own. Catch that
220
+ * here every frame: same state, new facing, re-run the resolution.
221
+ * AnimatedSprite.play() no-ops on an unchanged clip name, so this is
222
+ * cheap when facing hasn't moved the resolved clip.
223
+ */
224
+ reresolveOnFacingChange() {
225
+ if (!this.current || !installedDirectionalAnimation())
226
+ return;
227
+ const sprite = this.entity.get(AnimatedSprite);
228
+ if (!sprite)
229
+ return;
230
+ const provider = this.facingProvider();
231
+ if (!provider || provider.getAnimationFacing() === this.lastFacing)
232
+ return;
233
+ this.playDirectionalClip(sprite, this.states[this.current]?.clip ?? this.current);
234
+ }
179
235
  }
@@ -0,0 +1,26 @@
1
+ export interface TilemapGridSpec {
2
+ mapWidth: number;
3
+ mapHeight: number;
4
+ cellSize: number;
5
+ originX?: number;
6
+ originY?: number;
7
+ }
8
+ export interface TilemapCell {
9
+ column: number;
10
+ row: number;
11
+ index: number;
12
+ }
13
+ export interface TilemapCellBounds {
14
+ left: number;
15
+ right: number;
16
+ bottom: number;
17
+ top: number;
18
+ centerX: number;
19
+ centerY: number;
20
+ }
21
+ /** Row-major index for a map cell, or null outside a valid map. */
22
+ export declare function cellIndex(mapWidth: number, mapHeight: number, column: number, row: number): number | null;
23
+ /** Cell containing a logical point, relative to the Tilemap entity origin. */
24
+ export declare function cellAt(spec: TilemapGridSpec, logicalX: number, logicalY: number): TilemapCell | null;
25
+ /** Logical bounds and center of one map cell. */
26
+ export declare function cellBounds(spec: TilemapGridSpec, column: number, row: number): TilemapCellBounds | null;
@@ -0,0 +1,41 @@
1
+ function dimension(value) {
2
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
3
+ }
4
+ /** Row-major index for a map cell, or null outside a valid map. */
5
+ export function cellIndex(mapWidth, mapHeight, column, row) {
6
+ const width = dimension(mapWidth);
7
+ const height = dimension(mapHeight);
8
+ if (!Number.isInteger(column) || !Number.isInteger(row))
9
+ return null;
10
+ if (column < 0 || row < 0 || column >= width || row >= height)
11
+ return null;
12
+ return row * width + column;
13
+ }
14
+ /** Cell containing a logical point, relative to the Tilemap entity origin. */
15
+ export function cellAt(spec, logicalX, logicalY) {
16
+ const size = spec.cellSize;
17
+ if (!Number.isFinite(size) || size <= 0)
18
+ return null;
19
+ const column = Math.floor((logicalX - (spec.originX ?? 0)) / size);
20
+ const row = Math.floor((logicalY - (spec.originY ?? 0)) / size);
21
+ const index = cellIndex(spec.mapWidth, spec.mapHeight, column, row);
22
+ return index === null ? null : { column, row, index };
23
+ }
24
+ /** Logical bounds and center of one map cell. */
25
+ export function cellBounds(spec, column, row) {
26
+ if (cellIndex(spec.mapWidth, spec.mapHeight, column, row) === null)
27
+ return null;
28
+ const size = spec.cellSize;
29
+ if (!Number.isFinite(size) || size <= 0)
30
+ return null;
31
+ const left = (spec.originX ?? 0) + column * size;
32
+ const bottom = (spec.originY ?? 0) + row * size;
33
+ return {
34
+ left,
35
+ right: left + size,
36
+ bottom,
37
+ top: bottom + size,
38
+ centerX: left + size / 2,
39
+ centerY: bottom + size / 2,
40
+ };
41
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/engine",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Waica game engine core — archetype-driven, web-first, 2D & 3D",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,7 @@ import { access, readFile, realpath } from 'node:fs/promises';
2
2
  import { createRequire } from 'node:module';
3
3
  import path from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
+ import { KNOWN_ARCHETYPES } from './known-archetypes.js';
5
6
  import { workspacePackageRoot } from './workspace-runtime.js';
6
7
  /** A declared package that is absent from both the project and MCP bundle. */
7
8
  export class PackageUnavailableError extends Error {
@@ -15,8 +16,7 @@ export class PackageUnavailableError extends Error {
15
16
  const BUNDLED_SPECIFIERS = new Set([
16
17
  '@waica/engine',
17
18
  '@waica/behaviors',
18
- '@waica/archetype-platformer',
19
- '@waica/archetype-platformer/manifest',
19
+ ...KNOWN_ARCHETYPES.flatMap(({ packageName }) => [packageName, `${packageName}/manifest`]),
20
20
  ]);
21
21
  const requireBundled = createRequire(import.meta.url);
22
22
  /** Node resolution anchored at a game project rather than the MCP host process. */
@@ -26,8 +26,10 @@ export function projectAnchoredRequire(projectPath) {
26
26
  const BUILT_ENTRY_BY_SPECIFIER = {
27
27
  '@waica/engine': 'dist/index.js',
28
28
  '@waica/behaviors': 'dist/index.js',
29
- '@waica/archetype-platformer': 'dist/index.js',
30
- '@waica/archetype-platformer/manifest': 'dist/manifest.js',
29
+ ...Object.fromEntries(KNOWN_ARCHETYPES.flatMap(({ packageName }) => [
30
+ [packageName, 'dist/index.js'],
31
+ [`${packageName}/manifest`, 'dist/manifest.js'],
32
+ ])),
31
33
  };
32
34
  function specifierFor(packageName, subpath) {
33
35
  if (!subpath || subpath === '.')
@@ -127,6 +129,14 @@ async function loadBundledModule(specifier, packageRoot) {
127
129
  return import('@waica/archetype-platformer');
128
130
  case '@waica/archetype-platformer/manifest':
129
131
  return import('@waica/archetype-platformer/manifest');
132
+ case '@waica/archetype-topdown':
133
+ return import('@waica/archetype-topdown');
134
+ case '@waica/archetype-topdown/manifest':
135
+ return import('@waica/archetype-topdown/manifest');
136
+ case '@waica/archetype-isometric':
137
+ return import('@waica/archetype-isometric');
138
+ case '@waica/archetype-isometric/manifest':
139
+ return import('@waica/archetype-isometric/manifest');
130
140
  default:
131
141
  throw new PackageUnavailableError(specifier);
132
142
  }
@@ -7,6 +7,7 @@ import { tmpdir } from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { promisify } from 'node:util';
10
+ import { KNOWN_ARCHETYPES } from './known-archetypes.js';
10
11
  import { directFiles } from './project-path.js';
11
12
  export const PROJECT_COMPONENT_PROTOCOL_VERSION = 1;
12
13
  export const PROJECT_COMPONENT_DEADLINE_MS = 5_000;
@@ -16,7 +17,7 @@ const REF_KINDS = new Set(['prefab', 'clip', 'action', 'stat']);
16
17
  const FALLBACK_PACKAGE_DIRECTORIES = [
17
18
  ['@waica/engine', 'engine'],
18
19
  ['@waica/behaviors', 'behaviors'],
19
- ['@waica/archetype-platformer', 'archetype-platformer'],
20
+ ...KNOWN_ARCHETYPES.map(({ packageName, directory }) => [packageName, directory]),
20
21
  ];
21
22
  const execFileAsync = promisify(execFile);
22
23
  let sourceFallbackEntries;
@@ -5,10 +5,15 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  const PROTOCOL_VERSION = 1;
6
6
  const { createRequire } = nodeModule;
7
7
  const hostRequire = createRequire(import.meta.url);
8
+ // This file runs as a standalone forked child (see runnerFromModule in the
9
+ // loader) and cannot import siblings, so it mirrors known-archetypes.ts by
10
+ // hand: one entry per KNOWN_ARCHETYPES row, plus engine and behaviors.
8
11
  const FALLBACK_PACKAGES = new Set([
9
12
  '@waica/engine',
10
13
  '@waica/behaviors',
11
14
  '@waica/archetype-platformer',
15
+ '@waica/archetype-topdown',
16
+ '@waica/archetype-isometric',
12
17
  ]);
13
18
  const FALLBACK_SPECIFIERS = new Set([...FALLBACK_PACKAGES, 'three']);
14
19
  const REF_KINDS = new Set(['prefab', 'clip', 'action', 'stat']);
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
6
6
  import { createProject } from './create-project.js';
7
+ import { DEFAULT_ARCHETYPE_ID, knownArchetype, knownArchetypeIds, } from './known-archetypes.js';
7
8
  import { describeArchetype, listComponents, projectSummary, } from './introspection.js';
8
9
  import { ABSOLUTE_PATH_MESSAGE, WaicaToolError, assertAbsoluteProjectPath, requireWaicaProject, } from './project-path.js';
9
10
  import { scaffoldComponent, scaffoldPrefab, scaffoldRole, scaffoldState, scaffoldUi, } from './scaffolds.js';
@@ -34,6 +35,12 @@ export const TOOLS = [
34
35
  default: 'demo',
35
36
  description: 'demo includes archetype content; blank creates only the chassis.',
36
37
  },
38
+ archetype: {
39
+ type: 'string',
40
+ enum: knownArchetypeIds(),
41
+ default: DEFAULT_ARCHETYPE_ID,
42
+ description: 'Archetype the project is created for.',
43
+ },
37
44
  }),
38
45
  annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: false },
39
46
  },
@@ -352,7 +359,17 @@ async function execute(name, args, runtime, signal, componentLoader) {
352
359
  projectPath,
353
360
  });
354
361
  }
355
- return { ...(await createProject(projectPath, start)) };
362
+ const archetype = args.archetype === undefined
363
+ ? DEFAULT_ARCHETYPE_ID
364
+ : requiredString(args, 'archetype', projectPath);
365
+ if (!knownArchetype(archetype)) {
366
+ throw new WaicaToolError({
367
+ code: 'unknown-archetype',
368
+ message: `Unknown archetype "${archetype}"; available: ${knownArchetypeIds().join(', ')}.`,
369
+ projectPath,
370
+ });
371
+ }
372
+ return { ...(await createProject(projectPath, start, archetype)) };
356
373
  }
357
374
  case 'list_components':
358
375
  return listComponents(projectPath);
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@waica/archetype-platformer": "^__WAICA_VERSION__",
12
+ "__ARCHETYPE_PACKAGE__": "^__WAICA_VERSION__",
13
13
  "@waica/behaviors": "^__WAICA_VERSION__",
14
14
  "@waica/engine": "^__WAICA_VERSION__"
15
15
  },
@@ -2,11 +2,12 @@ import {
2
2
  collectModuleComponents,
3
3
  Game,
4
4
  installArchetype,
5
+ installDirectionalAnimation,
5
6
  loadScene,
6
7
  mergeRegistryComponents,
7
8
  type PrefabJson,
8
9
  } from '@waica/engine'
9
- import { PLATFORMER_BUNDLE, PLATFORMER_REGISTRY } from '@waica/archetype-platformer'
10
+ import { ARCHETYPE } from '__ARCHETYPE_PACKAGE__'
10
11
  import scene from './scenes/main.scene.json'
11
12
  import controls from './controls.json'
12
13
  import stats from './stats.json'
@@ -60,14 +61,14 @@ for (const [path, url] of Object.entries(artFiles)) {
60
61
  }
61
62
 
62
63
  const registryBase = {
63
- ...PLATFORMER_REGISTRY,
64
+ ...ARCHETYPE.registry,
64
65
  // Keep these two: the spread above carries the archetype's own catalogs,
65
66
  // and these replace them with yours. Drop one and the game starts
66
67
  // resolving pieces that are nowhere in src/.
67
68
  prefabs,
68
69
  ui,
69
70
  resolveAsset: (uri: string) =>
70
- artUrls[uri] ?? PLATFORMER_REGISTRY.resolveAsset?.(uri) ?? uri,
71
+ artUrls[uri] ?? ARCHETYPE.registry.resolveAsset?.(uri) ?? uri,
71
72
  }
72
73
 
73
74
  const canvas = document.querySelector<HTMLCanvasElement>('#game')
@@ -82,7 +83,10 @@ if (canvas.dataset.waica) {
82
83
  }
83
84
 
84
85
  async function main(canvas: HTMLCanvasElement): Promise<void> {
85
- installArchetype(PLATFORMER_BUNDLE)
86
+ installArchetype(ARCHETYPE.bundle)
87
+ // installArchetype resets the directional registry, so the contract goes
88
+ // right after it (archetypes without one install null — a no-op).
89
+ installDirectionalAnimation(ARCHETYPE.animation ?? null)
86
90
  const projectModules = await Promise.all(Object.values(projectCode).map((load) => load()))
87
91
  const registry = mergeRegistryComponents(
88
92
  registryBase,