@waica/cli 0.8.0 → 0.9.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-DwPoqXFO.js → cssMode-DM5x3eXD.js} +1 -1
- package/dist/editor/assets/{freemarker2-gl5y25bH.js → freemarker2-CmlzUOOI.js} +1 -1
- package/dist/editor/assets/{handlebars-DHUb8mb3.js → handlebars-Dlz-T3Pu.js} +1 -1
- package/dist/editor/assets/{html-C8W-psr4.js → html-B-NfgvUd.js} +1 -1
- package/dist/editor/assets/{htmlMode-BiF5TH2_.js → htmlMode-K4hF3BAu.js} +1 -1
- package/dist/editor/assets/{index-DtMwWp8R.js → index-Ca2oPxGt.js} +1705 -307
- package/dist/editor/assets/{javascript-D_t2jyWi.js → javascript-COj32tw-.js} +1 -1
- package/dist/editor/assets/{jsonMode-CZ0SltDj.js → jsonMode-DyL12Tvg.js} +1 -1
- package/dist/editor/assets/{liquid-DUxURZui.js → liquid-CGaRZ9Iu.js} +1 -1
- package/dist/editor/assets/{mdx-CDIQ7hmo.js → mdx-L4fRd4xO.js} +1 -1
- package/dist/editor/assets/{python-Cra2N916.js → python-Z6FCrHUG.js} +1 -1
- package/dist/editor/assets/{razor-B3qV-mNp.js → razor-6XSfF-a3.js} +1 -1
- package/dist/editor/assets/{tsMode-Ci457z4v.js → tsMode-C24rNZgD.js} +1 -1
- package/dist/editor/assets/{typescript-BXjh3GLT.js → typescript-BcFln1pD.js} +1 -1
- package/dist/editor/assets/waica-iso-hero-BsEdW0V7.png +0 -0
- package/dist/editor/assets/waica-iso-orc-DRK-SaXm.png +0 -0
- package/dist/editor/assets/waica-iso-villager-szDkD_Ag.png +0 -0
- package/dist/editor/assets/{xml-DQuEpqk5.js → xml-DJCue8rm.js} +1 -1
- package/dist/editor/assets/{yaml-CQA7WIT-.js → yaml-DiJf3V99.js} +1 -1
- package/dist/editor/index.html +1 -1
- package/dist/mcp/node_modules/@waica/archetype-isometric/assets/ATTRIBUTION.md +40 -19
- package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-hero.png +0 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-orc.png +0 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-villager.png +0 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/animation.d.ts +5 -1
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/animation.js +9 -2
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/controls.js +2 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/manifest.js +1 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/prefabs.d.ts +8 -4
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/prefabs.js +33 -14
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry-data.js +2 -1
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/scene-default.js +12 -2
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/ui.js +13 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/package.json +3 -3
- package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
- package/dist/mcp/node_modules/@waica/archetype-topdown/dist/animation.d.ts +2 -1
- package/dist/mcp/node_modules/@waica/archetype-topdown/dist/animation.js +6 -2
- package/dist/mcp/node_modules/@waica/archetype-topdown/package.json +3 -3
- package/dist/mcp/node_modules/@waica/behaviors/dist/facing.d.ts +18 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/facing.js +56 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/grid-motor.d.ts +8 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/grid-motor.js +3 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/grid-player-role.d.ts +5 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/grid-player-role.js +71 -9
- package/dist/mcp/node_modules/@waica/behaviors/dist/health.d.ts +29 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/health.js +79 -3
- package/dist/mcp/node_modules/@waica/behaviors/dist/index.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/index.js +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/iso-motor.d.ts +8 -1
- package/dist/mcp/node_modules/@waica/behaviors/dist/iso-motor.js +7 -19
- package/dist/mcp/node_modules/@waica/behaviors/dist/melee-attack.d.ts +48 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/melee-attack.js +83 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.d.ts +11 -3
- package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.js +49 -5
- package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
- package/dist/mcp/node_modules/@waica/engine/package.json +1 -1
- package/dist/mcp/template/src/stats.json +2 -1
- package/package.json +1 -1
|
@@ -7,6 +7,8 @@ import { Component, StateMachine } from '@waica/engine';
|
|
|
7
7
|
* otherwise miss forever, leaving the entity undead at 0 HP.
|
|
8
8
|
*/
|
|
9
9
|
const DEATH_EPSILON = 1e-9;
|
|
10
|
+
/** Seconds each blink phase lasts while invulnerable: 10 Hz on/off. */
|
|
11
|
+
const BLINK_PERIOD = 0.1;
|
|
10
12
|
/**
|
|
11
13
|
* Where a 'signal:death' from `current` could land: the targets of every
|
|
12
14
|
* death edge on the current state or on '*'. Mirrors the merge
|
|
@@ -44,19 +46,56 @@ export class Health extends Component {
|
|
|
44
46
|
static params = {
|
|
45
47
|
max: { label: 'Max health', min: 1, max: 20, step: 1 },
|
|
46
48
|
invulnerability: { label: 'Invulnerability', min: 0, max: 5, step: 0.1 },
|
|
49
|
+
stat: { label: 'Stat', ref: 'stat' },
|
|
47
50
|
};
|
|
48
|
-
static transient = [
|
|
51
|
+
static transient = [
|
|
52
|
+
'current',
|
|
53
|
+
'invulnerable',
|
|
54
|
+
'deathPending',
|
|
55
|
+
'expectedStates',
|
|
56
|
+
'lastDamageSource',
|
|
57
|
+
'blinkClock',
|
|
58
|
+
];
|
|
49
59
|
max = 3;
|
|
50
60
|
/** Seconds of immunity granted by taking a hit. 0 disables i-frames. */
|
|
51
61
|
invulnerability = 0;
|
|
62
|
+
/**
|
|
63
|
+
* Stat that mirrors `current` (a HUD binds to it with `{{stat}}`). Empty
|
|
64
|
+
* publishes nothing: the stat is the project's to declare.
|
|
65
|
+
*/
|
|
66
|
+
stat = '';
|
|
52
67
|
/** Health left; 0 is dead. Filled in from max on ready. */
|
|
53
68
|
current = 0;
|
|
69
|
+
/** Whoever dealt the last accepted hit, for the state that reacts to it. */
|
|
70
|
+
lastDamageSource;
|
|
54
71
|
/** Seconds left in the invulnerability window. */
|
|
55
72
|
invulnerable = 0;
|
|
73
|
+
/** Seconds into the current blink, for the 10 Hz toggle. */
|
|
74
|
+
blinkClock = 0;
|
|
75
|
+
/** Whether the node is being flashed: exactly while a window is open. */
|
|
76
|
+
get blinking() {
|
|
77
|
+
return this.invulnerable > 0;
|
|
78
|
+
}
|
|
56
79
|
/** Whether the signalled death must be checked on this component's next update. */
|
|
57
80
|
deathPending = false;
|
|
58
81
|
/** States the death signal was supposed to reach. */
|
|
59
82
|
expectedStates = [];
|
|
83
|
+
/**
|
|
84
|
+
* What a Runtime Snapshot shows: the authored values, the live ones, and
|
|
85
|
+
* the last source by name — projecting the whole Entity would drag its
|
|
86
|
+
* scene node along and bury the numbers that matter.
|
|
87
|
+
*/
|
|
88
|
+
inspectState() {
|
|
89
|
+
return {
|
|
90
|
+
max: this.max,
|
|
91
|
+
invulnerability: this.invulnerability,
|
|
92
|
+
stat: this.stat,
|
|
93
|
+
current: this.current,
|
|
94
|
+
invulnerable: this.invulnerable,
|
|
95
|
+
blinking: this.blinking,
|
|
96
|
+
lastDamageSource: this.lastDamageSource?.name ?? null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
60
99
|
onReady() {
|
|
61
100
|
// The inspector has no min clamp on max, so an authored 0 (or a
|
|
62
101
|
// negative value) is reachable. Without this, current lands at <= 0
|
|
@@ -64,12 +103,15 @@ export class Health extends Component {
|
|
|
64
103
|
// so die() is never reached — permanently invulnerable instead of
|
|
65
104
|
// already dead.
|
|
66
105
|
this.current = Math.max(0, this.max);
|
|
106
|
+
this.publish();
|
|
67
107
|
if (this.current === 0)
|
|
68
108
|
this.die();
|
|
69
109
|
}
|
|
70
110
|
onUpdate(dt) {
|
|
71
|
-
if (this.invulnerable > 0)
|
|
111
|
+
if (this.invulnerable > 0) {
|
|
72
112
|
this.invulnerable = Math.max(0, this.invulnerable - dt);
|
|
113
|
+
this.blink(dt);
|
|
114
|
+
}
|
|
73
115
|
if (this.deathPending) {
|
|
74
116
|
this.deathPending = false;
|
|
75
117
|
this.settleDeath();
|
|
@@ -108,6 +150,8 @@ export class Health extends Component {
|
|
|
108
150
|
this.current = Math.max(0, this.current - amount);
|
|
109
151
|
if (this.current < DEATH_EPSILON)
|
|
110
152
|
this.current = 0;
|
|
153
|
+
this.lastDamageSource = source;
|
|
154
|
+
this.publish();
|
|
111
155
|
this.game.events.emit('damage', {
|
|
112
156
|
entity: this.entity,
|
|
113
157
|
amount,
|
|
@@ -115,14 +159,39 @@ export class Health extends Component {
|
|
|
115
159
|
source,
|
|
116
160
|
});
|
|
117
161
|
this.invulnerable = this.invulnerability;
|
|
118
|
-
|
|
162
|
+
this.blinkClock = 0;
|
|
163
|
+
if (this.current === 0) {
|
|
119
164
|
this.die();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Fire-and-forget, like death: a graph with no 'signal:hurt' edge simply
|
|
168
|
+
// ignores it, so a stunned/knocked-back reaction is the role's to add.
|
|
169
|
+
this.entity.get(StateMachine)?.signal('hurt');
|
|
120
170
|
}
|
|
121
171
|
/** Gives health back, capped at max. heal(Infinity) is a full restore. */
|
|
122
172
|
heal(amount) {
|
|
123
173
|
if (amount <= 0)
|
|
124
174
|
return;
|
|
125
175
|
this.current = Math.min(this.max, this.current + amount);
|
|
176
|
+
this.publish();
|
|
177
|
+
}
|
|
178
|
+
/** Mirrors current into the named stat, when one is named. */
|
|
179
|
+
publish() {
|
|
180
|
+
if (this.stat)
|
|
181
|
+
this.game.stats.set(this.stat, this.current);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Flashes the node at 10 Hz for as long as the window stays open, and
|
|
185
|
+
* leaves it visible the frame the window closes — the flash is feedback,
|
|
186
|
+
* never a state a hit can leave the entity stuck in.
|
|
187
|
+
*/
|
|
188
|
+
blink(dt) {
|
|
189
|
+
if (this.invulnerable > 0) {
|
|
190
|
+
this.blinkClock += dt;
|
|
191
|
+
this.entity.node.visible = Math.floor(this.blinkClock / BLINK_PERIOD) % 2 === 0;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.entity.node.visible = true;
|
|
126
195
|
}
|
|
127
196
|
/**
|
|
128
197
|
* Announces the death, then lets the role decide — but only if it says it
|
|
@@ -130,6 +199,13 @@ export class Health extends Component {
|
|
|
130
199
|
* destroying is the fallback rather than the exception.
|
|
131
200
|
*/
|
|
132
201
|
die() {
|
|
202
|
+
// The killing blow opened a window like any other hit; close it. The
|
|
203
|
+
// death pose must hold steady, and a revived entity starts without
|
|
204
|
+
// leftover immunity. The node is visible here by construction: blink()
|
|
205
|
+
// restores it the frame a window closes, and a lethal hit only lands
|
|
206
|
+
// while no window is open.
|
|
207
|
+
this.invulnerable = 0;
|
|
208
|
+
this.blinkClock = 0;
|
|
133
209
|
this.game.events.emit('death', { entity: this.entity });
|
|
134
210
|
const machine = this.entity.get(StateMachine);
|
|
135
211
|
const targets = machine ? deathTargets(machine.states, machine.current) : [];
|
|
@@ -10,6 +10,7 @@ export { Patrol, PATROLLER_ROLE, PATROLLER_STATE_GRAPH, type PatrolAxis } from '
|
|
|
10
10
|
export { Chaser, CHASER_ROLE, CHASER_STATE_GRAPH, type ChaserMode } from './chaser.js';
|
|
11
11
|
export { NPC_ROLE, NPC_STATE_GRAPH } from './npc.js';
|
|
12
12
|
export { Hazard, resolveHazardTouch, type HazardTouch } from './hazard.js';
|
|
13
|
+
export { MeleeAttack } from './melee-attack.js';
|
|
13
14
|
export { Health, declaresDeathHandling, deathTargets } from './health.js';
|
|
14
15
|
export { Respawnable } from './respawnable.js';
|
|
15
16
|
export { OutOfBounds } from './out-of-bounds.js';
|
|
@@ -10,6 +10,7 @@ export { Patrol, PATROLLER_ROLE, PATROLLER_STATE_GRAPH } from './patrol.js';
|
|
|
10
10
|
export { Chaser, CHASER_ROLE, CHASER_STATE_GRAPH } from './chaser.js';
|
|
11
11
|
export { NPC_ROLE, NPC_STATE_GRAPH } from './npc.js';
|
|
12
12
|
export { Hazard, resolveHazardTouch } from './hazard.js';
|
|
13
|
+
export { MeleeAttack } from './melee-attack.js';
|
|
13
14
|
export { Health, declaresDeathHandling, deathTargets } from './health.js';
|
|
14
15
|
export { Respawnable } from './respawnable.js';
|
|
15
16
|
export { OutOfBounds } from './out-of-bounds.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type ScreenFacing } from './facing.js';
|
|
1
2
|
import { GridMotor } from './grid-motor.js';
|
|
2
3
|
/** The eight screen-relative facings declared by the isometric archetype. */
|
|
3
|
-
export type IsoFacing =
|
|
4
|
+
export type IsoFacing = ScreenFacing;
|
|
4
5
|
/**
|
|
5
6
|
* Passive isometric motor. Input and facing are screen-relative while the
|
|
6
7
|
* shared GridMotor integrates and collides in logical square-grid space.
|
|
@@ -9,6 +10,12 @@ export type IsoFacing = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
|
|
|
9
10
|
export declare class IsoMotor extends GridMotor<IsoFacing> {
|
|
10
11
|
static componentName: string;
|
|
11
12
|
facing: IsoFacing;
|
|
13
|
+
/**
|
|
14
|
+
* Under the diamond projection neither logical axis is "up the screen", so
|
|
15
|
+
* the footprint is square: as deep as it is wide, one cell-ish, instead of
|
|
16
|
+
* the short body top-down uses to let the player overlap a fence's top.
|
|
17
|
+
*/
|
|
18
|
+
hitboxHeight: number;
|
|
12
19
|
/** Accelerates screen-relative input toward a logical-space velocity. */
|
|
13
20
|
run(inputX: number, inputY: number, dt: number): void;
|
|
14
21
|
}
|
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
import { screenInputToLogical } from '@waica/engine';
|
|
2
|
+
import { facingForInput } from './facing.js';
|
|
2
3
|
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
4
|
/**
|
|
23
5
|
* Passive isometric motor. Input and facing are screen-relative while the
|
|
24
6
|
* shared GridMotor integrates and collides in logical square-grid space.
|
|
@@ -27,6 +9,12 @@ function facingForInput(inputX, inputY) {
|
|
|
27
9
|
export class IsoMotor extends GridMotor {
|
|
28
10
|
static componentName = 'IsoMotor';
|
|
29
11
|
facing = 's';
|
|
12
|
+
/**
|
|
13
|
+
* Under the diamond projection neither logical axis is "up the screen", so
|
|
14
|
+
* the footprint is square: as deep as it is wide, one cell-ish, instead of
|
|
15
|
+
* the short body top-down uses to let the player overlap a fence's top.
|
|
16
|
+
*/
|
|
17
|
+
hitboxHeight = 0.9;
|
|
30
18
|
/** Accelerates screen-relative input toward a logical-space velocity. */
|
|
31
19
|
run(inputX, inputY, dt) {
|
|
32
20
|
const direction = screenInputToLogical(inputX, inputY);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Component, type Entity } from '@waica/engine';
|
|
2
|
+
/**
|
|
3
|
+
* A short frontal strike. Passive like a motor: state code decides when it
|
|
4
|
+
* happens (the player's `attack` state) and calls strike(facing); this only
|
|
5
|
+
* knows where the blow lands and who can be hurt by it. The hit area is a
|
|
6
|
+
* `range` × `width` rectangle in logical space, laid along the facing from
|
|
7
|
+
* the attacker's position — under the isometric projection that is the
|
|
8
|
+
* diamond diagonal screen-east maps to, so "in front" matches the screen.
|
|
9
|
+
*/
|
|
10
|
+
export declare class MeleeAttack extends Component {
|
|
11
|
+
static componentName: string;
|
|
12
|
+
static displayName: string;
|
|
13
|
+
static params: {
|
|
14
|
+
damage: {
|
|
15
|
+
label: string;
|
|
16
|
+
min: number;
|
|
17
|
+
max: number;
|
|
18
|
+
step: number;
|
|
19
|
+
};
|
|
20
|
+
range: {
|
|
21
|
+
label: string;
|
|
22
|
+
min: number;
|
|
23
|
+
max: number;
|
|
24
|
+
step: number;
|
|
25
|
+
};
|
|
26
|
+
width: {
|
|
27
|
+
label: string;
|
|
28
|
+
min: number;
|
|
29
|
+
max: number;
|
|
30
|
+
step: number;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
damage: number;
|
|
34
|
+
/** How far in front of the attacker the blow reaches, in world units. */
|
|
35
|
+
range: number;
|
|
36
|
+
/** How wide the blow is, across the facing. */
|
|
37
|
+
width: number;
|
|
38
|
+
/**
|
|
39
|
+
* Lands one blow along `facing`: every other living entity with a Hitbox
|
|
40
|
+
* and a Health inside the strike area takes `damage` once, with this
|
|
41
|
+
* entity as the source. Returns the entities whose health actually
|
|
42
|
+
* dropped — a target still inside its invulnerability window is not
|
|
43
|
+
* counted. An unknown facing strikes nothing.
|
|
44
|
+
*/
|
|
45
|
+
strike(facing: string): Entity[];
|
|
46
|
+
/** The oriented rectangle in front of the attacker, as a unit-scaled polygon. */
|
|
47
|
+
private strikeArea;
|
|
48
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Component, Hitbox, collisionOverlap, } from '@waica/engine';
|
|
2
|
+
import { logicalDirection } from './facing.js';
|
|
3
|
+
import { Health } from './health.js';
|
|
4
|
+
/**
|
|
5
|
+
* A short frontal strike. Passive like a motor: state code decides when it
|
|
6
|
+
* happens (the player's `attack` state) and calls strike(facing); this only
|
|
7
|
+
* knows where the blow lands and who can be hurt by it. The hit area is a
|
|
8
|
+
* `range` × `width` rectangle in logical space, laid along the facing from
|
|
9
|
+
* the attacker's position — under the isometric projection that is the
|
|
10
|
+
* diamond diagonal screen-east maps to, so "in front" matches the screen.
|
|
11
|
+
*/
|
|
12
|
+
export class MeleeAttack extends Component {
|
|
13
|
+
static componentName = 'MeleeAttack';
|
|
14
|
+
static displayName = 'Melee attack';
|
|
15
|
+
static params = {
|
|
16
|
+
damage: { label: 'Damage', min: 0, max: 20, step: 1 },
|
|
17
|
+
range: { label: 'Range', min: 0.5, max: 5, step: 0.5 },
|
|
18
|
+
width: { label: 'Width', min: 0.5, max: 5, step: 0.5 },
|
|
19
|
+
};
|
|
20
|
+
damage = 1;
|
|
21
|
+
/** How far in front of the attacker the blow reaches, in world units. */
|
|
22
|
+
range = 1;
|
|
23
|
+
/** How wide the blow is, across the facing. */
|
|
24
|
+
width = 1;
|
|
25
|
+
/**
|
|
26
|
+
* Lands one blow along `facing`: every other living entity with a Hitbox
|
|
27
|
+
* and a Health inside the strike area takes `damage` once, with this
|
|
28
|
+
* entity as the source. Returns the entities whose health actually
|
|
29
|
+
* dropped — a target still inside its invulnerability window is not
|
|
30
|
+
* counted. An unknown facing strikes nothing.
|
|
31
|
+
*/
|
|
32
|
+
strike(facing) {
|
|
33
|
+
const direction = logicalDirection(facing, this.game.projection);
|
|
34
|
+
if (!direction)
|
|
35
|
+
return [];
|
|
36
|
+
const area = this.strikeArea(direction.x, direction.y);
|
|
37
|
+
const struck = [];
|
|
38
|
+
// A copy: a target with no death-handling graph is destroyed on the spot,
|
|
39
|
+
// which splices it out of the live array mid-loop (Game does the same).
|
|
40
|
+
for (const other of [...this.game.entities]) {
|
|
41
|
+
if (other === this.entity || !other.alive)
|
|
42
|
+
continue;
|
|
43
|
+
const hitbox = other.get(Hitbox);
|
|
44
|
+
const health = other.get(Health);
|
|
45
|
+
if (!hitbox || !health)
|
|
46
|
+
continue;
|
|
47
|
+
const body = {
|
|
48
|
+
x: other.position.x + hitbox.offsetX,
|
|
49
|
+
y: other.position.y + hitbox.offsetY,
|
|
50
|
+
width: hitbox.width,
|
|
51
|
+
height: hitbox.height,
|
|
52
|
+
shape: hitbox.shape,
|
|
53
|
+
points: hitbox.points,
|
|
54
|
+
};
|
|
55
|
+
if (!collisionOverlap(area, body))
|
|
56
|
+
continue;
|
|
57
|
+
const before = health.current;
|
|
58
|
+
health.damage(this.damage, this.entity);
|
|
59
|
+
if (health.current < before)
|
|
60
|
+
struck.push(other);
|
|
61
|
+
}
|
|
62
|
+
return struck;
|
|
63
|
+
}
|
|
64
|
+
/** The oriented rectangle in front of the attacker, as a unit-scaled polygon. */
|
|
65
|
+
strikeArea(dx, dy) {
|
|
66
|
+
const along = { x: (dx * this.range) / 2, y: (dy * this.range) / 2 };
|
|
67
|
+
const across = { x: (-dy * this.width) / 2, y: (dx * this.width) / 2 };
|
|
68
|
+
const points = [
|
|
69
|
+
[-along.x - across.x, -along.y - across.y],
|
|
70
|
+
[along.x - across.x, along.y - across.y],
|
|
71
|
+
[along.x + across.x, along.y + across.y],
|
|
72
|
+
[-along.x + across.x, -along.y + across.y],
|
|
73
|
+
];
|
|
74
|
+
return {
|
|
75
|
+
x: this.entity.position.x + along.x,
|
|
76
|
+
y: this.entity.position.y + along.y,
|
|
77
|
+
width: 1,
|
|
78
|
+
height: 1,
|
|
79
|
+
shape: 'polygon',
|
|
80
|
+
points,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { Component, type RoleDefinition, type RoleGraph } from '@waica/engine';
|
|
1
|
+
import { Component, type AnimationFacingProvider, type RoleDefinition, type RoleGraph } from '@waica/engine';
|
|
2
2
|
export type PatrolAxis = 'horizontal' | 'vertical';
|
|
3
3
|
/**
|
|
4
4
|
* Rail patrol: back and forth `distance` units from the starting
|
|
5
5
|
* position along one axis — sideways or up and down — turning around
|
|
6
|
-
* at the ends
|
|
6
|
+
* at the ends. It reports which way it walks as seen on screen, so a
|
|
7
|
+
* directional contract picks the matching clip; without one, walking
|
|
8
|
+
* sideways flips the sprite by scale instead.
|
|
7
9
|
* Passive like a motor: no onUpdate of its own — the 'patroller' logic
|
|
8
10
|
* set's walk state calls step(dt), so the StateMachine stays the single
|
|
9
11
|
* owner of the frame.
|
|
10
12
|
*/
|
|
11
|
-
export declare class Patrol extends Component {
|
|
13
|
+
export declare class Patrol extends Component implements AnimationFacingProvider {
|
|
12
14
|
static componentName: string;
|
|
13
15
|
static params: {
|
|
14
16
|
axis: {
|
|
@@ -36,6 +38,12 @@ export declare class Patrol extends Component {
|
|
|
36
38
|
private originY;
|
|
37
39
|
private dir;
|
|
38
40
|
onReady(): void;
|
|
41
|
+
/**
|
|
42
|
+
* The rail direction as rendered: the logical axis it walks along,
|
|
43
|
+
* projected when the scene is isometric — a horizontal rail reads as
|
|
44
|
+
* south-east/north-west there, a vertical one as south-west/north-east.
|
|
45
|
+
*/
|
|
46
|
+
getAnimationFacing(): string;
|
|
39
47
|
/** One patrol step: advance, turn at the rail's ends, flip the sprite. */
|
|
40
48
|
step(dt: number): void;
|
|
41
49
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { Component } from '@waica/engine';
|
|
1
|
+
import { Component, installedDirectionalAnimation, projectIsometric, } from '@waica/engine';
|
|
2
|
+
import { facingForInput } from './facing.js';
|
|
3
|
+
/** Seconds a hit stops the rail. */
|
|
4
|
+
const HURT_SECONDS = 0.25;
|
|
5
|
+
/** Seconds the death pose stays on screen before the entity goes away. */
|
|
6
|
+
const DEATH_SECONDS = 0.5;
|
|
2
7
|
/**
|
|
3
8
|
* Rail patrol: back and forth `distance` units from the starting
|
|
4
9
|
* position along one axis — sideways or up and down — turning around
|
|
5
|
-
* at the ends
|
|
10
|
+
* at the ends. It reports which way it walks as seen on screen, so a
|
|
11
|
+
* directional contract picks the matching clip; without one, walking
|
|
12
|
+
* sideways flips the sprite by scale instead.
|
|
6
13
|
* Passive like a motor: no onUpdate of its own — the 'patroller' logic
|
|
7
14
|
* set's walk state calls step(dt), so the StateMachine stays the single
|
|
8
15
|
* owner of the frame.
|
|
@@ -26,6 +33,18 @@ export class Patrol extends Component {
|
|
|
26
33
|
this.originX = this.entity.position.x;
|
|
27
34
|
this.originY = this.entity.position.y;
|
|
28
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The rail direction as rendered: the logical axis it walks along,
|
|
38
|
+
* projected when the scene is isometric — a horizontal rail reads as
|
|
39
|
+
* south-east/north-west there, a vertical one as south-west/north-east.
|
|
40
|
+
*/
|
|
41
|
+
getAnimationFacing() {
|
|
42
|
+
const vertical = this.axis === 'vertical';
|
|
43
|
+
const lx = vertical ? 0 : this.dir;
|
|
44
|
+
const ly = vertical ? this.dir : 0;
|
|
45
|
+
const screen = this.game.projection === 'isometric' ? projectIsometric(lx, ly) : { x: lx, y: ly };
|
|
46
|
+
return facingForInput(Math.sign(screen.x), Math.sign(screen.y)) ?? 's';
|
|
47
|
+
}
|
|
29
48
|
/** One patrol step: advance, turn at the rail's ends, flip the sprite. */
|
|
30
49
|
step(dt) {
|
|
31
50
|
const pos = this.entity.position;
|
|
@@ -42,16 +61,32 @@ export class Patrol extends Component {
|
|
|
42
61
|
this.dir = 1;
|
|
43
62
|
}
|
|
44
63
|
pos[key] = next;
|
|
45
|
-
|
|
64
|
+
// With a directional contract installed the clip carries the mirroring
|
|
65
|
+
// (flipX); a scale flip on top would undo it.
|
|
66
|
+
if (!vertical && !installedDirectionalAnimation())
|
|
46
67
|
this.entity.scale.x = this.dir;
|
|
47
68
|
}
|
|
48
69
|
}
|
|
49
70
|
/** The state graph new patrolling characters start with, as prefab data. */
|
|
50
71
|
export const PATROLLER_STATE_GRAPH = {
|
|
51
72
|
initial: 'walk',
|
|
52
|
-
states: {
|
|
73
|
+
states: {
|
|
74
|
+
walk: {},
|
|
75
|
+
hurt: { transitions: [{ on: `timer:${HURT_SECONDS}`, to: 'walk' }] },
|
|
76
|
+
// The death pose, resolved directionally like every other clip.
|
|
77
|
+
dead: { clip: 'death' },
|
|
78
|
+
// Death first: a lethal hit must never land in hurt. The death edge is
|
|
79
|
+
// also what makes Health hand the death to this graph instead of
|
|
80
|
+
// destroying the entity on the spot.
|
|
81
|
+
'*': {
|
|
82
|
+
transitions: [
|
|
83
|
+
{ on: 'signal:death', to: 'dead' },
|
|
84
|
+
{ on: 'signal:hurt', to: 'hurt' },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
53
88
|
};
|
|
54
|
-
// The walking-critter role.
|
|
89
|
+
// The walking-critter role. Walks, flinches, dies; give patrolling
|
|
55
90
|
// characters more states by registering on top (defineStates('patroller',
|
|
56
91
|
// { chasing: {...} })) plus prefab data.
|
|
57
92
|
export const PATROLLER_ROLE = {
|
|
@@ -65,5 +100,14 @@ export const PATROLLER_ROLE = {
|
|
|
65
100
|
entity.get(Patrol)?.step(dt);
|
|
66
101
|
},
|
|
67
102
|
},
|
|
103
|
+
// The rail stops for the flinch: no step, nothing else to do.
|
|
104
|
+
hurt: { onUpdate() { } },
|
|
105
|
+
// Holds the death pose for a beat, then the body goes away for good.
|
|
106
|
+
dead: {
|
|
107
|
+
onUpdate({ entity, fsm }) {
|
|
108
|
+
if (fsm.elapsed >= DEATH_SECONDS)
|
|
109
|
+
entity.destroy();
|
|
110
|
+
},
|
|
111
|
+
},
|
|
68
112
|
},
|
|
69
113
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waica/behaviors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.9.0"
|
|
23
23
|
}
|
|
24
24
|
}
|