@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.
- package/dist/editor/assets/{cssMode-BO8FrG9g.js → cssMode-2_8FrcPt.js} +1 -1
- package/dist/editor/assets/{freemarker2-C6rCz24J.js → freemarker2-BgSLfjNS.js} +1 -1
- package/dist/editor/assets/{handlebars-MZdu9Qw_.js → handlebars-CZIkUhwZ.js} +1 -1
- package/dist/editor/assets/{html-BrjX1TJ1.js → html-B66ooLhD.js} +1 -1
- package/dist/editor/assets/{htmlMode-cEvpMwiW.js → htmlMode-BiIT--Mv.js} +1 -1
- package/dist/editor/assets/{index-eTEBznqW.js → index-Br-M9Tdo.js} +1846 -259
- package/dist/editor/assets/{index-Btzc6zxr.css → index-q7IkLe-P.css} +1 -1
- package/dist/editor/assets/{javascript-DHUgp9_L.js → javascript-BZrbbzWQ.js} +1 -1
- package/dist/editor/assets/{jsonMode-D0PmIuaM.js → jsonMode-CABkbZgt.js} +1 -1
- package/dist/editor/assets/{liquid-UM67NCct.js → liquid-CT7pyNYe.js} +1 -1
- package/dist/editor/assets/{mdx-D3xXLqrP.js → mdx-DW7MPqdm.js} +1 -1
- package/dist/editor/assets/{python-BhLrcAva.js → python-CgBwEaNf.js} +1 -1
- package/dist/editor/assets/{razor-DcG373mQ.js → razor-Dvggr_Q1.js} +1 -1
- package/dist/editor/assets/{tsMode-ByTYaf45.js → tsMode-OfEpAJE7.js} +1 -1
- package/dist/editor/assets/{typescript-DfJlvcTN.js → typescript-BQbhlbBK.js} +1 -1
- package/dist/editor/assets/{xml-22dL0oCq.js → xml-5qToUR1k.js} +1 -1
- package/dist/editor/assets/{yaml-mgCu6yV6.js → yaml-CFM1jPNu.js} +1 -1
- package/dist/editor/index.html +2 -2
- package/dist/mcp/component-metadata.js +23 -0
- package/dist/mcp/introspection.js +2 -14
- package/dist/mcp/native-import.cjs +3 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/prefabs.js +7 -1
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry-data.js +3 -1
- package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
- package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.js +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.js +1 -1
- package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.d.ts +42 -1
- package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.js +49 -3
- package/dist/mcp/node_modules/@waica/behaviors/dist/health.d.ts +81 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/health.js +150 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/index.d.ts +2 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/index.js +2 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.js +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.d.ts +29 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.js +37 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.js +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.js +10 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/player-states.js +26 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.d.ts +5 -13
- package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.js +5 -11
- package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
- package/dist/mcp/node_modules/@waica/engine/dist/authoring-defaults.d.ts +11 -0
- package/dist/mcp/node_modules/@waica/engine/dist/authoring-defaults.js +51 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +10 -1
- package/dist/mcp/node_modules/@waica/engine/dist/component.js +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +10 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.js +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/index.js +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.d.ts +2 -1
- package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.js +7 -1
- package/dist/mcp/node_modules/@waica/engine/package.json +1 -1
- package/dist/mcp/package-resolver.js +10 -2
- package/dist/mcp/project-component-loader.js +287 -0
- package/dist/mcp/stdio.js +25 -0
- package/dist/mcp/template/src/characters/player.character.json +29 -2
- package/dist/mcp/template/src/characters/slime.character.json +6 -0
- package/dist/mcp/validation.js +168 -23
- 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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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(
|
|
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';
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -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
|
|
4
|
-
*
|
|
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
|
|
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
|
|
5
|
-
*
|
|
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
|
|
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.
|
|
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.
|
|
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>;
|