@waica/cli 0.4.2 → 0.5.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 (66) hide show
  1. package/dist/editor/assets/{cssMode-BO8FrG9g.js → cssMode-2_8FrcPt.js} +1 -1
  2. package/dist/editor/assets/{freemarker2-C6rCz24J.js → freemarker2-BgSLfjNS.js} +1 -1
  3. package/dist/editor/assets/{handlebars-MZdu9Qw_.js → handlebars-CZIkUhwZ.js} +1 -1
  4. package/dist/editor/assets/{html-BrjX1TJ1.js → html-B66ooLhD.js} +1 -1
  5. package/dist/editor/assets/{htmlMode-cEvpMwiW.js → htmlMode-BiIT--Mv.js} +1 -1
  6. package/dist/editor/assets/{index-eTEBznqW.js → index-Br-M9Tdo.js} +1846 -259
  7. package/dist/editor/assets/{index-Btzc6zxr.css → index-q7IkLe-P.css} +1 -1
  8. package/dist/editor/assets/{javascript-DHUgp9_L.js → javascript-BZrbbzWQ.js} +1 -1
  9. package/dist/editor/assets/{jsonMode-D0PmIuaM.js → jsonMode-CABkbZgt.js} +1 -1
  10. package/dist/editor/assets/{liquid-UM67NCct.js → liquid-CT7pyNYe.js} +1 -1
  11. package/dist/editor/assets/{mdx-D3xXLqrP.js → mdx-DW7MPqdm.js} +1 -1
  12. package/dist/editor/assets/{python-BhLrcAva.js → python-CgBwEaNf.js} +1 -1
  13. package/dist/editor/assets/{razor-DcG373mQ.js → razor-Dvggr_Q1.js} +1 -1
  14. package/dist/editor/assets/{tsMode-ByTYaf45.js → tsMode-OfEpAJE7.js} +1 -1
  15. package/dist/editor/assets/{typescript-DfJlvcTN.js → typescript-BQbhlbBK.js} +1 -1
  16. package/dist/editor/assets/{xml-22dL0oCq.js → xml-5qToUR1k.js} +1 -1
  17. package/dist/editor/assets/{yaml-mgCu6yV6.js → yaml-CFM1jPNu.js} +1 -1
  18. package/dist/editor/index.html +2 -2
  19. package/dist/mcp/component-metadata.js +23 -0
  20. package/dist/mcp/introspection.js +2 -14
  21. package/dist/mcp/native-import.cjs +3 -0
  22. package/dist/mcp/node_modules/@waica/archetype-platformer/dist/prefabs.js +7 -1
  23. package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry-data.js +3 -1
  24. package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
  25. package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.d.ts +1 -0
  26. package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.js +1 -0
  27. package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.d.ts +1 -0
  28. package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.js +1 -1
  29. package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.d.ts +42 -1
  30. package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.js +49 -3
  31. package/dist/mcp/node_modules/@waica/behaviors/dist/health.d.ts +81 -0
  32. package/dist/mcp/node_modules/@waica/behaviors/dist/health.js +150 -0
  33. package/dist/mcp/node_modules/@waica/behaviors/dist/index.d.ts +2 -0
  34. package/dist/mcp/node_modules/@waica/behaviors/dist/index.js +2 -0
  35. package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.d.ts +1 -0
  36. package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.js +1 -0
  37. package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.d.ts +29 -0
  38. package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.js +37 -0
  39. package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.d.ts +1 -0
  40. package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.js +1 -0
  41. package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.d.ts +1 -0
  42. package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.js +10 -0
  43. package/dist/mcp/node_modules/@waica/behaviors/dist/player-states.js +26 -0
  44. package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.d.ts +5 -13
  45. package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.js +5 -11
  46. package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
  47. package/dist/mcp/node_modules/@waica/engine/dist/authoring-defaults.d.ts +11 -0
  48. package/dist/mcp/node_modules/@waica/engine/dist/authoring-defaults.js +51 -0
  49. package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +10 -1
  50. package/dist/mcp/node_modules/@waica/engine/dist/component.js +1 -0
  51. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +1 -0
  52. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +10 -0
  53. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.d.ts +1 -0
  54. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.js +1 -0
  55. package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +1 -0
  56. package/dist/mcp/node_modules/@waica/engine/dist/index.js +1 -0
  57. package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.d.ts +2 -1
  58. package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.js +7 -1
  59. package/dist/mcp/node_modules/@waica/engine/package.json +1 -1
  60. package/dist/mcp/package-resolver.js +10 -2
  61. package/dist/mcp/project-component-loader.js +287 -0
  62. package/dist/mcp/stdio.js +25 -0
  63. package/dist/mcp/template/src/characters/player.character.json +29 -2
  64. package/dist/mcp/template/src/characters/slime.character.json +6 -0
  65. package/dist/mcp/validation.js +168 -23
  66. package/package.json +2 -2
@@ -8,7 +8,13 @@ export type HazardTouch = 'stomp' | 'hurt';
8
8
  export declare function resolveHazardTouch(playerVy: number, playerBottom: number, hazardY: number, stompable: boolean): HazardTouch;
9
9
  /**
10
10
  * Hurts the player on contact. If stompable (Mario-style), stomping it
11
- * destroys it and bounces the player. Requires Hitbox on both entities.
11
+ * bounces the player. Requires Hitbox on both entities.
12
+ *
13
+ * "Hurts on touch" is all this means — being able to take a hit is Health's
14
+ * job, so a spike is a Hazard alone and an enemy is Hazard + Health. Either
15
+ * side of the interaction falls back to its pre-Health behavior (destroy
16
+ * itself on a stomp, respawn the player on contact) when there is no Health
17
+ * to route the damage through.
12
18
  */
13
19
  export declare class Hazard extends Component {
14
20
  static componentName: string;
@@ -22,8 +28,43 @@ export declare class Hazard extends Component {
22
28
  max: number;
23
29
  step: number;
24
30
  };
31
+ stompDamage: {
32
+ label: string;
33
+ min: number;
34
+ max: number;
35
+ step: number;
36
+ };
37
+ contactDamage: {
38
+ label: string;
39
+ min: number;
40
+ max: number;
41
+ step: number;
42
+ };
25
43
  };
44
+ static transient: string[];
26
45
  stompable: boolean;
27
46
  bounce: number;
47
+ /**
48
+ * Dealt to this entity when the player stomps it. Only read on the
49
+ * Health path — with no Health on this entity, a stomp always destroys
50
+ * it outright (the legacy behavior), whatever this is set to. Setting it
51
+ * to 0 does not make a Health-less hazard survive a stomp.
52
+ */
53
+ stompDamage: number;
54
+ /**
55
+ * Dealt to whoever touches it any other way. Only read on the Health
56
+ * path — with no Health on the other entity, contact always respawns it
57
+ * (the legacy behavior), whatever this is set to. Setting it to 0 does
58
+ * not make contact harmless for a Health-less target.
59
+ */
60
+ contactDamage: number;
61
+ /**
62
+ * Set once a stomp bounces the player, until it stops rising. The bounce
63
+ * (motor.vy = this.bounce) can leave the hitboxes still overlapping on
64
+ * the very next check, and with vy no longer < 0 resolveHazardTouch reads
65
+ * that as 'hurt' — a survivor would otherwise hurt the player it is still
66
+ * bouncing away from.
67
+ */
68
+ private bouncing;
28
69
  onCollide(other: Entity): void;
29
70
  }
@@ -1,4 +1,5 @@
1
1
  import { Component } from '@waica/engine';
2
+ import { Health } from './health.js';
2
3
  import { PlatformerMotor } from './platformer-motor.js';
3
4
  import { isPlayer } from './player-identity.js';
4
5
  import { Respawnable } from './respawnable.js';
@@ -14,16 +15,47 @@ export function resolveHazardTouch(playerVy, playerBottom, hazardY, stompable) {
14
15
  }
15
16
  /**
16
17
  * Hurts the player on contact. If stompable (Mario-style), stomping it
17
- * destroys it and bounces the player. Requires Hitbox on both entities.
18
+ * bounces the player. Requires Hitbox on both entities.
19
+ *
20
+ * "Hurts on touch" is all this means — being able to take a hit is Health's
21
+ * job, so a spike is a Hazard alone and an enemy is Hazard + Health. Either
22
+ * side of the interaction falls back to its pre-Health behavior (destroy
23
+ * itself on a stomp, respawn the player on contact) when there is no Health
24
+ * to route the damage through.
18
25
  */
19
26
  export class Hazard extends Component {
20
27
  static componentName = 'Hazard';
21
28
  static params = {
22
29
  stompable: { label: 'Stompable' },
23
30
  bounce: { label: 'Stomp bounce', min: 0, max: 30, step: 0.5 },
31
+ stompDamage: { label: 'Stomp damage', min: 0, max: 20, step: 1 },
32
+ contactDamage: { label: 'Contact damage', min: 0, max: 20, step: 1 },
24
33
  };
34
+ static transient = ['bouncing'];
25
35
  stompable = true;
26
36
  bounce = 10;
37
+ /**
38
+ * Dealt to this entity when the player stomps it. Only read on the
39
+ * Health path — with no Health on this entity, a stomp always destroys
40
+ * it outright (the legacy behavior), whatever this is set to. Setting it
41
+ * to 0 does not make a Health-less hazard survive a stomp.
42
+ */
43
+ stompDamage = 1;
44
+ /**
45
+ * Dealt to whoever touches it any other way. Only read on the Health
46
+ * path — with no Health on the other entity, contact always respawns it
47
+ * (the legacy behavior), whatever this is set to. Setting it to 0 does
48
+ * not make contact harmless for a Health-less target.
49
+ */
50
+ contactDamage = 1;
51
+ /**
52
+ * Set once a stomp bounces the player, until it stops rising. The bounce
53
+ * (motor.vy = this.bounce) can leave the hitboxes still overlapping on
54
+ * the very next check, and with vy no longer < 0 resolveHazardTouch reads
55
+ * that as 'hurt' — a survivor would otherwise hurt the player it is still
56
+ * bouncing away from.
57
+ */
58
+ bouncing = false;
27
59
  onCollide(other) {
28
60
  if (!isPlayer(other))
29
61
  return;
@@ -32,11 +64,25 @@ export class Hazard extends Component {
32
64
  const playerBottom = other.position.y - motor.hitboxHeight / 2;
33
65
  const touch = resolveHazardTouch(motor.vy, playerBottom, this.entity.position.y, this.stompable);
34
66
  if (touch === 'stomp') {
35
- this.entity.destroy();
67
+ const health = this.entity.get(Health);
68
+ if (health)
69
+ health.damage(this.stompDamage, other);
70
+ else
71
+ this.entity.destroy();
36
72
  motor.vy = this.bounce;
73
+ this.bouncing = true;
37
74
  return;
38
75
  }
76
+ if (this.bouncing) {
77
+ if (motor.vy > 0)
78
+ return;
79
+ this.bouncing = false;
80
+ }
39
81
  }
40
- other.get(Respawnable)?.respawn();
82
+ const health = other.get(Health);
83
+ if (health)
84
+ health.damage(this.contactDamage, this.entity);
85
+ else
86
+ other.get(Respawnable)?.respawn();
41
87
  }
42
88
  }
@@ -0,0 +1,81 @@
1
+ import { Component, type Entity, type StateJson } from '@waica/engine';
2
+ /**
3
+ * Where a 'signal:death' from `current` could land: the targets of every
4
+ * death edge on the current state or on '*'. Mirrors the merge
5
+ * nextTransition performs.
6
+ */
7
+ export declare function deathTargets(states: Record<string, StateJson>, current: string): string[];
8
+ /**
9
+ * Whether a state graph reacts to death on its own: a 'signal:death' edge
10
+ * on the current state or on '*'. Mirrors the merge nextTransition performs,
11
+ * because StateMachine.signal is fire-and-forget — it cannot report whether
12
+ * anybody handled the signal, so the graph is asked statically instead.
13
+ *
14
+ * Declaring an edge is not the same as taking it: nextTransition returns the
15
+ * FIRST firing edge, so a death edge listed after another edge that also
16
+ * fires loses the race and the signal is dropped. That is why signalling is
17
+ * followed by the deferred check in Health.onUpdate rather than trusted.
18
+ */
19
+ export declare function declaresDeathHandling(states: Record<string, StateJson>, current: string): boolean;
20
+ /**
21
+ * How much punishment an entity takes before it dies. Deliberately separate
22
+ * from Hazard's "hurts on touch": an enemy is both, a spike is only the
23
+ * hazard, and a projectile targets only what can be hurt.
24
+ *
25
+ * There is no kill(): lethality is arithmetic, so damage(Infinity) is the
26
+ * only death path and every death goes through the same policy.
27
+ */
28
+ export declare class Health extends Component {
29
+ static componentName: string;
30
+ static params: {
31
+ max: {
32
+ label: string;
33
+ min: number;
34
+ max: number;
35
+ step: number;
36
+ };
37
+ invulnerability: {
38
+ label: string;
39
+ min: number;
40
+ max: number;
41
+ step: number;
42
+ };
43
+ };
44
+ static transient: string[];
45
+ max: number;
46
+ /** Seconds of immunity granted by taking a hit. 0 disables i-frames. */
47
+ invulnerability: number;
48
+ /** Health left; 0 is dead. Filled in from max on ready. */
49
+ current: number;
50
+ /** Seconds left in the invulnerability window. */
51
+ private invulnerable;
52
+ /** Frames left before the signalled death is checked; 0 when not waiting. */
53
+ private deathGrace;
54
+ /** States the death signal was supposed to reach. */
55
+ private expectedStates;
56
+ onReady(): void;
57
+ onUpdate(dt: number): void;
58
+ /**
59
+ * Makes good on a death the graph said it would handle. Signalling is
60
+ * fire-and-forget, so "it declared an edge" is a promise, not a receipt:
61
+ * if the machine is not in one of the states that edge led to and the
62
+ * entity is still dead, nobody took it and the destroy fallback applies
63
+ * after all. Being healed back up counts as handled — a project that
64
+ * revives on death did resolve it, just not by changing state.
65
+ */
66
+ private settleDeath;
67
+ /**
68
+ * Takes `amount` off unless already dead or still invulnerable, then opens
69
+ * the invulnerability window. `source` is whatever dealt the damage, for
70
+ * listeners that care who hit them.
71
+ */
72
+ damage(amount: number, source?: Entity): void;
73
+ /** Gives health back, capped at max. heal(Infinity) is a full restore. */
74
+ heal(amount: number): void;
75
+ /**
76
+ * Announces the death, then lets the role decide — but only if it says it
77
+ * can: a graph with no death edge would swallow the signal silently, so
78
+ * destroying is the fallback rather than the exception.
79
+ */
80
+ private die;
81
+ }
@@ -0,0 +1,150 @@
1
+ import { Component, StateMachine } from '@waica/engine';
2
+ /**
3
+ * Below this, current is treated as exactly zero. Repeated fractional
4
+ * damage (e.g. ten hits of 0.1 against max: 1) can leave a floating-point
5
+ * residual — Math.max(0, current - amount) only clamps values that land
6
+ * at or below zero, not tiny positive ones — that current === 0 would
7
+ * otherwise miss forever, leaving the entity undead at 0 HP.
8
+ */
9
+ const DEATH_EPSILON = 1e-9;
10
+ /**
11
+ * Where a 'signal:death' from `current` could land: the targets of every
12
+ * death edge on the current state or on '*'. Mirrors the merge
13
+ * nextTransition performs.
14
+ */
15
+ export function deathTargets(states, current) {
16
+ const edges = [...(states[current]?.transitions ?? []), ...(states['*']?.transitions ?? [])];
17
+ return edges.filter((transition) => transition.on === 'signal:death').map((t) => t.to);
18
+ }
19
+ /**
20
+ * Whether a state graph reacts to death on its own: a 'signal:death' edge
21
+ * on the current state or on '*'. Mirrors the merge nextTransition performs,
22
+ * because StateMachine.signal is fire-and-forget — it cannot report whether
23
+ * anybody handled the signal, so the graph is asked statically instead.
24
+ *
25
+ * Declaring an edge is not the same as taking it: nextTransition returns the
26
+ * FIRST firing edge, so a death edge listed after another edge that also
27
+ * fires loses the race and the signal is dropped. That is why signalling is
28
+ * followed by the deferred check in Health.onUpdate rather than trusted.
29
+ */
30
+ export function declaresDeathHandling(states, current) {
31
+ return deathTargets(states, current).length > 0;
32
+ }
33
+ /**
34
+ * Frames to wait before deciding the graph ignored the death signal. Two,
35
+ * not one, because component update order is prefab-authored: with Health
36
+ * listed before its StateMachine, one frame would check before the machine
37
+ * ever ran. Two guarantees the machine got a full tick either way.
38
+ */
39
+ const DEATH_GRACE_FRAMES = 2;
40
+ /**
41
+ * How much punishment an entity takes before it dies. Deliberately separate
42
+ * from Hazard's "hurts on touch": an enemy is both, a spike is only the
43
+ * hazard, and a projectile targets only what can be hurt.
44
+ *
45
+ * There is no kill(): lethality is arithmetic, so damage(Infinity) is the
46
+ * only death path and every death goes through the same policy.
47
+ */
48
+ export class Health extends Component {
49
+ static componentName = 'Health';
50
+ static params = {
51
+ max: { label: 'Max health', min: 1, max: 20, step: 1 },
52
+ invulnerability: { label: 'Invulnerability', min: 0, max: 5, step: 0.1 },
53
+ };
54
+ static transient = ['current', 'invulnerable', 'deathGrace', 'expectedStates'];
55
+ max = 3;
56
+ /** Seconds of immunity granted by taking a hit. 0 disables i-frames. */
57
+ invulnerability = 0;
58
+ /** Health left; 0 is dead. Filled in from max on ready. */
59
+ current = 0;
60
+ /** Seconds left in the invulnerability window. */
61
+ invulnerable = 0;
62
+ /** Frames left before the signalled death is checked; 0 when not waiting. */
63
+ deathGrace = 0;
64
+ /** States the death signal was supposed to reach. */
65
+ expectedStates = [];
66
+ onReady() {
67
+ // The inspector has no min clamp on max, so an authored 0 (or a
68
+ // negative value) is reachable. Without this, current lands at <= 0
69
+ // and stays there forever: damage() early-returns while current <= 0,
70
+ // so die() is never reached — permanently invulnerable instead of
71
+ // already dead.
72
+ this.current = Math.max(0, this.max);
73
+ if (this.current === 0)
74
+ this.die();
75
+ }
76
+ onUpdate(dt) {
77
+ if (this.invulnerable > 0)
78
+ this.invulnerable = Math.max(0, this.invulnerable - dt);
79
+ if (this.deathGrace > 0 && --this.deathGrace === 0)
80
+ this.settleDeath();
81
+ }
82
+ /**
83
+ * Makes good on a death the graph said it would handle. Signalling is
84
+ * fire-and-forget, so "it declared an edge" is a promise, not a receipt:
85
+ * if the machine is not in one of the states that edge led to and the
86
+ * entity is still dead, nobody took it and the destroy fallback applies
87
+ * after all. Being healed back up counts as handled — a project that
88
+ * revives on death did resolve it, just not by changing state.
89
+ */
90
+ settleDeath() {
91
+ const expected = this.expectedStates;
92
+ this.expectedStates = [];
93
+ if (!this.entity.alive || this.current > 0)
94
+ return;
95
+ const machine = this.entity.get(StateMachine);
96
+ if (machine && expected.includes(machine.current))
97
+ return;
98
+ this.entity.destroy();
99
+ }
100
+ /**
101
+ * Takes `amount` off unless already dead or still invulnerable, then opens
102
+ * the invulnerability window. `source` is whatever dealt the damage, for
103
+ * listeners that care who hit them.
104
+ */
105
+ damage(amount, source) {
106
+ // Written as !(amount > 0) rather than amount <= 0 so NaN is rejected
107
+ // too — every NaN comparison is false, so amount <= 0 lets it through
108
+ // and poisons current (NaN - anything is NaN, and every guard against
109
+ // it is false forever after).
110
+ if (!(amount > 0) || this.current <= 0 || this.invulnerable > 0)
111
+ return;
112
+ this.current = Math.max(0, this.current - amount);
113
+ if (this.current < DEATH_EPSILON)
114
+ this.current = 0;
115
+ this.game.events.emit('damage', {
116
+ entity: this.entity,
117
+ amount,
118
+ current: this.current,
119
+ source,
120
+ });
121
+ this.invulnerable = this.invulnerability;
122
+ if (this.current === 0)
123
+ this.die();
124
+ }
125
+ /** Gives health back, capped at max. heal(Infinity) is a full restore. */
126
+ heal(amount) {
127
+ if (amount <= 0)
128
+ return;
129
+ this.current = Math.min(this.max, this.current + amount);
130
+ }
131
+ /**
132
+ * Announces the death, then lets the role decide — but only if it says it
133
+ * can: a graph with no death edge would swallow the signal silently, so
134
+ * destroying is the fallback rather than the exception.
135
+ */
136
+ die() {
137
+ this.game.events.emit('death', { entity: this.entity });
138
+ const machine = this.entity.get(StateMachine);
139
+ const targets = machine ? deathTargets(machine.states, machine.current) : [];
140
+ if (machine && targets.length > 0) {
141
+ machine.signal('death');
142
+ // Trust, then verify: settleDeath falls back to destroying if the
143
+ // signal never actually moved the machine anywhere it said it would.
144
+ this.expectedStates = targets;
145
+ this.deathGrace = DEATH_GRACE_FRAMES;
146
+ return;
147
+ }
148
+ this.entity.destroy();
149
+ }
150
+ }
@@ -5,5 +5,7 @@ export { Patrol, PATROLLER_ROLE, PATROLLER_STATE_GRAPH, type PatrolAxis } from '
5
5
  export { Chaser, CHASER_ROLE, CHASER_STATE_GRAPH, type ChaserMode } from './chaser.js';
6
6
  export { NPC_ROLE, NPC_STATE_GRAPH } from './npc.js';
7
7
  export { Hazard, resolveHazardTouch, type HazardTouch } from './hazard.js';
8
+ export { Health, declaresDeathHandling, deathTargets } from './health.js';
8
9
  export { Respawnable } from './respawnable.js';
10
+ export { OutOfBounds } from './out-of-bounds.js';
9
11
  export { Lifetime } from './lifetime.js';
@@ -5,5 +5,7 @@ export { Patrol, PATROLLER_ROLE, PATROLLER_STATE_GRAPH } from './patrol.js';
5
5
  export { Chaser, CHASER_ROLE, CHASER_STATE_GRAPH } from './chaser.js';
6
6
  export { NPC_ROLE, NPC_STATE_GRAPH } from './npc.js';
7
7
  export { Hazard, resolveHazardTouch } from './hazard.js';
8
+ export { Health, declaresDeathHandling, deathTargets } from './health.js';
8
9
  export { Respawnable } from './respawnable.js';
10
+ export { OutOfBounds } from './out-of-bounds.js';
9
11
  export { Lifetime } from './lifetime.js';
@@ -10,6 +10,7 @@ export declare class Lifetime extends Component {
10
10
  step: number;
11
11
  };
12
12
  };
13
+ static transient: string[];
13
14
  seconds: number;
14
15
  private elapsed;
15
16
  onUpdate(dt: number): void;
@@ -5,6 +5,7 @@ export class Lifetime extends Component {
5
5
  static params = {
6
6
  seconds: { label: 'Seconds', min: 0.05, max: 60, step: 0.05 },
7
7
  };
8
+ static transient = ['elapsed'];
8
9
  seconds = 1;
9
10
  elapsed = 0;
10
11
  onUpdate(dt) {
@@ -0,0 +1,29 @@
1
+ import { Component } from '@waica/engine';
2
+ /**
3
+ * Kills whatever falls out of the world. The damage is not configurable
4
+ * because leaving the level is lethal by nature — being hurt by a hard
5
+ * landing is a different mechanic, measured on ground contact, and project
6
+ * code can write it with Health.damage and the motor's readable velocity.
7
+ *
8
+ * "Lethal" still goes through Health.damage, which means invulnerability
9
+ * frames delay it: an entity that falls right after taking a hit keeps
10
+ * falling until its i-frames lapse. That is deliberate — damage() has no
11
+ * bypass, by design — and it resolves itself because this check re-runs
12
+ * every frame. An entity with no Health is destroyed outright instead;
13
+ * unlike Hazard, this does NOT fall back to Respawnable.
14
+ */
15
+ export declare class OutOfBounds extends Component {
16
+ static componentName: string;
17
+ static displayName: string;
18
+ static params: {
19
+ minY: {
20
+ label: string;
21
+ min: number;
22
+ max: number;
23
+ step: number;
24
+ };
25
+ };
26
+ /** Falling below this height is fatal. */
27
+ minY: number;
28
+ onUpdate(): void;
29
+ }
@@ -0,0 +1,37 @@
1
+ import { Component } from '@waica/engine';
2
+ import { Health } from './health.js';
3
+ /**
4
+ * Kills whatever falls out of the world. The damage is not configurable
5
+ * because leaving the level is lethal by nature — being hurt by a hard
6
+ * landing is a different mechanic, measured on ground contact, and project
7
+ * code can write it with Health.damage and the motor's readable velocity.
8
+ *
9
+ * "Lethal" still goes through Health.damage, which means invulnerability
10
+ * frames delay it: an entity that falls right after taking a hit keeps
11
+ * falling until its i-frames lapse. That is deliberate — damage() has no
12
+ * bypass, by design — and it resolves itself because this check re-runs
13
+ * every frame. An entity with no Health is destroyed outright instead;
14
+ * unlike Hazard, this does NOT fall back to Respawnable.
15
+ */
16
+ export class OutOfBounds extends Component {
17
+ static componentName = 'OutOfBounds';
18
+ static displayName = 'Out of bounds';
19
+ static params = {
20
+ minY: { label: 'Floor height', min: -50, max: 0, step: 1 },
21
+ };
22
+ /** Falling below this height is fatal. */
23
+ minY = -12;
24
+ onUpdate() {
25
+ // The frame that destroys the entity still iterates a copy of its
26
+ // components: stop checking instead of destroying twice.
27
+ if (!this.entity.alive)
28
+ return;
29
+ if (this.entity.position.y >= this.minY)
30
+ return;
31
+ const health = this.entity.get(Health);
32
+ if (health)
33
+ health.damage(Infinity, this.entity);
34
+ else
35
+ this.entity.destroy();
36
+ }
37
+ }
@@ -28,6 +28,7 @@ export declare class Patrol extends Component {
28
28
  step: number;
29
29
  };
30
30
  };
31
+ static transient: string[];
31
32
  axis: PatrolAxis;
32
33
  distance: number;
33
34
  speed: number;
@@ -14,6 +14,7 @@ export class Patrol extends Component {
14
14
  distance: { label: 'Distance', min: 0.5, max: 20, step: 0.5 },
15
15
  speed: { label: 'Speed', min: 0.5, max: 15, step: 0.5 },
16
16
  };
17
+ static transient = ['originX', 'originY', 'dir'];
17
18
  axis = 'horizontal';
18
19
  distance = 3;
19
20
  speed = 2;
@@ -77,6 +77,7 @@ export declare class PlatformerMotor extends Component {
77
77
  label: string;
78
78
  };
79
79
  };
80
+ static transient: string[];
80
81
  moveSpeed: number;
81
82
  acceleration: number;
82
83
  deceleration: number;
@@ -25,6 +25,16 @@ export class PlatformerMotor extends Component {
25
25
  runThreshold: { label: 'Run threshold', min: 0, max: 5, step: 0.1 },
26
26
  squashStretch: { label: 'Squash & stretch' },
27
27
  };
28
+ static transient = [
29
+ 'vx',
30
+ 'vy',
31
+ 'grounded',
32
+ 'facing',
33
+ 'coyoteTimer',
34
+ 'bufferTimer',
35
+ 'squashX',
36
+ 'squashY',
37
+ ];
28
38
  moveSpeed = 9;
29
39
  acceleration = 60;
30
40
  deceleration = 80;
@@ -1,5 +1,7 @@
1
1
  import {} from '@waica/engine';
2
+ import { Health } from './health.js';
2
3
  import { PlatformerMotor } from './platformer-motor.js';
4
+ import { Respawnable } from './respawnable.js';
3
5
  /**
4
6
  * The 'player' role: the character you control. All four default states
5
7
  * share one body update; they differ only in the edges their prefab data
@@ -38,6 +40,17 @@ export const PLAYER_STATE_GRAPH = {
38
40
  { on: 'signal:land', to: 'idle' },
39
41
  ],
40
42
  },
43
+ dead: {
44
+ // The stock dog sheet has no death animation; freeze on idle rather
45
+ // than name a clip that does not exist and warn every frame.
46
+ clip: 'idle',
47
+ // A beat before control comes back, so death reads as an event.
48
+ transitions: [{ on: 'timer:0.8', to: 'idle' }],
49
+ },
50
+ // Dying is the same from every state, so the edge lives on '*'. Its
51
+ // presence is also the contract Health checks before signalling: a graph
52
+ // without it gets its entity destroyed instead.
53
+ '*': { transitions: [{ on: 'signal:death', to: 'dead' }] },
41
54
  },
42
55
  };
43
56
  /**
@@ -91,5 +104,18 @@ export const PLAYER_ROLE = {
91
104
  run: { onUpdate: playerUpdate },
92
105
  jump: { onUpdate: playerUpdate },
93
106
  fall: { onUpdate: playerUpdate },
107
+ // Coming back is what leaving death means, so it hangs off onExit: any
108
+ // other way out of this state (a project's own edge) revives too.
109
+ dead: {
110
+ // A state without its own onUpdate falls back to the role's default
111
+ // body update — the shape every custom state relies on — so without
112
+ // this no-op the player kept running, jumping and falling for the
113
+ // whole death beat instead of the graph taking control away.
114
+ onUpdate() { },
115
+ onExit({ entity }) {
116
+ entity.get(Respawnable)?.respawn();
117
+ entity.get(Health)?.heal(Infinity);
118
+ },
119
+ },
94
120
  },
95
121
  };
@@ -1,23 +1,15 @@
1
1
  import { Component } from '@waica/engine';
2
2
  /**
3
- * Remembers the spawn point and puts the entity back there on death
4
- * (falling off the world or touching a Hazard).
3
+ * Remembers where the entity started and puts it back there on demand.
4
+ * That is all: what counts as death, and whether death leads to a respawn
5
+ * at all, belongs to Health and to the role's state graph. The `killY`
6
+ * lower bound this used to author moved to OutOfBounds (docs/adr/0003).
5
7
  */
6
8
  export declare class Respawnable extends Component {
7
9
  static componentName: string;
8
10
  static displayName: string;
9
- static params: {
10
- killY: {
11
- label: string;
12
- min: number;
13
- max: number;
14
- step: number;
15
- };
16
- };
17
- /** Falling below this height respawns. */
18
- killY: number;
11
+ static transient: string[];
19
12
  private spawn;
20
13
  onReady(): void;
21
14
  respawn(): void;
22
- onUpdate(): void;
23
15
  }
@@ -1,17 +1,15 @@
1
1
  import { Component, THREE } from '@waica/engine';
2
2
  import { PlatformerMotor } from './platformer-motor.js';
3
3
  /**
4
- * Remembers the spawn point and puts the entity back there on death
5
- * (falling off the world or touching a Hazard).
4
+ * Remembers where the entity started and puts it back there on demand.
5
+ * That is all: what counts as death, and whether death leads to a respawn
6
+ * at all, belongs to Health and to the role's state graph. The `killY`
7
+ * lower bound this used to author moved to OutOfBounds (docs/adr/0003).
6
8
  */
7
9
  export class Respawnable extends Component {
8
10
  static componentName = 'Respawnable';
9
11
  static displayName = 'Respawn';
10
- static params = {
11
- killY: { label: 'Kill height', min: -50, max: 0, step: 1 },
12
- };
13
- /** Falling below this height respawns. */
14
- killY = -12;
12
+ static transient = ['spawn'];
15
13
  spawn = new THREE.Vector3();
16
14
  onReady() {
17
15
  this.spawn.copy(this.entity.position);
@@ -20,8 +18,4 @@ export class Respawnable extends Component {
20
18
  this.entity.position.copy(this.spawn);
21
19
  this.entity.get(PlatformerMotor)?.halt();
22
20
  }
23
- onUpdate() {
24
- if (this.entity.position.y < this.killY)
25
- this.respawn();
26
- }
27
21
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/behaviors",
3
- "version": "0.4.2",
3
+ "version": "0.5.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.4.2"
22
+ "@waica/engine": "^0.5.0"
23
23
  }
24
24
  }
@@ -0,0 +1,11 @@
1
+ import type { ComponentClass } from './component.js';
2
+ /**
3
+ * The public authoring surface of a component class: own enumerable fields
4
+ * plus setter-backed accessors (walking the whole prototype chain, so a
5
+ * getter/setter pair inherited from a base class counts), read through the
6
+ * getter so the value is the public one. Excludes `_`-prefixed keys, the
7
+ * base Component wiring (`entity`, `game`), anything the class (or an
8
+ * ancestor) declares `transient`, and values that are `undefined` or not
9
+ * JSON-serializable (`Map`, `Set`, functions, class instances).
10
+ */
11
+ export declare function authoringDefaults(Class: ComponentClass, onError?: (error: unknown) => void): Record<string, unknown>;