@waica/cli 0.7.0 → 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 (100) hide show
  1. package/README.md +1 -1
  2. package/dist/editor/assets/{cssMode-eS4pAOyh.js → cssMode-DwPoqXFO.js} +1 -1
  3. package/dist/editor/assets/{freemarker2-FMQjZKQk.js → freemarker2-gl5y25bH.js} +1 -1
  4. package/dist/editor/assets/{handlebars-dDYOnqat.js → handlebars-DHUb8mb3.js} +1 -1
  5. package/dist/editor/assets/{html-Ba6xZh4T.js → html-C8W-psr4.js} +1 -1
  6. package/dist/editor/assets/{htmlMode-DC9qJr2p.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-CoyeInOT.js → index-DtMwWp8R.js} +2732 -608
  9. package/dist/editor/assets/{javascript-DOwFvmvM.js → javascript-D_t2jyWi.js} +1 -1
  10. package/dist/editor/assets/{jsonMode-CcG7zlSf.js → jsonMode-CZ0SltDj.js} +1 -1
  11. package/dist/editor/assets/{liquid-Dfp71r7J.js → liquid-DUxURZui.js} +1 -1
  12. package/dist/editor/assets/{mdx-Ci7vVeht.js → mdx-CDIQ7hmo.js} +1 -1
  13. package/dist/editor/assets/{python-Bluv3Y9Q.js → python-Cra2N916.js} +1 -1
  14. package/dist/editor/assets/{razor-BtwTXMaK.js → razor-B3qV-mNp.js} +1 -1
  15. package/dist/editor/assets/{tsMode-D3_D-H85.js → tsMode-Ci457z4v.js} +1 -1
  16. package/dist/editor/assets/{typescript-Ch_PS5f7.js → typescript-BXjh3GLT.js} +1 -1
  17. package/dist/editor/assets/{xml-CI_bH_MX.js → xml-DQuEpqk5.js} +1 -1
  18. package/dist/editor/assets/{yaml-LD9GTZyy.js → yaml-CQA7WIT-.js} +1 -1
  19. package/dist/editor/index.html +2 -2
  20. package/dist/mcp/known-archetypes.js +5 -0
  21. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/ATTRIBUTION.md +72 -0
  22. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-crate.png +0 -0
  23. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-ground.png +0 -0
  24. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-hero.png +0 -0
  25. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-orc.png +0 -0
  26. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-rock.png +0 -0
  27. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-tree.png +0 -0
  28. package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-villager.png +0 -0
  29. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/animation.d.ts +3 -0
  30. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/animation.js +10 -0
  31. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/art.d.ts +3 -0
  32. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/art.js +10 -0
  33. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/bundle.d.ts +2 -0
  34. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/bundle.js +9 -0
  35. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/controls.d.ts +3 -0
  36. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/controls.js +14 -0
  37. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/index.d.ts +24 -0
  38. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/index.js +15 -0
  39. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/manifest.d.ts +16 -0
  40. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/manifest.js +29 -0
  41. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/prefabs.d.ts +18 -0
  42. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/prefabs.js +196 -0
  43. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry-data.d.ts +3 -0
  44. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry-data.js +46 -0
  45. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry.d.ts +4 -0
  46. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry.js +17 -0
  47. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/scene-default.d.ts +5 -0
  48. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/scene-default.js +49 -0
  49. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/ui.d.ts +1 -0
  50. package/dist/mcp/node_modules/@waica/archetype-isometric/dist/ui.js +17 -0
  51. package/dist/mcp/node_modules/@waica/archetype-isometric/package.json +30 -0
  52. package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
  53. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/registry-data.js +2 -1
  54. package/dist/mcp/node_modules/@waica/archetype-topdown/dist/ui.js +2 -19
  55. package/dist/mcp/node_modules/@waica/archetype-topdown/package.json +3 -3
  56. package/dist/mcp/node_modules/@waica/behaviors/dist/grid-motor.d.ts +56 -0
  57. package/dist/mcp/node_modules/@waica/behaviors/dist/grid-motor.js +74 -0
  58. package/dist/mcp/node_modules/@waica/behaviors/dist/grid-player-role.d.ts +15 -0
  59. package/dist/mcp/node_modules/@waica/behaviors/dist/grid-player-role.js +71 -0
  60. package/dist/mcp/node_modules/@waica/behaviors/dist/index.d.ts +3 -1
  61. package/dist/mcp/node_modules/@waica/behaviors/dist/index.js +3 -1
  62. package/dist/mcp/node_modules/@waica/behaviors/dist/interactable.d.ts +3 -0
  63. package/dist/mcp/node_modules/@waica/behaviors/dist/interactable.js +25 -2
  64. package/dist/mcp/node_modules/@waica/behaviors/dist/iso-motor.d.ts +14 -0
  65. package/dist/mcp/node_modules/@waica/behaviors/dist/iso-motor.js +36 -0
  66. package/dist/mcp/node_modules/@waica/behaviors/dist/iso-player-states.d.ts +5 -0
  67. package/dist/mcp/node_modules/@waica/behaviors/dist/iso-player-states.js +9 -0
  68. package/dist/mcp/node_modules/@waica/behaviors/dist/topdown-motor.d.ts +7 -61
  69. package/dist/mcp/node_modules/@waica/behaviors/dist/topdown-motor.js +8 -79
  70. package/dist/mcp/node_modules/@waica/behaviors/dist/topdown-player-states.d.ts +4 -17
  71. package/dist/mcp/node_modules/@waica/behaviors/dist/topdown-player-states.js +10 -82
  72. package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
  73. package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +2 -0
  74. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +19 -1
  75. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +35 -7
  76. package/dist/mcp/node_modules/@waica/engine/dist/components/dynamic-body.js +2 -4
  77. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.d.ts +19 -0
  78. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.js +44 -8
  79. package/dist/mcp/node_modules/@waica/engine/dist/components/tilemap.d.ts +113 -0
  80. package/dist/mcp/node_modules/@waica/engine/dist/components/tilemap.js +316 -0
  81. package/dist/mcp/node_modules/@waica/engine/dist/entity.d.ts +3 -0
  82. package/dist/mcp/node_modules/@waica/engine/dist/entity.js +13 -1
  83. package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +4 -0
  84. package/dist/mcp/node_modules/@waica/engine/dist/game.js +41 -9
  85. package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +9 -0
  86. package/dist/mcp/node_modules/@waica/engine/dist/index.js +5 -0
  87. package/dist/mcp/node_modules/@waica/engine/dist/projection.d.ts +14 -0
  88. package/dist/mcp/node_modules/@waica/engine/dist/projection.js +30 -0
  89. package/dist/mcp/node_modules/@waica/engine/dist/scene-solids.d.ts +16 -0
  90. package/dist/mcp/node_modules/@waica/engine/dist/scene-solids.js +28 -0
  91. package/dist/mcp/node_modules/@waica/engine/dist/scene.d.ts +3 -1
  92. package/dist/mcp/node_modules/@waica/engine/dist/solid-axis.js +2 -4
  93. package/dist/mcp/node_modules/@waica/engine/dist/sprite-placement.d.ts +22 -0
  94. package/dist/mcp/node_modules/@waica/engine/dist/sprite-placement.js +14 -0
  95. package/dist/mcp/node_modules/@waica/engine/dist/tilemap-grid.d.ts +26 -0
  96. package/dist/mcp/node_modules/@waica/engine/dist/tilemap-grid.js +41 -0
  97. package/dist/mcp/node_modules/@waica/engine/package.json +1 -1
  98. package/dist/mcp/package-resolver.js +4 -0
  99. package/dist/mcp/project-component-runner.js +1 -0
  100. package/package.json +1 -1
@@ -0,0 +1,74 @@
1
+ import { Component, resolveSolidAxis, THREE, } from '@waica/engine';
2
+ /** Shared velocity, collision and provider behavior for square-grid motors. */
3
+ export class GridMotor extends Component {
4
+ static displayName = 'Motor';
5
+ static params = {
6
+ moveSpeed: { label: 'Speed', min: 1, max: 30, step: 0.5 },
7
+ acceleration: { label: 'Acceleration', min: 5, max: 200, step: 5 },
8
+ deceleration: { label: 'Deceleration', min: 5, max: 200, step: 5 },
9
+ walkThreshold: { label: 'Walk threshold', min: 0, max: 5, step: 0.1 },
10
+ };
11
+ static transient = ['vx', 'vy', 'facing'];
12
+ moveSpeed = 6;
13
+ acceleration = 60;
14
+ deceleration = 80;
15
+ /** Speed above this reads as walking (the idle ↔ walk edge). */
16
+ walkThreshold = 0.5;
17
+ /** The character's AABB hitbox — a footprint, shorter than the sprite. */
18
+ hitboxWidth = 0.9;
19
+ hitboxHeight = 0.6;
20
+ vx = 0;
21
+ vy = 0;
22
+ /** The scene camera reads follow velocity through this explicit seam. */
23
+ getCameraVelocity() {
24
+ return { vx: this.vx, vy: this.vy };
25
+ }
26
+ /** The StateMachine resolves directional clips through this explicit seam. */
27
+ getAnimationFacing() {
28
+ return this.facing;
29
+ }
30
+ /** Accelerates each axis toward an already-normalized logical direction. */
31
+ accelerateToward(x, y, dt) {
32
+ const rateX = x !== 0 ? this.acceleration : this.deceleration;
33
+ const rateY = y !== 0 ? this.acceleration : this.deceleration;
34
+ this.vx = THREE.MathUtils.damp(this.vx, x * this.moveSpeed, rateX / this.moveSpeed, dt);
35
+ this.vy = THREE.MathUtils.damp(this.vy, y * this.moveSpeed, rateY / this.moveSpeed, dt);
36
+ }
37
+ /** Live speed, for the idle ↔ walk edge. */
38
+ speed() {
39
+ return Math.hypot(this.vx, this.vy);
40
+ }
41
+ /** Integrates velocity and resolves each logical axis against Solids. */
42
+ step(dt) {
43
+ const position = this.entity.position;
44
+ const previousX = position.x;
45
+ position.x += this.vx * dt;
46
+ if (this.resolveAxis('x', previousX))
47
+ this.vx = 0;
48
+ const previousY = position.y;
49
+ position.y += this.vy * dt;
50
+ if (this.resolveAxis('y', previousY))
51
+ this.vy = 0;
52
+ }
53
+ halt() {
54
+ this.vx = 0;
55
+ this.vy = 0;
56
+ }
57
+ resolveAxis(axis, previous) {
58
+ return resolveSolidAxis({
59
+ entity: this.entity,
60
+ axis,
61
+ previous,
62
+ body: () => this.collisionBody(),
63
+ });
64
+ }
65
+ collisionBody() {
66
+ return {
67
+ x: this.entity.position.x,
68
+ y: this.entity.position.y,
69
+ width: this.hitboxWidth,
70
+ height: this.hitboxHeight,
71
+ shape: 'rectangle',
72
+ };
73
+ }
74
+ }
@@ -0,0 +1,15 @@
1
+ import { type Component, type ComponentClass, type RoleDefinition, type RoleGraph, type StateContext } from '@waica/engine';
2
+ interface PlayerMotor extends Component {
3
+ walkThreshold: number;
4
+ run(inputX: number, inputY: number, dt: number): void;
5
+ step(dt: number): void;
6
+ speed(): number;
7
+ }
8
+ interface GridPlayerRoleParts {
9
+ graph: RoleGraph;
10
+ update(ctx: StateContext, dt: number): void;
11
+ role: RoleDefinition;
12
+ }
13
+ /** Builds the common player graph and role around one genre-specific grid motor. */
14
+ export declare function createGridPlayerRole<T extends PlayerMotor>(Motor: ComponentClass<T>, description: string): GridPlayerRoleParts;
15
+ export {};
@@ -0,0 +1,71 @@
1
+ import {} from '@waica/engine';
2
+ import { Health } from './health.js';
3
+ import { interactUpdate } from './interactable.js';
4
+ import { Respawnable } from './respawnable.js';
5
+ /** Builds the common player graph and role around one genre-specific grid motor. */
6
+ export function createGridPlayerRole(Motor, description) {
7
+ const graph = {
8
+ initial: 'idle',
9
+ states: {
10
+ idle: {
11
+ transitions: [{ on: 'signal:move', to: 'walk' }],
12
+ },
13
+ walk: {
14
+ transitions: [{ on: 'signal:stop', to: 'idle' }],
15
+ },
16
+ dead: {
17
+ // Freeze on idle (resolved directionally) rather than name a death
18
+ // clip the stock sheets do not have and warn every frame.
19
+ clip: 'idle',
20
+ transitions: [{ on: 'timer:0.8', to: 'idle' }],
21
+ },
22
+ // Dying is the same from every state, so the edge lives on '*'. Its
23
+ // presence is also the contract Health checks before signalling: a graph
24
+ // without it gets its entity destroyed instead.
25
+ '*': { transitions: [{ on: 'signal:death', to: 'dead' }] },
26
+ },
27
+ };
28
+ const update = ({ entity, game, fsm }, dt) => {
29
+ const motor = entity.get(Motor);
30
+ if (!motor)
31
+ return;
32
+ motor.run(game.input.axis('left', 'right'), game.input.axis('down', 'up'), dt);
33
+ motor.step(dt);
34
+ fsm.signal(motor.speed() > motor.walkThreshold ? 'move' : 'stop');
35
+ };
36
+ const role = {
37
+ description,
38
+ driver: Motor.componentName,
39
+ graph,
40
+ signals: {
41
+ move: 'moving',
42
+ stop: 'standing still',
43
+ },
44
+ states: {
45
+ // Always-hook: the interact lookup must survive custom states like an
46
+ // attack, so it runs in every state.
47
+ '*': {
48
+ onUpdate(ctx) {
49
+ interactUpdate(ctx);
50
+ },
51
+ },
52
+ // Fallback: a custom state without its own onUpdate keeps the full body
53
+ // update — its file only has to say what makes it special.
54
+ default: { onUpdate: update },
55
+ idle: { onUpdate: update },
56
+ walk: { onUpdate: update },
57
+ // Coming back is what leaving death means, so it hangs off onExit: any
58
+ // other way out of this state (a project's own edge) revives too.
59
+ dead: {
60
+ // A state without its own onUpdate falls back to the role's default
61
+ // body update, so this no-op keeps the player still for the death beat.
62
+ onUpdate() { },
63
+ onExit({ entity }) {
64
+ entity.get(Respawnable)?.respawn();
65
+ entity.get(Health)?.heal(Infinity);
66
+ },
67
+ },
68
+ },
69
+ };
70
+ return { graph, update, role };
71
+ }
@@ -1,8 +1,10 @@
1
1
  export { PlatformerMotor } from './platformer-motor.js';
2
2
  export { PLAYER_ROLE, PLAYER_STATE_GRAPH, playerUpdate } from './player-states.js';
3
3
  export { TopDownMotor, type TopDownFacing } from './topdown-motor.js';
4
+ export { IsoMotor, type IsoFacing } from './iso-motor.js';
4
5
  export { TOPDOWN_PLAYER_ROLE, TOPDOWN_PLAYER_STATE_GRAPH, topdownPlayerUpdate, } from './topdown-player-states.js';
5
- export { Interactable, interactUpdate } from './interactable.js';
6
+ export { ISO_PLAYER_ROLE, ISO_PLAYER_STATE_GRAPH, isoPlayerUpdate, } from './iso-player-states.js';
7
+ export { INTERACTABLE_UI, INTERACTABLE_UI_PIECE, Interactable, interactUpdate, } from './interactable.js';
6
8
  export { Collectible } from './collectible.js';
7
9
  export { Patrol, PATROLLER_ROLE, PATROLLER_STATE_GRAPH, type PatrolAxis } from './patrol.js';
8
10
  export { Chaser, CHASER_ROLE, CHASER_STATE_GRAPH, type ChaserMode } from './chaser.js';
@@ -1,8 +1,10 @@
1
1
  export { PlatformerMotor } from './platformer-motor.js';
2
2
  export { PLAYER_ROLE, PLAYER_STATE_GRAPH, playerUpdate } from './player-states.js';
3
3
  export { TopDownMotor } from './topdown-motor.js';
4
+ export { IsoMotor } from './iso-motor.js';
4
5
  export { TOPDOWN_PLAYER_ROLE, TOPDOWN_PLAYER_STATE_GRAPH, topdownPlayerUpdate, } from './topdown-player-states.js';
5
- export { Interactable, interactUpdate } from './interactable.js';
6
+ export { ISO_PLAYER_ROLE, ISO_PLAYER_STATE_GRAPH, isoPlayerUpdate, } from './iso-player-states.js';
7
+ export { INTERACTABLE_UI, INTERACTABLE_UI_PIECE, Interactable, interactUpdate, } from './interactable.js';
6
8
  export { Collectible } from './collectible.js';
7
9
  export { Patrol, PATROLLER_ROLE, PATROLLER_STATE_GRAPH } from './patrol.js';
8
10
  export { Chaser, CHASER_ROLE, CHASER_STATE_GRAPH } from './chaser.js';
@@ -22,6 +22,9 @@ export declare class Interactable extends Component {
22
22
  line: string;
23
23
  radius: number;
24
24
  }
25
+ export declare const INTERACTABLE_UI_PIECE = "npc-line";
26
+ /** The UI fragment every archetype using Interactable must register. */
27
+ export declare const INTERACTABLE_UI: Readonly<Record<string, string>>;
25
28
  /**
26
29
  * The player role's interact lookup, run by its '*' hook in every state:
27
30
  * pressing interact near an Interactable publishes its line through the
@@ -15,6 +15,29 @@ export class Interactable extends Component {
15
15
  line = 'Hello, traveler!';
16
16
  radius = 1.5;
17
17
  }
18
+ export const INTERACTABLE_UI_PIECE = 'npc-line';
19
+ /** The UI fragment every archetype using Interactable must register. */
20
+ export const INTERACTABLE_UI = {
21
+ [INTERACTABLE_UI_PIECE]: `<style>
22
+ .npc-line {
23
+ position: absolute;
24
+ left: 50%;
25
+ bottom: 24px;
26
+ transform: translateX(-50%);
27
+ max-width: 70%;
28
+ padding: 10px 18px;
29
+ border-radius: 8px;
30
+ background: #1a1a2ecc;
31
+ border: 1px solid #ffffff2e;
32
+ font: 500 18px system-ui, sans-serif;
33
+ color: #f5f5f5;
34
+ text-shadow: 0 1px 2px #000a;
35
+ user-select: none;
36
+ }
37
+ </style>
38
+ <div class="npc-line">{{npcLine}}</div>
39
+ `,
40
+ };
18
41
  /**
19
42
  * The player role's interact lookup, run by its '*' hook in every state:
20
43
  * pressing interact near an Interactable publishes its line through the
@@ -37,13 +60,13 @@ export function interactUpdate({ entity, game }) {
37
60
  }
38
61
  }
39
62
  if (!nearest) {
40
- game.ui.hide('npc-line');
63
+ game.ui.hide(INTERACTABLE_UI_PIECE);
41
64
  return;
42
65
  }
43
66
  if (game.input.justPressed('interact') && !game.input.consumed('interact')) {
44
67
  // The press is spent: an input:interact edge needs a NEW press.
45
68
  game.input.consume('interact');
46
69
  game.stats.set('npcLine', nearest.line);
47
- game.ui.show('npc-line');
70
+ game.ui.show(INTERACTABLE_UI_PIECE);
48
71
  }
49
72
  }
@@ -0,0 +1,14 @@
1
+ import { GridMotor } from './grid-motor.js';
2
+ /** The eight screen-relative facings declared by the isometric archetype. */
3
+ export type IsoFacing = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
4
+ /**
5
+ * Passive isometric motor. Input and facing are screen-relative while the
6
+ * shared GridMotor integrates and collides in logical square-grid space.
7
+ * State code owns the frame; this component has no onUpdate of its own.
8
+ */
9
+ export declare class IsoMotor extends GridMotor<IsoFacing> {
10
+ static componentName: string;
11
+ facing: IsoFacing;
12
+ /** Accelerates screen-relative input toward a logical-space velocity. */
13
+ run(inputX: number, inputY: number, dt: number): void;
14
+ }
@@ -0,0 +1,36 @@
1
+ import { screenInputToLogical } from '@waica/engine';
2
+ import { GridMotor } from './grid-motor.js';
3
+ function facingForInput(inputX, inputY) {
4
+ if (inputX === 0 && inputY === 0)
5
+ return undefined;
6
+ if (inputY > 0) {
7
+ if (inputX > 0)
8
+ return 'ne';
9
+ if (inputX < 0)
10
+ return 'nw';
11
+ return 'n';
12
+ }
13
+ if (inputY < 0) {
14
+ if (inputX > 0)
15
+ return 'se';
16
+ if (inputX < 0)
17
+ return 'sw';
18
+ return 's';
19
+ }
20
+ return inputX > 0 ? 'e' : 'w';
21
+ }
22
+ /**
23
+ * Passive isometric motor. Input and facing are screen-relative while the
24
+ * shared GridMotor integrates and collides in logical square-grid space.
25
+ * State code owns the frame; this component has no onUpdate of its own.
26
+ */
27
+ export class IsoMotor extends GridMotor {
28
+ static componentName = 'IsoMotor';
29
+ facing = 's';
30
+ /** Accelerates screen-relative input toward a logical-space velocity. */
31
+ run(inputX, inputY, dt) {
32
+ const direction = screenInputToLogical(inputX, inputY);
33
+ this.accelerateToward(direction.x, direction.y, dt);
34
+ this.facing = facingForInput(inputX, inputY) ?? this.facing;
35
+ }
36
+ }
@@ -0,0 +1,5 @@
1
+ /** The state graph new isometric player characters start with, as prefab data. */
2
+ export declare const ISO_PLAYER_STATE_GRAPH: import("@waica/engine").RoleGraph;
3
+ /** Drives IsoMotor from screen-relative actions and reports body state. */
4
+ export declare const isoPlayerUpdate: (ctx: import("@waica/engine").StateContext, dt: number) => void;
5
+ export declare const ISO_PLAYER_ROLE: import("@waica/engine").RoleDefinition;
@@ -0,0 +1,9 @@
1
+ import { createGridPlayerRole } from './grid-player-role.js';
2
+ import { IsoMotor } from './iso-motor.js';
3
+ const isoPlayer = createGridPlayerRole(IsoMotor, 'You control this character with screen-relative eight-direction movement ' +
4
+ 'projected into an isometric logical world. Its states move the Motor.');
5
+ /** The state graph new isometric player characters start with, as prefab data. */
6
+ export const ISO_PLAYER_STATE_GRAPH = isoPlayer.graph;
7
+ /** Drives IsoMotor from screen-relative actions and reports body state. */
8
+ export const isoPlayerUpdate = isoPlayer.update;
9
+ export const ISO_PLAYER_ROLE = isoPlayer.role;
@@ -1,72 +1,18 @@
1
- import { Component, type AnimationFacingProvider, type CameraVelocity, type CameraVelocityProvider } from '@waica/engine';
1
+ import { GridMotor } from './grid-motor.js';
2
2
  /** The four facings the top-down animation contract declares. */
3
3
  export type TopDownFacing = 'n' | 's' | 'e' | 'w';
4
4
  /**
5
- * Passive top-down motor: tuning params, physical state and movement
6
- * methods for state code to call. It has no onUpdate of its own — the
7
- * StateMachine is the only owner of the frame; this is the toolbox the
8
- * active state moves the body with. Eight-direction movement with the
9
- * input vector normalized so diagonals match cardinal speed, no gravity,
10
- * and per-axis collision against the scene's Solids — deterministic
11
- * genre movement (Zelda-style), not "realistic" physics.
5
+ * Passive top-down motor: state code owns the frame and calls run() then
6
+ * step(). Eight-direction input is normalized so diagonals match cardinal
7
+ * speed; movement has no gravity and resolves each axis against Solids.
12
8
  */
13
- export declare class TopDownMotor extends Component implements CameraVelocityProvider, AnimationFacingProvider {
9
+ export declare class TopDownMotor extends GridMotor<TopDownFacing> {
14
10
  static componentName: string;
15
- static displayName: string;
16
- static params: {
17
- moveSpeed: {
18
- label: string;
19
- min: number;
20
- max: number;
21
- step: number;
22
- };
23
- acceleration: {
24
- label: string;
25
- min: number;
26
- max: number;
27
- step: number;
28
- };
29
- deceleration: {
30
- label: string;
31
- min: number;
32
- max: number;
33
- step: number;
34
- };
35
- walkThreshold: {
36
- label: string;
37
- min: number;
38
- max: number;
39
- step: number;
40
- };
41
- };
42
- static transient: string[];
43
- moveSpeed: number;
44
- acceleration: number;
45
- deceleration: number;
46
- /** Speed above this reads as walking (the idle ↔ walk edge). */
47
- walkThreshold: number;
48
- /** The character's AABB hitbox — a footprint, shorter than the sprite. */
49
- hitboxWidth: number;
50
- hitboxHeight: number;
51
- vx: number;
52
- vy: number;
53
11
  /** Four-direction facing; starts looking at the camera. */
54
12
  facing: TopDownFacing;
55
- /** The scene camera reads follow velocity through this explicit seam. */
56
- getCameraVelocity(): CameraVelocity;
57
- /** The StateMachine resolves directional clips through this explicit seam. */
58
- getAnimationFacing(): string;
59
13
  /**
60
- * Accelerates toward the input vector (each axis -1..1) * moveSpeed.
61
- * The vector is normalized so diagonals move at cardinal speed, and
62
- * facing follows the dominant input axis — ties keep the last facing.
14
+ * Accelerates toward the input vector and lets the dominant raw input axis
15
+ * select facing. Perfect ties preserve the previous facing.
63
16
  */
64
17
  run(inputX: number, inputY: number, dt: number): void;
65
- /** Live speed, for the idle ↔ walk edge. */
66
- speed(): number;
67
- /** Integrates velocity and resolves each axis against Solids. */
68
- step(dt: number): void;
69
- halt(): void;
70
- private resolveAxis;
71
- private collisionBody;
72
18
  }
@@ -1,47 +1,16 @@
1
- import { Component, resolveSolidAxis, THREE, } from '@waica/engine';
1
+ import { GridMotor } from './grid-motor.js';
2
2
  /**
3
- * Passive top-down motor: tuning params, physical state and movement
4
- * methods for state code to call. It has no onUpdate of its own — the
5
- * StateMachine is the only owner of the frame; this is the toolbox the
6
- * active state moves the body with. Eight-direction movement with the
7
- * input vector normalized so diagonals match cardinal speed, no gravity,
8
- * and per-axis collision against the scene's Solids — deterministic
9
- * genre movement (Zelda-style), not "realistic" physics.
3
+ * Passive top-down motor: state code owns the frame and calls run() then
4
+ * step(). Eight-direction input is normalized so diagonals match cardinal
5
+ * speed; movement has no gravity and resolves each axis against Solids.
10
6
  */
11
- export class TopDownMotor extends Component {
7
+ export class TopDownMotor extends GridMotor {
12
8
  static componentName = 'TopDownMotor';
13
- static displayName = 'Motor';
14
- static params = {
15
- moveSpeed: { label: 'Speed', min: 1, max: 30, step: 0.5 },
16
- acceleration: { label: 'Acceleration', min: 5, max: 200, step: 5 },
17
- deceleration: { label: 'Deceleration', min: 5, max: 200, step: 5 },
18
- walkThreshold: { label: 'Walk threshold', min: 0, max: 5, step: 0.1 },
19
- };
20
- static transient = ['vx', 'vy', 'facing'];
21
- moveSpeed = 6;
22
- acceleration = 60;
23
- deceleration = 80;
24
- /** Speed above this reads as walking (the idle ↔ walk edge). */
25
- walkThreshold = 0.5;
26
- /** The character's AABB hitbox — a footprint, shorter than the sprite. */
27
- hitboxWidth = 0.9;
28
- hitboxHeight = 0.6;
29
- vx = 0;
30
- vy = 0;
31
9
  /** Four-direction facing; starts looking at the camera. */
32
10
  facing = 's';
33
- /** The scene camera reads follow velocity through this explicit seam. */
34
- getCameraVelocity() {
35
- return { vx: this.vx, vy: this.vy };
36
- }
37
- /** The StateMachine resolves directional clips through this explicit seam. */
38
- getAnimationFacing() {
39
- return this.facing;
40
- }
41
11
  /**
42
- * Accelerates toward the input vector (each axis -1..1) * moveSpeed.
43
- * The vector is normalized so diagonals move at cardinal speed, and
44
- * facing follows the dominant input axis — ties keep the last facing.
12
+ * Accelerates toward the input vector and lets the dominant raw input axis
13
+ * select facing. Perfect ties preserve the previous facing.
45
14
  */
46
15
  run(inputX, inputY, dt) {
47
16
  let x = inputX;
@@ -51,10 +20,7 @@ export class TopDownMotor extends Component {
51
20
  x /= length;
52
21
  y /= length;
53
22
  }
54
- const rateX = x !== 0 ? this.acceleration : this.deceleration;
55
- const rateY = y !== 0 ? this.acceleration : this.deceleration;
56
- this.vx = THREE.MathUtils.damp(this.vx, x * this.moveSpeed, rateX / this.moveSpeed, dt);
57
- this.vy = THREE.MathUtils.damp(this.vy, y * this.moveSpeed, rateY / this.moveSpeed, dt);
23
+ this.accelerateToward(x, y, dt);
58
24
  const absX = Math.abs(inputX);
59
25
  const absY = Math.abs(inputY);
60
26
  if (absX > absY)
@@ -62,41 +28,4 @@ export class TopDownMotor extends Component {
62
28
  else if (absY > absX)
63
29
  this.facing = inputY < 0 ? 's' : 'n';
64
30
  }
65
- /** Live speed, for the idle ↔ walk edge. */
66
- speed() {
67
- return Math.hypot(this.vx, this.vy);
68
- }
69
- /** Integrates velocity and resolves each axis against Solids. */
70
- step(dt) {
71
- const pos = this.entity.position;
72
- const previousX = pos.x;
73
- pos.x += this.vx * dt;
74
- if (this.resolveAxis('x', previousX))
75
- this.vx = 0;
76
- const previousY = pos.y;
77
- pos.y += this.vy * dt;
78
- if (this.resolveAxis('y', previousY))
79
- this.vy = 0;
80
- }
81
- halt() {
82
- this.vx = 0;
83
- this.vy = 0;
84
- }
85
- resolveAxis(axis, previous) {
86
- return resolveSolidAxis({
87
- entity: this.entity,
88
- axis,
89
- previous,
90
- body: () => this.collisionBody(),
91
- });
92
- }
93
- collisionBody() {
94
- return {
95
- x: this.entity.position.x,
96
- y: this.entity.position.y,
97
- width: this.hitboxWidth,
98
- height: this.hitboxHeight,
99
- shape: 'rectangle',
100
- };
101
- }
102
31
  }
@@ -1,18 +1,5 @@
1
- import { type RoleDefinition, type RoleGraph, type StateContext } from '@waica/engine';
2
- /**
3
- * The top-down 'player' role: the character you control from above. Both
4
- * default states share one body update; they differ only in the edges
5
- * their prefab data declares. Extending the player is defineStates
6
- * ('player', { yourState: {...} }) plus a state in the prefab — never a
7
- * fight with a parallel controller.
8
- */
9
1
  /** The state graph new top-down player characters start with, as prefab data. */
10
- export declare const TOPDOWN_PLAYER_STATE_GRAPH: RoleGraph;
11
- /**
12
- * One body update shared by every default state: read the four-direction
13
- * actions, accelerate, collide, then report what the body is doing as
14
- * signals. Unmatched signals are no-ops, so each state only reacts to
15
- * the edges its data declares.
16
- */
17
- export declare function topdownPlayerUpdate({ entity, game, fsm }: StateContext, dt: number): void;
18
- export declare const TOPDOWN_PLAYER_ROLE: RoleDefinition;
2
+ export declare const TOPDOWN_PLAYER_STATE_GRAPH: import("@waica/engine").RoleGraph;
3
+ /** Drives TopDownMotor from project actions and reports body state. */
4
+ export declare const topdownPlayerUpdate: (ctx: import("@waica/engine").StateContext, dt: number) => void;
5
+ export declare const TOPDOWN_PLAYER_ROLE: import("@waica/engine").RoleDefinition;
@@ -1,86 +1,14 @@
1
- import {} from '@waica/engine';
2
- import { Health } from './health.js';
3
- import { interactUpdate } from './interactable.js';
4
- import { Respawnable } from './respawnable.js';
1
+ import { createGridPlayerRole } from './grid-player-role.js';
5
2
  import { TopDownMotor } from './topdown-motor.js';
6
3
  /**
7
- * The top-down 'player' role: the character you control from above. Both
8
- * default states share one body update; they differ only in the edges
9
- * their prefab data declares. Extending the player is defineStates
10
- * ('player', { yourState: {...} }) plus a state in the prefab — never a
11
- * fight with a parallel controller.
4
+ * The top-down player role: eight-direction logical-grid movement with
5
+ * normalized diagonals and no gravity, driven by TopDownMotor.
12
6
  */
7
+ const topdownPlayer = createGridPlayerRole(TopDownMotor, 'You control this character with the project controls: eight-direction ' +
8
+ 'top-down movement with normalized diagonals and no gravity. Its ' +
9
+ 'states move the Motor.');
13
10
  /** The state graph new top-down player characters start with, as prefab data. */
14
- export const TOPDOWN_PLAYER_STATE_GRAPH = {
15
- initial: 'idle',
16
- states: {
17
- idle: {
18
- transitions: [{ on: 'signal:move', to: 'walk' }],
19
- },
20
- walk: {
21
- transitions: [{ on: 'signal:stop', to: 'idle' }],
22
- },
23
- dead: {
24
- // Freeze on idle (resolved directionally) rather than name a death
25
- // clip the stock sheets do not have and warn every frame.
26
- clip: 'idle',
27
- // A beat before control comes back, so death reads as an event.
28
- transitions: [{ on: 'timer:0.8', to: 'idle' }],
29
- },
30
- // Dying is the same from every state, so the edge lives on '*'. Its
31
- // presence is also the contract Health checks before signalling: a graph
32
- // without it gets its entity destroyed instead.
33
- '*': { transitions: [{ on: 'signal:death', to: 'dead' }] },
34
- },
35
- };
36
- /**
37
- * One body update shared by every default state: read the four-direction
38
- * actions, accelerate, collide, then report what the body is doing as
39
- * signals. Unmatched signals are no-ops, so each state only reacts to
40
- * the edges its data declares.
41
- */
42
- export function topdownPlayerUpdate({ entity, game, fsm }, dt) {
43
- const motor = entity.get(TopDownMotor);
44
- if (!motor)
45
- return;
46
- motor.run(game.input.axis('left', 'right'), game.input.axis('down', 'up'), dt);
47
- motor.step(dt);
48
- fsm.signal(motor.speed() > motor.walkThreshold ? 'move' : 'stop');
49
- }
50
- export const TOPDOWN_PLAYER_ROLE = {
51
- description: 'You control this character with the project controls: eight-direction ' +
52
- 'top-down movement with normalized diagonals and no gravity. Its ' +
53
- 'states move the Motor.',
54
- driver: 'TopDownMotor',
55
- graph: TOPDOWN_PLAYER_STATE_GRAPH,
56
- signals: {
57
- move: 'moving',
58
- stop: 'standing still',
59
- },
60
- states: {
61
- // Always-hook: the interact lookup must survive custom states like an
62
- // attack, so it runs in every state.
63
- '*': {
64
- onUpdate(ctx) {
65
- interactUpdate(ctx);
66
- },
67
- },
68
- // Fallback: a custom state without its own onUpdate keeps the full
69
- // body update — its file only has to say what makes it special.
70
- default: { onUpdate: topdownPlayerUpdate },
71
- idle: { onUpdate: topdownPlayerUpdate },
72
- walk: { onUpdate: topdownPlayerUpdate },
73
- // Coming back is what leaving death means, so it hangs off onExit: any
74
- // other way out of this state (a project's own edge) revives too.
75
- dead: {
76
- // A state without its own onUpdate falls back to the role's default
77
- // body update, so without this no-op the player kept walking for the
78
- // whole death beat instead of the graph taking control away.
79
- onUpdate() { },
80
- onExit({ entity }) {
81
- entity.get(Respawnable)?.respawn();
82
- entity.get(Health)?.heal(Infinity);
83
- },
84
- },
85
- },
86
- };
11
+ export const TOPDOWN_PLAYER_STATE_GRAPH = topdownPlayer.graph;
12
+ /** Drives TopDownMotor from project actions and reports body state. */
13
+ export const topdownPlayerUpdate = topdownPlayer.update;
14
+ export const TOPDOWN_PLAYER_ROLE = topdownPlayer.role;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/behaviors",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Waica built-in behaviors — the curated game-feel library",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,6 +19,6 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@waica/engine": "^0.7.0"
22
+ "@waica/engine": "^0.8.0"
23
23
  }
24
24
  }
@@ -63,6 +63,8 @@ export declare abstract class Component {
63
63
  onReady?(): void;
64
64
  /** Runs once per frame. */
65
65
  onUpdate?(dt: number): void;
66
+ /** Runs after the scene changes between identity and projected rendering. */
67
+ onProjectionChange?(projection: 'isometric' | null): void;
66
68
  /** Runs when this entity's Hitbox overlaps another one's. */
67
69
  onCollide?(other: Entity): void;
68
70
  /** Runs when this entity's DynamicBody physically contacts a Solid. */