@waica/behaviors 0.19.0 → 0.19.1
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/click-to-move.js +9 -4
- package/dist/grid-motor.d.ts +3 -2
- package/dist/grid-player-role.js +31 -20
- package/dist/iso-motor.d.ts +1 -1
- package/dist/pathfinding.js +11 -6
- package/dist/topdown-motor.d.ts +1 -1
- package/package.json +2 -2
package/dist/click-to-move.js
CHANGED
|
@@ -75,7 +75,13 @@ function waypointsToward(game, mover, target) {
|
|
|
75
75
|
const plan = planPath(grid, pointOf(mover), target);
|
|
76
76
|
if (!plan)
|
|
77
77
|
return [];
|
|
78
|
-
return plan.cells.map((cell) =>
|
|
78
|
+
return plan.cells.map((cell) => {
|
|
79
|
+
const center = grid.cellCenter(cell);
|
|
80
|
+
// Invariant: planPath only returns cells of the grid it planned on.
|
|
81
|
+
if (!center)
|
|
82
|
+
throw new Error(`Planned cell (${cell.column}, ${cell.row}) is outside its navigation grid`);
|
|
83
|
+
return center;
|
|
84
|
+
});
|
|
79
85
|
}
|
|
80
86
|
function spawnMarker(clickToMove, game, mover, at) {
|
|
81
87
|
const marker = game.spawn(`${mover.name}:click-marker`);
|
|
@@ -101,8 +107,7 @@ function spawnMarker(clickToMove, game, mover, at) {
|
|
|
101
107
|
}
|
|
102
108
|
/** Consumes the leading waypoints already reached; returns the direction to what's left. */
|
|
103
109
|
function followWaypoints(clickToMove, mover, waypoints) {
|
|
104
|
-
|
|
105
|
-
const next = waypoints[0];
|
|
110
|
+
for (let next = waypoints[0]; next !== undefined; next = waypoints[0]) {
|
|
106
111
|
const gap = distance(pointOf(mover), next);
|
|
107
112
|
if (gap <= clickToMove.arrivalTolerance) {
|
|
108
113
|
waypoints.shift();
|
|
@@ -168,7 +173,7 @@ function startOrder(clickToMove, entity, game, pick) {
|
|
|
168
173
|
// An entity without Health/Interactable: fall through to a ground order.
|
|
169
174
|
}
|
|
170
175
|
const waypoints = waypointsToward(game, entity, pick.point);
|
|
171
|
-
const destination = waypoints.
|
|
176
|
+
const destination = waypoints.at(-1) ?? pick.point;
|
|
172
177
|
clickToMove.order = { kind: 'ground', waypoints, destination };
|
|
173
178
|
spawnMarker(clickToMove, game, entity, destination);
|
|
174
179
|
}
|
package/dist/grid-motor.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Component, type AnimationFacingProvider, type CameraVelocity, type CameraVelocityProvider } from '@waica/engine';
|
|
2
2
|
/** Shared velocity, collision and provider behavior for square-grid motors. */
|
|
3
|
-
export declare abstract class GridMotor
|
|
3
|
+
export declare abstract class GridMotor extends Component implements CameraVelocityProvider, AnimationFacingProvider {
|
|
4
4
|
static displayName: string;
|
|
5
5
|
static params: {
|
|
6
6
|
moveSpeed: {
|
|
@@ -47,7 +47,8 @@ export declare abstract class GridMotor<Facing extends string> extends Component
|
|
|
47
47
|
hitboxHeight: number;
|
|
48
48
|
vx: number;
|
|
49
49
|
vy: number;
|
|
50
|
-
|
|
50
|
+
/** Each motor narrows this to its own facing vocabulary. */
|
|
51
|
+
abstract facing: string;
|
|
51
52
|
/** The scene camera reads follow velocity through this explicit seam. */
|
|
52
53
|
getCameraVelocity(): CameraVelocity;
|
|
53
54
|
/** The StateMachine resolves directional clips through this explicit seam. */
|
package/dist/grid-player-role.js
CHANGED
|
@@ -8,9 +8,9 @@ import { Respawnable } from './respawnable.js';
|
|
|
8
8
|
/** Seconds the attack swing and the hurt stun each keep the body from the player. */
|
|
9
9
|
const ATTACK_SECONDS = 0.3;
|
|
10
10
|
const HURT_SECONDS = 0.3;
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
/** The common player graph; fresh per role so no two roles share a mutable graph. */
|
|
12
|
+
function gridPlayerGraph() {
|
|
13
|
+
return {
|
|
14
14
|
initial: 'idle',
|
|
15
15
|
states: {
|
|
16
16
|
idle: {
|
|
@@ -50,7 +50,10 @@ export function createGridPlayerRole(Motor, description) {
|
|
|
50
50
|
},
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
|
-
|
|
53
|
+
}
|
|
54
|
+
/** The body update: keyboard input, else a live Move Order, then the motor step and move/stop signal. */
|
|
55
|
+
function gridPlayerUpdate(Motor) {
|
|
56
|
+
return (ctx, dt) => {
|
|
54
57
|
const { entity, game, fsm } = ctx;
|
|
55
58
|
const motor = entity.get(Motor);
|
|
56
59
|
if (!motor)
|
|
@@ -78,6 +81,26 @@ export function createGridPlayerRole(Motor, description) {
|
|
|
78
81
|
motor.step(dt);
|
|
79
82
|
fsm.signal(motor.speed() > motor.walkThreshold ? 'move' : 'stop');
|
|
80
83
|
};
|
|
84
|
+
}
|
|
85
|
+
/** Sets the motor's knockback velocity away from the last damage source, else back from its facing. */
|
|
86
|
+
function knockBack({ entity, game }, motor) {
|
|
87
|
+
const source = entity.get(Health)?.lastDamageSource;
|
|
88
|
+
const dx = source ? entity.position.x - source.position.x : 0;
|
|
89
|
+
const dy = source ? entity.position.y - source.position.y : 0;
|
|
90
|
+
const distance = Math.hypot(dx, dy);
|
|
91
|
+
let away = distance > 0 ? { x: dx / distance, y: dy / distance } : undefined;
|
|
92
|
+
if (!away) {
|
|
93
|
+
// No source (or one standing on top of us): recoil from the facing.
|
|
94
|
+
const forward = logicalDirection(motor.facing, game.projection);
|
|
95
|
+
away = forward ? { x: -forward.x, y: -forward.y } : { x: 0, y: 0 };
|
|
96
|
+
}
|
|
97
|
+
motor.vx = away.x * motor.knockbackSpeed;
|
|
98
|
+
motor.vy = away.y * motor.knockbackSpeed;
|
|
99
|
+
}
|
|
100
|
+
/** Builds the common player graph and role around one genre-specific grid motor. */
|
|
101
|
+
export function createGridPlayerRole(Motor, description) {
|
|
102
|
+
const graph = gridPlayerGraph();
|
|
103
|
+
const update = gridPlayerUpdate(Motor);
|
|
81
104
|
const role = {
|
|
82
105
|
description,
|
|
83
106
|
driver: Motor.componentName,
|
|
@@ -112,22 +135,10 @@ export function createGridPlayerRole(Motor, description) {
|
|
|
112
135
|
onUpdate() { },
|
|
113
136
|
},
|
|
114
137
|
hurt: {
|
|
115
|
-
onEnter(
|
|
116
|
-
const motor = entity.get(Motor);
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
const source = entity.get(Health)?.lastDamageSource;
|
|
120
|
-
const dx = source ? entity.position.x - source.position.x : 0;
|
|
121
|
-
const dy = source ? entity.position.y - source.position.y : 0;
|
|
122
|
-
const distance = Math.hypot(dx, dy);
|
|
123
|
-
let away = distance > 0 ? { x: dx / distance, y: dy / distance } : undefined;
|
|
124
|
-
if (!away) {
|
|
125
|
-
// No source (or one standing on top of us): recoil from the facing.
|
|
126
|
-
const forward = logicalDirection(motor.facing, game.projection);
|
|
127
|
-
away = forward ? { x: -forward.x, y: -forward.y } : { x: 0, y: 0 };
|
|
128
|
-
}
|
|
129
|
-
motor.vx = away.x * motor.knockbackSpeed;
|
|
130
|
-
motor.vy = away.y * motor.knockbackSpeed;
|
|
138
|
+
onEnter(ctx) {
|
|
139
|
+
const motor = ctx.entity.get(Motor);
|
|
140
|
+
if (motor)
|
|
141
|
+
knockBack(ctx, motor);
|
|
131
142
|
},
|
|
132
143
|
// The shove resolves against Solids like any movement; no input.
|
|
133
144
|
onUpdate({ entity }, dt) {
|
package/dist/iso-motor.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type IsoFacing = ScreenFacing;
|
|
|
7
7
|
* shared GridMotor integrates and collides in logical square-grid space.
|
|
8
8
|
* State code owns the frame; this component has no onUpdate of its own.
|
|
9
9
|
*/
|
|
10
|
-
export declare class IsoMotor extends GridMotor
|
|
10
|
+
export declare class IsoMotor extends GridMotor {
|
|
11
11
|
static componentName: string;
|
|
12
12
|
facing: IsoFacing;
|
|
13
13
|
/**
|
package/dist/pathfinding.js
CHANGED
|
@@ -35,6 +35,13 @@ function neighbors(grid, cell) {
|
|
|
35
35
|
function stepCost(dc, dr) {
|
|
36
36
|
return dc !== 0 && dr !== 0 ? Math.SQRT2 : 1;
|
|
37
37
|
}
|
|
38
|
+
/** The best known cost of a cell A* has already scored (every open or current cell has one). */
|
|
39
|
+
function scoreOf(gScore, cellKey) {
|
|
40
|
+
const score = gScore.get(cellKey);
|
|
41
|
+
if (score === undefined)
|
|
42
|
+
throw new Error(`A* has no score for cell ${cellKey}`);
|
|
43
|
+
return score;
|
|
44
|
+
}
|
|
38
45
|
/** Octile distance: admissible heuristic for 8-way movement with unit/√2 costs. */
|
|
39
46
|
function octile(a, b) {
|
|
40
47
|
const dx = Math.abs(a.column - b.column);
|
|
@@ -51,8 +58,7 @@ export function reachableCells(grid, start) {
|
|
|
51
58
|
return [];
|
|
52
59
|
const visited = new Map([[key(start), start]]);
|
|
53
60
|
const queue = [start];
|
|
54
|
-
|
|
55
|
-
const current = queue.shift();
|
|
61
|
+
for (let current = queue.shift(); current !== undefined; current = queue.shift()) {
|
|
56
62
|
for (const next of neighbors(grid, current)) {
|
|
57
63
|
const k = key(next);
|
|
58
64
|
if (visited.has(k))
|
|
@@ -84,7 +90,7 @@ export function findPath(grid, start, goal) {
|
|
|
84
90
|
let current;
|
|
85
91
|
let bestF = Infinity;
|
|
86
92
|
for (const [k, cell] of open) {
|
|
87
|
-
const f = gScore
|
|
93
|
+
const f = scoreOf(gScore, k) + octile(cell, goal);
|
|
88
94
|
if (f < bestF) {
|
|
89
95
|
bestF = f;
|
|
90
96
|
currentKey = k;
|
|
@@ -96,8 +102,7 @@ export function findPath(grid, start, goal) {
|
|
|
96
102
|
if (currentKey === goalKey) {
|
|
97
103
|
const path = [current];
|
|
98
104
|
let trace = currentKey;
|
|
99
|
-
|
|
100
|
-
const previous = cameFrom.get(trace);
|
|
105
|
+
for (let previous = cameFrom.get(trace); previous; previous = cameFrom.get(trace)) {
|
|
101
106
|
path.unshift(previous);
|
|
102
107
|
trace = key(previous);
|
|
103
108
|
}
|
|
@@ -106,7 +111,7 @@ export function findPath(grid, start, goal) {
|
|
|
106
111
|
}
|
|
107
112
|
open.delete(currentKey);
|
|
108
113
|
closed.add(currentKey);
|
|
109
|
-
const currentG = gScore
|
|
114
|
+
const currentG = scoreOf(gScore, currentKey);
|
|
110
115
|
for (const next of neighbors(grid, current)) {
|
|
111
116
|
const nextKey = key(next);
|
|
112
117
|
if (closed.has(nextKey))
|
package/dist/topdown-motor.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type TopDownFacing = 'n' | 's' | 'e' | 'w';
|
|
|
6
6
|
* step(). Eight-direction input is normalized so diagonals match cardinal
|
|
7
7
|
* speed; movement has no gravity and resolves each axis against Solids.
|
|
8
8
|
*/
|
|
9
|
-
export declare class TopDownMotor extends GridMotor
|
|
9
|
+
export declare class TopDownMotor extends GridMotor {
|
|
10
10
|
static componentName: string;
|
|
11
11
|
/** Four-direction facing; starts looking at the camera. */
|
|
12
12
|
facing: TopDownFacing;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waica/behaviors",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
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.19.
|
|
22
|
+
"@waica/engine": "^0.19.1"
|
|
23
23
|
}
|
|
24
24
|
}
|