@waica/engine 0.26.0 → 0.28.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 +33 -3
- package/dist/animation/clip-player.d.ts +7 -0
- package/dist/animation/clip-player.js +9 -0
- package/dist/animation/clip-target.d.ts +38 -0
- package/dist/animation/clip-target.js +9 -0
- package/dist/animation/pose-hash.d.ts +10 -0
- package/dist/animation/pose-hash.js +43 -0
- package/dist/assets/glb-clip-names.d.ts +8 -0
- package/dist/assets/glb-clip-names.js +49 -0
- package/dist/camera-rig.d.ts +22 -1
- package/dist/camera-rig.js +41 -5
- package/dist/components/animated-sprite.d.ts +12 -2
- package/dist/components/animated-sprite.js +15 -2
- package/dist/components/animator.d.ts +98 -0
- package/dist/components/animator.js +197 -0
- package/dist/components/model.d.ts +8 -0
- package/dist/components/model.js +15 -4
- package/dist/game.d.ts +1 -1
- package/dist/game.js +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/pointer-raycast.d.ts +24 -6
- package/dist/pointer-raycast.js +44 -14
- package/dist/runtime-view-snapshot.js +2 -1
- package/dist/state/state-machine.d.ts +32 -8
- package/dist/state/state-machine.js +88 -27
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from '../animation/directional.js';
|
|
2
|
+
import { isClipTarget } from '../animation/clip-target.js';
|
|
2
3
|
import { Component } from '../component.js';
|
|
3
|
-
import { AnimatedSprite } from '../components/animated-sprite.js';
|
|
4
4
|
import { MAX_CHAINED_HOPS, SIMULATION_TIME_EPSILON } from '../fixed-step.js';
|
|
5
5
|
import { closestLogicSet, logicSet, registeredLogicSets, } from './hooks.js';
|
|
6
6
|
/** Whether one trigger fires. Unknown or malformed triggers never fire. */
|
|
@@ -23,6 +23,10 @@ export function evaluateTrigger(on, env) {
|
|
|
23
23
|
return env.elapsed + SIMULATION_TIME_EPSILON >= Number(arg);
|
|
24
24
|
if (kind === 'signal')
|
|
25
25
|
return env.signals.has(arg);
|
|
26
|
+
// The target updates after the machine (updateAfter: ['StateMachine']), so
|
|
27
|
+
// this fires on the Simulation Step after the clip finished, never for a loop.
|
|
28
|
+
if (kind === 'clip')
|
|
29
|
+
return arg === 'end' && env.clipFinished;
|
|
26
30
|
return false;
|
|
27
31
|
}
|
|
28
32
|
/** The first firing transition: the state's own edges, then '*''s. */
|
|
@@ -46,9 +50,10 @@ export function phaseHooks(hooks, fallback, phase) {
|
|
|
46
50
|
* logic — only the active state's code moves the body, so behaviors
|
|
47
51
|
* never fight over velocity. States and transitions are prefab data;
|
|
48
52
|
* state code lives in the role's logic set (defineRole / defineStates)
|
|
49
|
-
* picked by `role`. Entering a state plays the
|
|
50
|
-
*
|
|
51
|
-
*
|
|
53
|
+
* picked by `role`. Entering a state plays the clip of the same name (or
|
|
54
|
+
* the state's `clip` override) through the entity's Clip Target — the
|
|
55
|
+
* first sibling that plays clips, an AnimatedSprite in 2D or an Animator
|
|
56
|
+
* in 3D (issue #166); a missing clip warns and keeps the previous one.
|
|
52
57
|
*/
|
|
53
58
|
export class StateMachine extends Component {
|
|
54
59
|
static componentName = 'StateMachine';
|
|
@@ -62,7 +67,9 @@ export class StateMachine extends Component {
|
|
|
62
67
|
'instanceHooks',
|
|
63
68
|
'signals',
|
|
64
69
|
'warnedClips',
|
|
70
|
+
'warnedFlips',
|
|
65
71
|
'lastFacing',
|
|
72
|
+
'requestedClip',
|
|
66
73
|
];
|
|
67
74
|
/** The character's role — names the logic set providing its state code. */
|
|
68
75
|
role = '';
|
|
@@ -76,8 +83,17 @@ export class StateMachine extends Component {
|
|
|
76
83
|
instanceHooks = new Map();
|
|
77
84
|
signals = new Set();
|
|
78
85
|
warnedClips = new Set();
|
|
86
|
+
/** States already warned that their mirrored clip met a target that cannot flip. */
|
|
87
|
+
warnedFlips = new Set();
|
|
79
88
|
/** Facing last used to resolve a directional clip — detects a turn mid-state. */
|
|
80
89
|
lastFacing;
|
|
90
|
+
/**
|
|
91
|
+
* The clip the current state asked its Clip Target for, undefined when it
|
|
92
|
+
* asked for nothing (no target, or a clip the target lacks). `clip:end`
|
|
93
|
+
* reads only this clip: a one-shot left finished by an earlier state never
|
|
94
|
+
* counts for a state that could not play its own.
|
|
95
|
+
*/
|
|
96
|
+
requestedClip;
|
|
81
97
|
onReady() {
|
|
82
98
|
if (this.role && !logicSet(this.role)) {
|
|
83
99
|
const sets = registeredLogicSets();
|
|
@@ -111,10 +127,12 @@ export class StateMachine extends Component {
|
|
|
111
127
|
this.run('*', 'onUpdate', dt);
|
|
112
128
|
this.run(this.current, 'onUpdate', dt);
|
|
113
129
|
this.elapsed += dt;
|
|
130
|
+
// The target is found once per step: entering a state never changes it.
|
|
131
|
+
const target = this.clipTarget();
|
|
114
132
|
// Chained transitions settle within the frame (e.g. land → idle → run),
|
|
115
133
|
// capped so a degenerate cyclic graph can't hang the loop.
|
|
116
134
|
for (let hops = 0; hops < MAX_CHAINED_HOPS; hops++) {
|
|
117
|
-
const edge = nextTransition(this.states, this.current, this.env());
|
|
135
|
+
const edge = nextTransition(this.states, this.current, this.env(target));
|
|
118
136
|
// A '*' edge is re-merged against whatever state the loop just
|
|
119
137
|
// entered, so a still-queued signal (signals.clear() only runs after
|
|
120
138
|
// this loop) keeps firing every remaining hop. Without this guard
|
|
@@ -145,11 +163,12 @@ export class StateMachine extends Component {
|
|
|
145
163
|
return;
|
|
146
164
|
this.runCollision(state, other);
|
|
147
165
|
}
|
|
148
|
-
env() {
|
|
166
|
+
env(target) {
|
|
149
167
|
return {
|
|
150
168
|
justPressed: (action) => this.game.input.justPressed(action) && !this.game.input.consumed(action),
|
|
151
169
|
elapsed: this.elapsed,
|
|
152
170
|
signals: this.signals,
|
|
171
|
+
clipFinished: this.requestedClip !== undefined && target?.current === this.requestedClip && target.clipFinished,
|
|
153
172
|
};
|
|
154
173
|
}
|
|
155
174
|
hooksFor(state) {
|
|
@@ -182,41 +201,74 @@ export class StateMachine extends Component {
|
|
|
182
201
|
this.run(state, 'onEnter');
|
|
183
202
|
}
|
|
184
203
|
playClip(state) {
|
|
185
|
-
|
|
186
|
-
|
|
204
|
+
this.requestedClip = undefined;
|
|
205
|
+
const target = this.clipTarget();
|
|
206
|
+
if (!target)
|
|
187
207
|
return;
|
|
188
208
|
const clip = this.states[state]?.clip ?? state;
|
|
189
|
-
if (this.playDirectionalClip(
|
|
209
|
+
if (this.playDirectionalClip(target, clip))
|
|
190
210
|
return;
|
|
191
|
-
|
|
192
|
-
|
|
211
|
+
// A target whose clips are pending (an Animator whose glb is still
|
|
212
|
+
// loading) is asked anyway and remembers the request (issue #166).
|
|
213
|
+
if (target.clipsPending || target.clipNames.includes(clip)) {
|
|
214
|
+
target.play(clip);
|
|
215
|
+
this.requestedClip = clip;
|
|
193
216
|
}
|
|
194
217
|
else if (!this.warnedClips.has(state)) {
|
|
195
218
|
this.warnedClips.add(state);
|
|
196
219
|
console.warn(`[waica] "${this.entity.name}": no clip "${clip}" for state "${state}" — ` +
|
|
197
|
-
`keeping "${
|
|
220
|
+
`keeping "${target.current ?? 'none'}"`);
|
|
198
221
|
}
|
|
199
222
|
}
|
|
223
|
+
/** The Clip Target: the first sibling that plays clips (duck-typed, like the facing provider). */
|
|
224
|
+
clipTarget() {
|
|
225
|
+
return this.entity.components.find((c) => isClipTarget(c));
|
|
226
|
+
}
|
|
200
227
|
/**
|
|
201
228
|
* Directional resolution: with an installed contract AND a sibling that
|
|
202
229
|
* reports facing, plays state × facing (mirroring included). Returns false
|
|
203
230
|
* to keep the name-based path — no contract, no facing, no playable clip.
|
|
204
231
|
*/
|
|
205
|
-
playDirectionalClip(
|
|
232
|
+
playDirectionalClip(target, clip) {
|
|
233
|
+
const resolved = this.resolveFacingClip(target, clip);
|
|
234
|
+
if (!resolved)
|
|
235
|
+
return false;
|
|
236
|
+
this.mirror(target, resolved);
|
|
237
|
+
target.play(resolved.clip);
|
|
238
|
+
this.requestedClip = resolved.clip;
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
/** State × facing through the installed contract and the target's clips, remembering the facing used; undefined keeps the name-based path. */
|
|
242
|
+
resolveFacingClip(target, clip) {
|
|
206
243
|
const animation = installedDirectionalAnimation();
|
|
207
244
|
if (!animation)
|
|
208
|
-
return
|
|
245
|
+
return undefined;
|
|
209
246
|
const provider = this.facingProvider();
|
|
210
247
|
if (!provider)
|
|
211
|
-
return
|
|
248
|
+
return undefined;
|
|
212
249
|
const facing = provider.getAnimationFacing();
|
|
213
250
|
this.lastFacing = facing;
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
251
|
+
const names = target.clipNames;
|
|
252
|
+
const resolved = resolveDirectionalClip(animation, names, clip, facing);
|
|
253
|
+
if (!resolved.clip || !names.includes(resolved.clip))
|
|
254
|
+
return undefined;
|
|
255
|
+
return { clip: resolved.clip, flip: resolved.flip };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Applies the resolved mirror. A target without `setFlipX` (an Animator:
|
|
259
|
+
* 3D faces by turning) plays a mirrored clip unmirrored and warns once
|
|
260
|
+
* per state, so a 2D target that forgot its mirror is not silent.
|
|
261
|
+
*/
|
|
262
|
+
mirror(target, resolved) {
|
|
263
|
+
if (target.setFlipX) {
|
|
264
|
+
target.setFlipX(resolved.flip);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (!resolved.flip || this.warnedFlips.has(this.current))
|
|
268
|
+
return;
|
|
269
|
+
this.warnedFlips.add(this.current);
|
|
270
|
+
console.warn(`[waica] "${this.entity.name}": clip "${resolved.clip}" is mirrored for facing "${this.lastFacing ?? ''}", ` +
|
|
271
|
+
'but the Clip Target cannot flip — playing it unmirrored');
|
|
220
272
|
}
|
|
221
273
|
facingProvider() {
|
|
222
274
|
return this.entity.components.find((c) => isAnimationFacingProvider(c));
|
|
@@ -225,19 +277,28 @@ export class StateMachine extends Component {
|
|
|
225
277
|
* playDirectionalClip only runs at enter() time (via playClip), so a
|
|
226
278
|
* facing change that doesn't cross a state boundary — turning while
|
|
227
279
|
* still walking, say — never re-resolves the clip on its own. Catch that
|
|
228
|
-
* here every frame: same state, new facing, re-run the resolution.
|
|
229
|
-
*
|
|
230
|
-
*
|
|
280
|
+
* here every frame: same state, new facing, re-run the resolution. A
|
|
281
|
+
* turn that only mirrors (west playing east flipped) re-applies the
|
|
282
|
+
* mirror and leaves the clip alone — a running one keeps its frame and a
|
|
283
|
+
* finished one-shot keeps its last — so only a turn onto another clip
|
|
284
|
+
* name plays, as re-entering the state would.
|
|
231
285
|
*/
|
|
232
286
|
reresolveOnFacingChange() {
|
|
233
287
|
if (!this.current || !installedDirectionalAnimation())
|
|
234
288
|
return;
|
|
235
|
-
const
|
|
236
|
-
if (!
|
|
289
|
+
const target = this.clipTarget();
|
|
290
|
+
if (!target)
|
|
237
291
|
return;
|
|
238
292
|
const provider = this.facingProvider();
|
|
239
293
|
if (!provider || provider.getAnimationFacing() === this.lastFacing)
|
|
240
294
|
return;
|
|
241
|
-
this.
|
|
295
|
+
const resolved = this.resolveFacingClip(target, this.states[this.current]?.clip ?? this.current);
|
|
296
|
+
if (!resolved)
|
|
297
|
+
return;
|
|
298
|
+
this.mirror(target, resolved);
|
|
299
|
+
if (resolved.clip === target.current)
|
|
300
|
+
return;
|
|
301
|
+
target.play(resolved.clip);
|
|
302
|
+
this.requestedClip = resolved.clip;
|
|
242
303
|
}
|
|
243
304
|
}
|