@waica/cli 0.7.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/README.md +1 -1
- package/dist/editor/assets/{cssMode-eS4pAOyh.js → cssMode-DM5x3eXD.js} +1 -1
- package/dist/editor/assets/{freemarker2-FMQjZKQk.js → freemarker2-CmlzUOOI.js} +1 -1
- package/dist/editor/assets/{handlebars-dDYOnqat.js → handlebars-Dlz-T3Pu.js} +1 -1
- package/dist/editor/assets/{html-Ba6xZh4T.js → html-B-NfgvUd.js} +1 -1
- package/dist/editor/assets/{htmlMode-DC9qJr2p.js → htmlMode-K4hF3BAu.js} +1 -1
- package/dist/editor/assets/{index-CoyeInOT.js → index-Ca2oPxGt.js} +4626 -1104
- package/dist/editor/assets/{index-Cy5pmBhN.css → index-CzarIYLW.css} +1 -1
- package/dist/editor/assets/{javascript-DOwFvmvM.js → javascript-COj32tw-.js} +1 -1
- package/dist/editor/assets/{jsonMode-CcG7zlSf.js → jsonMode-DyL12Tvg.js} +1 -1
- package/dist/editor/assets/{liquid-Dfp71r7J.js → liquid-CGaRZ9Iu.js} +1 -1
- package/dist/editor/assets/{mdx-Ci7vVeht.js → mdx-L4fRd4xO.js} +1 -1
- package/dist/editor/assets/{python-Bluv3Y9Q.js → python-Z6FCrHUG.js} +1 -1
- package/dist/editor/assets/{razor-BtwTXMaK.js → razor-6XSfF-a3.js} +1 -1
- package/dist/editor/assets/{tsMode-D3_D-H85.js → tsMode-C24rNZgD.js} +1 -1
- package/dist/editor/assets/{typescript-Ch_PS5f7.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-CI_bH_MX.js → xml-DJCue8rm.js} +1 -1
- package/dist/editor/assets/{yaml-LD9GTZyy.js → yaml-DiJf3V99.js} +1 -1
- package/dist/editor/index.html +2 -2
- package/dist/mcp/known-archetypes.js +5 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/assets/ATTRIBUTION.md +93 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-crate.png +0 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-ground.png +0 -0
- 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-rock.png +0 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/assets/waica-iso-tree.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 +7 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/animation.js +17 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/art.d.ts +3 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/art.js +10 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/bundle.d.ts +2 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/bundle.js +9 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/controls.d.ts +3 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/controls.js +16 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/index.d.ts +24 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/index.js +15 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/manifest.d.ts +16 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/manifest.js +30 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/prefabs.d.ts +22 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/prefabs.js +215 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry-data.d.ts +3 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry-data.js +47 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry.d.ts +4 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/registry.js +17 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/scene-default.d.ts +5 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/scene-default.js +59 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/ui.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/dist/ui.js +30 -0
- package/dist/mcp/node_modules/@waica/archetype-isometric/package.json +30 -0
- 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/dist/registry-data.js +2 -1
- package/dist/mcp/node_modules/@waica/archetype-topdown/dist/ui.js +2 -19
- 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 +64 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/grid-motor.js +77 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/grid-player-role.d.ts +20 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/grid-player-role.js +133 -0
- 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 +4 -1
- package/dist/mcp/node_modules/@waica/behaviors/dist/index.js +4 -1
- package/dist/mcp/node_modules/@waica/behaviors/dist/interactable.d.ts +3 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/interactable.js +25 -2
- package/dist/mcp/node_modules/@waica/behaviors/dist/iso-motor.d.ts +21 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/iso-motor.js +24 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/iso-player-states.d.ts +5 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/iso-player-states.js +9 -0
- 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/dist/topdown-motor.d.ts +7 -61
- package/dist/mcp/node_modules/@waica/behaviors/dist/topdown-motor.js +8 -79
- package/dist/mcp/node_modules/@waica/behaviors/dist/topdown-player-states.d.ts +4 -17
- package/dist/mcp/node_modules/@waica/behaviors/dist/topdown-player-states.js +10 -82
- package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
- package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +2 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +19 -1
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +35 -7
- package/dist/mcp/node_modules/@waica/engine/dist/components/dynamic-body.js +2 -4
- package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.d.ts +19 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.js +44 -8
- package/dist/mcp/node_modules/@waica/engine/dist/components/tilemap.d.ts +113 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/tilemap.js +316 -0
- package/dist/mcp/node_modules/@waica/engine/dist/entity.d.ts +3 -0
- package/dist/mcp/node_modules/@waica/engine/dist/entity.js +13 -1
- package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +4 -0
- package/dist/mcp/node_modules/@waica/engine/dist/game.js +41 -9
- package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +9 -0
- package/dist/mcp/node_modules/@waica/engine/dist/index.js +5 -0
- package/dist/mcp/node_modules/@waica/engine/dist/projection.d.ts +14 -0
- package/dist/mcp/node_modules/@waica/engine/dist/projection.js +30 -0
- package/dist/mcp/node_modules/@waica/engine/dist/scene-solids.d.ts +16 -0
- package/dist/mcp/node_modules/@waica/engine/dist/scene-solids.js +28 -0
- package/dist/mcp/node_modules/@waica/engine/dist/scene.d.ts +3 -1
- package/dist/mcp/node_modules/@waica/engine/dist/solid-axis.js +2 -4
- package/dist/mcp/node_modules/@waica/engine/dist/sprite-placement.d.ts +22 -0
- package/dist/mcp/node_modules/@waica/engine/dist/sprite-placement.js +14 -0
- package/dist/mcp/node_modules/@waica/engine/dist/tilemap-grid.d.ts +26 -0
- package/dist/mcp/node_modules/@waica/engine/dist/tilemap-grid.js +41 -0
- package/dist/mcp/node_modules/@waica/engine/package.json +1 -1
- package/dist/mcp/package-resolver.js +4 -0
- package/dist/mcp/project-component-runner.js +1 -0
- package/dist/mcp/template/src/stats.json +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ProjectedPoint } from '@waica/engine';
|
|
2
|
+
/** The eight screen-relative facings a directional contract can declare. */
|
|
3
|
+
export type ScreenFacing = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
|
|
4
|
+
export declare const SCREEN_FACINGS: readonly ScreenFacing[];
|
|
5
|
+
/**
|
|
6
|
+
* The eight-way facing a screen-relative input reads as — signs only, so a
|
|
7
|
+
* tiny diagonal component still counts. Undefined for no input, which lets
|
|
8
|
+
* the caller keep whatever it was facing.
|
|
9
|
+
*/
|
|
10
|
+
export declare function facingForInput(inputX: number, inputY: number): ScreenFacing | undefined;
|
|
11
|
+
/** The screen sign pair behind a facing; undefined for a name the table lacks. */
|
|
12
|
+
export declare function facingVector(facing: string): ProjectedPoint | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* The unit vector a facing points along in the scene's logical space: the
|
|
15
|
+
* screen vector itself without a projection, or its image on the logical
|
|
16
|
+
* diamond under the isometric one — the same mapping the motors move along.
|
|
17
|
+
*/
|
|
18
|
+
export declare function logicalDirection(facing: string, projection: 'isometric' | null): ProjectedPoint | undefined;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { screenInputToLogical } from '@waica/engine';
|
|
2
|
+
export const SCREEN_FACINGS = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
|
|
3
|
+
/** Facing → screen sign pair (x right, y up), the inverse of facingForInput. */
|
|
4
|
+
const FACING_VECTORS = {
|
|
5
|
+
n: { x: 0, y: 1 },
|
|
6
|
+
ne: { x: 1, y: 1 },
|
|
7
|
+
e: { x: 1, y: 0 },
|
|
8
|
+
se: { x: 1, y: -1 },
|
|
9
|
+
s: { x: 0, y: -1 },
|
|
10
|
+
sw: { x: -1, y: -1 },
|
|
11
|
+
w: { x: -1, y: 0 },
|
|
12
|
+
nw: { x: -1, y: 1 },
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The eight-way facing a screen-relative input reads as — signs only, so a
|
|
16
|
+
* tiny diagonal component still counts. Undefined for no input, which lets
|
|
17
|
+
* the caller keep whatever it was facing.
|
|
18
|
+
*/
|
|
19
|
+
export function facingForInput(inputX, inputY) {
|
|
20
|
+
if (inputX === 0 && inputY === 0)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (inputY > 0) {
|
|
23
|
+
if (inputX > 0)
|
|
24
|
+
return 'ne';
|
|
25
|
+
if (inputX < 0)
|
|
26
|
+
return 'nw';
|
|
27
|
+
return 'n';
|
|
28
|
+
}
|
|
29
|
+
if (inputY < 0) {
|
|
30
|
+
if (inputX > 0)
|
|
31
|
+
return 'se';
|
|
32
|
+
if (inputX < 0)
|
|
33
|
+
return 'sw';
|
|
34
|
+
return 's';
|
|
35
|
+
}
|
|
36
|
+
return inputX > 0 ? 'e' : 'w';
|
|
37
|
+
}
|
|
38
|
+
/** The screen sign pair behind a facing; undefined for a name the table lacks. */
|
|
39
|
+
export function facingVector(facing) {
|
|
40
|
+
const vector = FACING_VECTORS[facing];
|
|
41
|
+
return vector ? { ...vector } : undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The unit vector a facing points along in the scene's logical space: the
|
|
45
|
+
* screen vector itself without a projection, or its image on the logical
|
|
46
|
+
* diamond under the isometric one — the same mapping the motors move along.
|
|
47
|
+
*/
|
|
48
|
+
export function logicalDirection(facing, projection) {
|
|
49
|
+
const vector = facingVector(facing);
|
|
50
|
+
if (!vector)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (projection === 'isometric')
|
|
53
|
+
return screenInputToLogical(vector.x, vector.y);
|
|
54
|
+
const length = Math.hypot(vector.x, vector.y);
|
|
55
|
+
return { x: vector.x / length, y: vector.y / length };
|
|
56
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Component, type AnimationFacingProvider, type CameraVelocity, type CameraVelocityProvider } from '@waica/engine';
|
|
2
|
+
/** Shared velocity, collision and provider behavior for square-grid motors. */
|
|
3
|
+
export declare abstract class GridMotor<Facing extends string> extends Component implements CameraVelocityProvider, AnimationFacingProvider {
|
|
4
|
+
static displayName: string;
|
|
5
|
+
static params: {
|
|
6
|
+
moveSpeed: {
|
|
7
|
+
label: string;
|
|
8
|
+
min: number;
|
|
9
|
+
max: number;
|
|
10
|
+
step: number;
|
|
11
|
+
};
|
|
12
|
+
acceleration: {
|
|
13
|
+
label: string;
|
|
14
|
+
min: number;
|
|
15
|
+
max: number;
|
|
16
|
+
step: number;
|
|
17
|
+
};
|
|
18
|
+
deceleration: {
|
|
19
|
+
label: string;
|
|
20
|
+
min: number;
|
|
21
|
+
max: number;
|
|
22
|
+
step: number;
|
|
23
|
+
};
|
|
24
|
+
walkThreshold: {
|
|
25
|
+
label: string;
|
|
26
|
+
min: number;
|
|
27
|
+
max: number;
|
|
28
|
+
step: number;
|
|
29
|
+
};
|
|
30
|
+
knockbackSpeed: {
|
|
31
|
+
label: string;
|
|
32
|
+
min: number;
|
|
33
|
+
max: number;
|
|
34
|
+
step: number;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
static transient: string[];
|
|
38
|
+
moveSpeed: number;
|
|
39
|
+
acceleration: number;
|
|
40
|
+
deceleration: number;
|
|
41
|
+
/** Speed above this reads as walking (the idle ↔ walk edge). */
|
|
42
|
+
walkThreshold: number;
|
|
43
|
+
/** How hard a hit shoves the body away from its source (the hurt state). */
|
|
44
|
+
knockbackSpeed: number;
|
|
45
|
+
/** The character's AABB hitbox — a footprint, shorter than the sprite. */
|
|
46
|
+
hitboxWidth: number;
|
|
47
|
+
hitboxHeight: number;
|
|
48
|
+
vx: number;
|
|
49
|
+
vy: number;
|
|
50
|
+
abstract facing: Facing;
|
|
51
|
+
/** The scene camera reads follow velocity through this explicit seam. */
|
|
52
|
+
getCameraVelocity(): CameraVelocity;
|
|
53
|
+
/** The StateMachine resolves directional clips through this explicit seam. */
|
|
54
|
+
getAnimationFacing(): string;
|
|
55
|
+
/** Accelerates each axis toward an already-normalized logical direction. */
|
|
56
|
+
protected accelerateToward(x: number, y: number, dt: number): void;
|
|
57
|
+
/** Live speed, for the idle ↔ walk edge. */
|
|
58
|
+
speed(): number;
|
|
59
|
+
/** Integrates velocity and resolves each logical axis against Solids. */
|
|
60
|
+
step(dt: number): void;
|
|
61
|
+
halt(): void;
|
|
62
|
+
private resolveAxis;
|
|
63
|
+
private collisionBody;
|
|
64
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Component, resolveSolidAxis, THREE, } from '@waica/engine';
|
|
2
|
+
/** Shared velocity, collision and provider behavior for square-grid motors. */
|
|
3
|
+
export class GridMotor extends Component {
|
|
4
|
+
static displayName = 'Motor';
|
|
5
|
+
static params = {
|
|
6
|
+
moveSpeed: { label: 'Speed', min: 1, max: 30, step: 0.5 },
|
|
7
|
+
acceleration: { label: 'Acceleration', min: 5, max: 200, step: 5 },
|
|
8
|
+
deceleration: { label: 'Deceleration', min: 5, max: 200, step: 5 },
|
|
9
|
+
walkThreshold: { label: 'Walk threshold', min: 0, max: 5, step: 0.1 },
|
|
10
|
+
knockbackSpeed: { label: 'Knockback speed', min: 0, max: 30, step: 0.5 },
|
|
11
|
+
};
|
|
12
|
+
static transient = ['vx', 'vy', 'facing'];
|
|
13
|
+
moveSpeed = 6;
|
|
14
|
+
acceleration = 60;
|
|
15
|
+
deceleration = 80;
|
|
16
|
+
/** Speed above this reads as walking (the idle ↔ walk edge). */
|
|
17
|
+
walkThreshold = 0.5;
|
|
18
|
+
/** How hard a hit shoves the body away from its source (the hurt state). */
|
|
19
|
+
knockbackSpeed = 8;
|
|
20
|
+
/** The character's AABB hitbox — a footprint, shorter than the sprite. */
|
|
21
|
+
hitboxWidth = 0.9;
|
|
22
|
+
hitboxHeight = 0.6;
|
|
23
|
+
vx = 0;
|
|
24
|
+
vy = 0;
|
|
25
|
+
/** The scene camera reads follow velocity through this explicit seam. */
|
|
26
|
+
getCameraVelocity() {
|
|
27
|
+
return { vx: this.vx, vy: this.vy };
|
|
28
|
+
}
|
|
29
|
+
/** The StateMachine resolves directional clips through this explicit seam. */
|
|
30
|
+
getAnimationFacing() {
|
|
31
|
+
return this.facing;
|
|
32
|
+
}
|
|
33
|
+
/** Accelerates each axis toward an already-normalized logical direction. */
|
|
34
|
+
accelerateToward(x, y, dt) {
|
|
35
|
+
const rateX = x !== 0 ? this.acceleration : this.deceleration;
|
|
36
|
+
const rateY = y !== 0 ? this.acceleration : this.deceleration;
|
|
37
|
+
this.vx = THREE.MathUtils.damp(this.vx, x * this.moveSpeed, rateX / this.moveSpeed, dt);
|
|
38
|
+
this.vy = THREE.MathUtils.damp(this.vy, y * this.moveSpeed, rateY / this.moveSpeed, dt);
|
|
39
|
+
}
|
|
40
|
+
/** Live speed, for the idle ↔ walk edge. */
|
|
41
|
+
speed() {
|
|
42
|
+
return Math.hypot(this.vx, this.vy);
|
|
43
|
+
}
|
|
44
|
+
/** Integrates velocity and resolves each logical axis against Solids. */
|
|
45
|
+
step(dt) {
|
|
46
|
+
const position = this.entity.position;
|
|
47
|
+
const previousX = position.x;
|
|
48
|
+
position.x += this.vx * dt;
|
|
49
|
+
if (this.resolveAxis('x', previousX))
|
|
50
|
+
this.vx = 0;
|
|
51
|
+
const previousY = position.y;
|
|
52
|
+
position.y += this.vy * dt;
|
|
53
|
+
if (this.resolveAxis('y', previousY))
|
|
54
|
+
this.vy = 0;
|
|
55
|
+
}
|
|
56
|
+
halt() {
|
|
57
|
+
this.vx = 0;
|
|
58
|
+
this.vy = 0;
|
|
59
|
+
}
|
|
60
|
+
resolveAxis(axis, previous) {
|
|
61
|
+
return resolveSolidAxis({
|
|
62
|
+
entity: this.entity,
|
|
63
|
+
axis,
|
|
64
|
+
previous,
|
|
65
|
+
body: () => this.collisionBody(),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
collisionBody() {
|
|
69
|
+
return {
|
|
70
|
+
x: this.entity.position.x,
|
|
71
|
+
y: this.entity.position.y,
|
|
72
|
+
width: this.hitboxWidth,
|
|
73
|
+
height: this.hitboxHeight,
|
|
74
|
+
shape: 'rectangle',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Component, type ComponentClass, type RoleDefinition, type RoleGraph, type StateContext } from '@waica/engine';
|
|
2
|
+
interface PlayerMotor extends Component {
|
|
3
|
+
walkThreshold: number;
|
|
4
|
+
knockbackSpeed: number;
|
|
5
|
+
facing: string;
|
|
6
|
+
vx: number;
|
|
7
|
+
vy: number;
|
|
8
|
+
run(inputX: number, inputY: number, dt: number): void;
|
|
9
|
+
step(dt: number): void;
|
|
10
|
+
speed(): number;
|
|
11
|
+
halt(): void;
|
|
12
|
+
}
|
|
13
|
+
interface GridPlayerRoleParts {
|
|
14
|
+
graph: RoleGraph;
|
|
15
|
+
update(ctx: StateContext, dt: number): void;
|
|
16
|
+
role: RoleDefinition;
|
|
17
|
+
}
|
|
18
|
+
/** Builds the common player graph and role around one genre-specific grid motor. */
|
|
19
|
+
export declare function createGridPlayerRole<T extends PlayerMotor>(Motor: ComponentClass<T>, description: string): GridPlayerRoleParts;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {} from '@waica/engine';
|
|
2
|
+
import { logicalDirection } from './facing.js';
|
|
3
|
+
import { Health } from './health.js';
|
|
4
|
+
import { interactUpdate } from './interactable.js';
|
|
5
|
+
import { MeleeAttack } from './melee-attack.js';
|
|
6
|
+
import { Respawnable } from './respawnable.js';
|
|
7
|
+
/** Seconds the attack swing and the hurt stun each keep the body from the player. */
|
|
8
|
+
const ATTACK_SECONDS = 0.3;
|
|
9
|
+
const HURT_SECONDS = 0.3;
|
|
10
|
+
/** Builds the common player graph and role around one genre-specific grid motor. */
|
|
11
|
+
export function createGridPlayerRole(Motor, description) {
|
|
12
|
+
const graph = {
|
|
13
|
+
initial: 'idle',
|
|
14
|
+
states: {
|
|
15
|
+
idle: {
|
|
16
|
+
transitions: [
|
|
17
|
+
{ on: 'input:attack', to: 'attack' },
|
|
18
|
+
{ on: 'signal:move', to: 'walk' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
walk: {
|
|
22
|
+
transitions: [
|
|
23
|
+
{ on: 'input:attack', to: 'attack' },
|
|
24
|
+
{ on: 'signal:stop', to: 'idle' },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
// One swing, body frozen; the strike itself lands on entry.
|
|
28
|
+
attack: {
|
|
29
|
+
transitions: [{ on: `timer:${ATTACK_SECONDS}`, to: 'idle' }],
|
|
30
|
+
},
|
|
31
|
+
// Knocked back and stunned for a beat; the '*' edge below gets it here.
|
|
32
|
+
hurt: {
|
|
33
|
+
transitions: [{ on: `timer:${HURT_SECONDS}`, to: 'idle' }],
|
|
34
|
+
},
|
|
35
|
+
dead: {
|
|
36
|
+
// The death pose, resolved directionally like every other clip.
|
|
37
|
+
clip: 'death',
|
|
38
|
+
transitions: [{ on: 'timer:0.8', to: 'idle' }],
|
|
39
|
+
},
|
|
40
|
+
// Dying and getting hurt are the same from every state, so both edges
|
|
41
|
+
// live on '*' — death first, so a lethal hit never lands in hurt. The
|
|
42
|
+
// death edge is also the contract Health checks before signalling: a
|
|
43
|
+
// graph without it gets its entity destroyed instead.
|
|
44
|
+
'*': {
|
|
45
|
+
transitions: [
|
|
46
|
+
{ on: 'signal:death', to: 'dead' },
|
|
47
|
+
{ on: 'signal:hurt', to: 'hurt' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const update = ({ entity, game, fsm }, dt) => {
|
|
53
|
+
const motor = entity.get(Motor);
|
|
54
|
+
if (!motor)
|
|
55
|
+
return;
|
|
56
|
+
motor.run(game.input.axis('left', 'right'), game.input.axis('down', 'up'), dt);
|
|
57
|
+
motor.step(dt);
|
|
58
|
+
fsm.signal(motor.speed() > motor.walkThreshold ? 'move' : 'stop');
|
|
59
|
+
};
|
|
60
|
+
const role = {
|
|
61
|
+
description,
|
|
62
|
+
driver: Motor.componentName,
|
|
63
|
+
graph,
|
|
64
|
+
signals: {
|
|
65
|
+
move: 'moving',
|
|
66
|
+
stop: 'standing still',
|
|
67
|
+
},
|
|
68
|
+
states: {
|
|
69
|
+
// Always-hook: the interact lookup must survive custom states like an
|
|
70
|
+
// attack, so it runs in every state.
|
|
71
|
+
'*': {
|
|
72
|
+
onUpdate(ctx) {
|
|
73
|
+
interactUpdate(ctx);
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
// Fallback: a custom state without its own onUpdate keeps the full body
|
|
77
|
+
// update — its file only has to say what makes it special.
|
|
78
|
+
default: { onUpdate: update },
|
|
79
|
+
idle: { onUpdate: update },
|
|
80
|
+
walk: { onUpdate: update },
|
|
81
|
+
attack: {
|
|
82
|
+
onEnter({ entity }) {
|
|
83
|
+
const motor = entity.get(Motor);
|
|
84
|
+
if (!motor)
|
|
85
|
+
return;
|
|
86
|
+
motor.halt();
|
|
87
|
+
entity.get(MeleeAttack)?.strike(motor.facing);
|
|
88
|
+
},
|
|
89
|
+
// Explicit no-op: without it the state would inherit the default
|
|
90
|
+
// body update and keep walking through the swing.
|
|
91
|
+
onUpdate() { },
|
|
92
|
+
},
|
|
93
|
+
hurt: {
|
|
94
|
+
onEnter({ entity, game }) {
|
|
95
|
+
const motor = entity.get(Motor);
|
|
96
|
+
if (!motor)
|
|
97
|
+
return;
|
|
98
|
+
const source = entity.get(Health)?.lastDamageSource;
|
|
99
|
+
const dx = source ? entity.position.x - source.position.x : 0;
|
|
100
|
+
const dy = source ? entity.position.y - source.position.y : 0;
|
|
101
|
+
const distance = Math.hypot(dx, dy);
|
|
102
|
+
let away = distance > 0 ? { x: dx / distance, y: dy / distance } : undefined;
|
|
103
|
+
if (!away) {
|
|
104
|
+
// No source (or one standing on top of us): recoil from the facing.
|
|
105
|
+
const forward = logicalDirection(motor.facing, game.projection);
|
|
106
|
+
away = forward ? { x: -forward.x, y: -forward.y } : { x: 0, y: 0 };
|
|
107
|
+
}
|
|
108
|
+
motor.vx = away.x * motor.knockbackSpeed;
|
|
109
|
+
motor.vy = away.y * motor.knockbackSpeed;
|
|
110
|
+
},
|
|
111
|
+
// The shove resolves against Solids like any movement; no input.
|
|
112
|
+
onUpdate({ entity }, dt) {
|
|
113
|
+
entity.get(Motor)?.step(dt);
|
|
114
|
+
},
|
|
115
|
+
onExit({ entity }) {
|
|
116
|
+
entity.get(Motor)?.halt();
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
// Coming back is what leaving death means, so it hangs off onExit: any
|
|
120
|
+
// other way out of this state (a project's own edge) revives too.
|
|
121
|
+
dead: {
|
|
122
|
+
// A state without its own onUpdate falls back to the role's default
|
|
123
|
+
// body update, so this no-op keeps the player still for the death beat.
|
|
124
|
+
onUpdate() { },
|
|
125
|
+
onExit({ entity }) {
|
|
126
|
+
entity.get(Respawnable)?.respawn();
|
|
127
|
+
entity.get(Health)?.heal(Infinity);
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
return { graph, update, role };
|
|
133
|
+
}
|
|
@@ -41,19 +41,40 @@ export declare class Health extends Component {
|
|
|
41
41
|
max: number;
|
|
42
42
|
step: number;
|
|
43
43
|
};
|
|
44
|
+
stat: {
|
|
45
|
+
label: string;
|
|
46
|
+
ref: 'stat';
|
|
47
|
+
};
|
|
44
48
|
};
|
|
45
49
|
static transient: string[];
|
|
46
50
|
max: number;
|
|
47
51
|
/** Seconds of immunity granted by taking a hit. 0 disables i-frames. */
|
|
48
52
|
invulnerability: number;
|
|
53
|
+
/**
|
|
54
|
+
* Stat that mirrors `current` (a HUD binds to it with `{{stat}}`). Empty
|
|
55
|
+
* publishes nothing: the stat is the project's to declare.
|
|
56
|
+
*/
|
|
57
|
+
stat: string;
|
|
49
58
|
/** Health left; 0 is dead. Filled in from max on ready. */
|
|
50
59
|
current: number;
|
|
60
|
+
/** Whoever dealt the last accepted hit, for the state that reacts to it. */
|
|
61
|
+
lastDamageSource: Entity | undefined;
|
|
51
62
|
/** Seconds left in the invulnerability window. */
|
|
52
63
|
private invulnerable;
|
|
64
|
+
/** Seconds into the current blink, for the 10 Hz toggle. */
|
|
65
|
+
private blinkClock;
|
|
66
|
+
/** Whether the node is being flashed: exactly while a window is open. */
|
|
67
|
+
get blinking(): boolean;
|
|
53
68
|
/** Whether the signalled death must be checked on this component's next update. */
|
|
54
69
|
private deathPending;
|
|
55
70
|
/** States the death signal was supposed to reach. */
|
|
56
71
|
private expectedStates;
|
|
72
|
+
/**
|
|
73
|
+
* What a Runtime Snapshot shows: the authored values, the live ones, and
|
|
74
|
+
* the last source by name — projecting the whole Entity would drag its
|
|
75
|
+
* scene node along and bury the numbers that matter.
|
|
76
|
+
*/
|
|
77
|
+
inspectState(): Record<string, unknown>;
|
|
57
78
|
onReady(): void;
|
|
58
79
|
onUpdate(dt: number): void;
|
|
59
80
|
/**
|
|
@@ -73,6 +94,14 @@ export declare class Health extends Component {
|
|
|
73
94
|
damage(amount: number, source?: Entity): void;
|
|
74
95
|
/** Gives health back, capped at max. heal(Infinity) is a full restore. */
|
|
75
96
|
heal(amount: number): void;
|
|
97
|
+
/** Mirrors current into the named stat, when one is named. */
|
|
98
|
+
private publish;
|
|
99
|
+
/**
|
|
100
|
+
* Flashes the node at 10 Hz for as long as the window stays open, and
|
|
101
|
+
* leaves it visible the frame the window closes — the flash is feedback,
|
|
102
|
+
* never a state a hit can leave the entity stuck in.
|
|
103
|
+
*/
|
|
104
|
+
private blink;
|
|
76
105
|
/**
|
|
77
106
|
* Announces the death, then lets the role decide — but only if it says it
|
|
78
107
|
* can: a graph with no death edge would swallow the signal silently, so
|
|
@@ -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) : [];
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
export { PlatformerMotor } from './platformer-motor.js';
|
|
2
2
|
export { PLAYER_ROLE, PLAYER_STATE_GRAPH, playerUpdate } from './player-states.js';
|
|
3
3
|
export { TopDownMotor, type TopDownFacing } from './topdown-motor.js';
|
|
4
|
+
export { IsoMotor, type IsoFacing } from './iso-motor.js';
|
|
4
5
|
export { TOPDOWN_PLAYER_ROLE, TOPDOWN_PLAYER_STATE_GRAPH, topdownPlayerUpdate, } from './topdown-player-states.js';
|
|
5
|
-
export {
|
|
6
|
+
export { ISO_PLAYER_ROLE, ISO_PLAYER_STATE_GRAPH, isoPlayerUpdate, } from './iso-player-states.js';
|
|
7
|
+
export { INTERACTABLE_UI, INTERACTABLE_UI_PIECE, Interactable, interactUpdate, } from './interactable.js';
|
|
6
8
|
export { Collectible } from './collectible.js';
|
|
7
9
|
export { Patrol, PATROLLER_ROLE, PATROLLER_STATE_GRAPH, type PatrolAxis } from './patrol.js';
|
|
8
10
|
export { Chaser, CHASER_ROLE, CHASER_STATE_GRAPH, type ChaserMode } from './chaser.js';
|
|
9
11
|
export { NPC_ROLE, NPC_STATE_GRAPH } from './npc.js';
|
|
10
12
|
export { Hazard, resolveHazardTouch, type HazardTouch } from './hazard.js';
|
|
13
|
+
export { MeleeAttack } from './melee-attack.js';
|
|
11
14
|
export { Health, declaresDeathHandling, deathTargets } from './health.js';
|
|
12
15
|
export { Respawnable } from './respawnable.js';
|
|
13
16
|
export { OutOfBounds } from './out-of-bounds.js';
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
export { PlatformerMotor } from './platformer-motor.js';
|
|
2
2
|
export { PLAYER_ROLE, PLAYER_STATE_GRAPH, playerUpdate } from './player-states.js';
|
|
3
3
|
export { TopDownMotor } from './topdown-motor.js';
|
|
4
|
+
export { IsoMotor } from './iso-motor.js';
|
|
4
5
|
export { TOPDOWN_PLAYER_ROLE, TOPDOWN_PLAYER_STATE_GRAPH, topdownPlayerUpdate, } from './topdown-player-states.js';
|
|
5
|
-
export {
|
|
6
|
+
export { ISO_PLAYER_ROLE, ISO_PLAYER_STATE_GRAPH, isoPlayerUpdate, } from './iso-player-states.js';
|
|
7
|
+
export { INTERACTABLE_UI, INTERACTABLE_UI_PIECE, Interactable, interactUpdate, } from './interactable.js';
|
|
6
8
|
export { Collectible } from './collectible.js';
|
|
7
9
|
export { Patrol, PATROLLER_ROLE, PATROLLER_STATE_GRAPH } from './patrol.js';
|
|
8
10
|
export { Chaser, CHASER_ROLE, CHASER_STATE_GRAPH } from './chaser.js';
|
|
9
11
|
export { NPC_ROLE, NPC_STATE_GRAPH } from './npc.js';
|
|
10
12
|
export { Hazard, resolveHazardTouch } from './hazard.js';
|
|
13
|
+
export { MeleeAttack } from './melee-attack.js';
|
|
11
14
|
export { Health, declaresDeathHandling, deathTargets } from './health.js';
|
|
12
15
|
export { Respawnable } from './respawnable.js';
|
|
13
16
|
export { OutOfBounds } from './out-of-bounds.js';
|
|
@@ -22,6 +22,9 @@ export declare class Interactable extends Component {
|
|
|
22
22
|
line: string;
|
|
23
23
|
radius: number;
|
|
24
24
|
}
|
|
25
|
+
export declare const INTERACTABLE_UI_PIECE = "npc-line";
|
|
26
|
+
/** The UI fragment every archetype using Interactable must register. */
|
|
27
|
+
export declare const INTERACTABLE_UI: Readonly<Record<string, string>>;
|
|
25
28
|
/**
|
|
26
29
|
* The player role's interact lookup, run by its '*' hook in every state:
|
|
27
30
|
* pressing interact near an Interactable publishes its line through the
|
|
@@ -15,6 +15,29 @@ export class Interactable extends Component {
|
|
|
15
15
|
line = 'Hello, traveler!';
|
|
16
16
|
radius = 1.5;
|
|
17
17
|
}
|
|
18
|
+
export const INTERACTABLE_UI_PIECE = 'npc-line';
|
|
19
|
+
/** The UI fragment every archetype using Interactable must register. */
|
|
20
|
+
export const INTERACTABLE_UI = {
|
|
21
|
+
[INTERACTABLE_UI_PIECE]: `<style>
|
|
22
|
+
.npc-line {
|
|
23
|
+
position: absolute;
|
|
24
|
+
left: 50%;
|
|
25
|
+
bottom: 24px;
|
|
26
|
+
transform: translateX(-50%);
|
|
27
|
+
max-width: 70%;
|
|
28
|
+
padding: 10px 18px;
|
|
29
|
+
border-radius: 8px;
|
|
30
|
+
background: #1a1a2ecc;
|
|
31
|
+
border: 1px solid #ffffff2e;
|
|
32
|
+
font: 500 18px system-ui, sans-serif;
|
|
33
|
+
color: #f5f5f5;
|
|
34
|
+
text-shadow: 0 1px 2px #000a;
|
|
35
|
+
user-select: none;
|
|
36
|
+
}
|
|
37
|
+
</style>
|
|
38
|
+
<div class="npc-line">{{npcLine}}</div>
|
|
39
|
+
`,
|
|
40
|
+
};
|
|
18
41
|
/**
|
|
19
42
|
* The player role's interact lookup, run by its '*' hook in every state:
|
|
20
43
|
* pressing interact near an Interactable publishes its line through the
|
|
@@ -37,13 +60,13 @@ export function interactUpdate({ entity, game }) {
|
|
|
37
60
|
}
|
|
38
61
|
}
|
|
39
62
|
if (!nearest) {
|
|
40
|
-
game.ui.hide(
|
|
63
|
+
game.ui.hide(INTERACTABLE_UI_PIECE);
|
|
41
64
|
return;
|
|
42
65
|
}
|
|
43
66
|
if (game.input.justPressed('interact') && !game.input.consumed('interact')) {
|
|
44
67
|
// The press is spent: an input:interact edge needs a NEW press.
|
|
45
68
|
game.input.consume('interact');
|
|
46
69
|
game.stats.set('npcLine', nearest.line);
|
|
47
|
-
game.ui.show(
|
|
70
|
+
game.ui.show(INTERACTABLE_UI_PIECE);
|
|
48
71
|
}
|
|
49
72
|
}
|