@eva/plugin-state-machine 2.1.0-beta.1 → 2.1.0-beta.10

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.
@@ -0,0 +1,362 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var eva_js = require('@eva/eva.js');
6
+ var pluginSignalBus = require('@eva/plugin-signal-bus');
7
+
8
+ /******************************************************************************
9
+ Copyright (c) Microsoft Corporation.
10
+
11
+ Permission to use, copy, modify, and/or distribute this software for any
12
+ purpose with or without fee is hereby granted.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
15
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
16
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
17
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
18
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
19
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
20
+ PERFORMANCE OF THIS SOFTWARE.
21
+ ***************************************************************************** */
22
+
23
+ function __decorate(decorators, target, key, desc) {
24
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
25
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
26
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
27
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
28
+ }
29
+
30
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
31
+ var e = new Error(message);
32
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
33
+ };
34
+
35
+ var StateMachine_1;
36
+ /** 全局信号名:任意 reset() 都会 emit,供观测与跨实例联动 */
37
+ const FSM_RESET_SIGNAL = 'fsm:reset';
38
+ /**
39
+ * StateMachine 组件 — 简易有限状态机。
40
+ *
41
+ * DSL 用法:
42
+ * ```json
43
+ * {
44
+ * "type": "StateMachine",
45
+ * "props": {
46
+ * "initial": "moving",
47
+ * "states": {
48
+ * "moving": { "onEnter": "monster:state-moving",
49
+ * "transitions": [
50
+ * { "on": "monster:hit", "to": "knockback" },
51
+ * { "after": 3000, "to": "resting" }
52
+ * ] },
53
+ * "resting": { "transitions": [{ "after": 1000, "to": "moving" }] },
54
+ * "knockback":{ "transitions": [{ "after": 800, "to": "resting" }] }
55
+ * },
56
+ * "signalChange": "monster:state-change"
57
+ * }
58
+ * }
59
+ * ```
60
+ *
61
+ * 行为:
62
+ * - 进入状态:emit `onEnter` + `signalChange` { from, to }
63
+ * - `transitions[i].on`: 监听信号,信号触发即迁移
64
+ * - `transitions[i].after`: 进入状态 N ms 后自动迁移
65
+ * - `transitions[i].guard`: JS 表达式,以 `ctx` 为变量,假则跳过该规则
66
+ *
67
+ * 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
68
+ */
69
+ exports.StateMachine = StateMachine_1 = class StateMachine extends eva_js.Component {
70
+ constructor() {
71
+ super(...arguments);
72
+ this.states = {};
73
+ this.current = '';
74
+ this.elapsedInState = 0;
75
+ this.subs = [];
76
+ /** 上下文,guard 可读 */
77
+ this.ctx = {};
78
+ /** reset() 用的 initial state 引用;init 时缓存一次,后续不变 */
79
+ this.initialState = '';
80
+ }
81
+ init(params) {
82
+ var _a, _b;
83
+ if (!params)
84
+ return;
85
+ this.states = (_a = params.states) !== null && _a !== void 0 ? _a : {};
86
+ this.signalChange = params.signalChange;
87
+ this.ctx = (_b = params.context) !== null && _b !== void 0 ? _b : {};
88
+ if (params.initial && this.states[params.initial]) {
89
+ this.initialState = params.initial;
90
+ this.enter(params.initial, '__init__');
91
+ }
92
+ }
93
+ /**
94
+ * 主动迁移(代码侧也能调)。
95
+ *
96
+ * 第二参向后兼容两种形式:
97
+ * - `goto('idle', 'manual')` 旧签名,等价于 `{ reason: 'manual' }`
98
+ * - `goto('idle', { reason: 'manual', force: true })` 新签名,`force:true` 时
99
+ * 即便 `current === to` 也走 exit→enter,用于"重入同 state 重新初始化定时器/订阅"
100
+ */
101
+ goto(to, opts = 'manual') {
102
+ var _a;
103
+ const { reason, force } = this.normalizeGotoOpts(opts);
104
+ if (!this.states[to]) {
105
+ // eslint-disable-next-line no-console
106
+ console.warn(`[plugin-state-machine] no such state: ${to}`);
107
+ return;
108
+ }
109
+ if (this.current === to && !force)
110
+ return;
111
+ const from = this.current;
112
+ if (from && ((_a = this.states[from]) === null || _a === void 0 ? void 0 : _a.onExit)) {
113
+ pluginSignalBus.getSignalBus().emit(this.states[from].onExit, { from, to, reason });
114
+ }
115
+ this.cleanupSubs();
116
+ this.enter(to, reason);
117
+ }
118
+ normalizeGotoOpts(opts) {
119
+ var _a;
120
+ if (typeof opts === 'string')
121
+ return { reason: opts, force: false };
122
+ return {
123
+ reason: (_a = opts.reason) !== null && _a !== void 0 ? _a : 'manual',
124
+ force: opts.force === true,
125
+ };
126
+ }
127
+ /**
128
+ * 显式 reset:强制回到 initial 并重发 onEnter,清掉所有挂在旧 state 上的订阅。
129
+ *
130
+ * 为什么独立于 goto:
131
+ * - 跨 scene 切换时,挂在 globalEntities 上的 FSM 实例不会被销毁;
132
+ * `current` 保留旧值,旧 state 的 signal subs 仍生效,业务想"重置到 initial
133
+ * 重发 onEnter"时用 `goto(initial)` 会被 short-circuit。
134
+ * - `reset()` 走的是 `current = '' → enter(initial)`,绕过 short-circuit,
135
+ * 并 emit `'fsm:reset'` 让消费方观测。
136
+ * - 如果原 state 配了 `onExit`,会先 emit 一次再清订阅。
137
+ *
138
+ * 框架不会自动调 reset —— 跨 scene 是否要 reset 是消费方语义。
139
+ * StateMachineSystem 只在 sceneChanged 时 emit `'fsm:scene-switch'` 提醒。
140
+ */
141
+ reset(payload) {
142
+ var _a, _b;
143
+ const initial = this.initialState;
144
+ if (!initial) {
145
+ // eslint-disable-next-line no-console
146
+ console.warn('[plugin-state-machine] reset() called but no valid initial state');
147
+ return;
148
+ }
149
+ const fromState = this.current;
150
+ // 1. 先发 onExit(如果当前 state 有)
151
+ if (fromState && ((_a = this.states[fromState]) === null || _a === void 0 ? void 0 : _a.onExit)) {
152
+ pluginSignalBus.getSignalBus().emit(this.states[fromState].onExit, {
153
+ from: fromState,
154
+ to: initial,
155
+ reason: 'reset',
156
+ });
157
+ }
158
+ // 2. 清订阅 + 把 current 拉回空串,让 enter() 不被 short-circuit
159
+ this.cleanupSubs();
160
+ this.current = '';
161
+ // 3. 重发 onEnter(走 enter() 标准路径)
162
+ this.enter(initial, 'reset');
163
+ // 4. emit 全局观测信号
164
+ const resetPayload = {
165
+ entityId: (_b = this.gameObject) === null || _b === void 0 ? void 0 : _b.name,
166
+ fsmName: StateMachine_1.componentName,
167
+ fromState,
168
+ payload,
169
+ };
170
+ pluginSignalBus.getSignalBus().emit(FSM_RESET_SIGNAL, resetPayload);
171
+ }
172
+ /**
173
+ * 重发当前状态的 onEnter 信号(不 exit、不改 current、不重新订阅 transitions)。
174
+ *
175
+ * 用于 scene 切换 / restartScene 之后:globalEntities 上的 StateMachine 实例不销毁、
176
+ * 不发生状态转换,所以不会重发 onEnter;但本 scene 重建出的新 controller(如依赖
177
+ * `meta:fsm:enter:PLAYING` 激活的 cannon)需要这个信号才能 active。restartScene
178
+ * 完成后由 GlobalEntitiesManager 对 global StateMachine 调 reenterCurrent(),让新
179
+ * controller 收到 enter 信号(ADR-0017 选项 D)。
180
+ *
181
+ * 与 reset() 区别:reset 回 initial 并清订阅;reenterCurrent 保持当前状态,只补发
182
+ * onEnter(+ signalChange),订阅不动。
183
+ * 与 goto(current, {force}) 区别:force 会先 emit onExit 再重新订阅;reenterCurrent
184
+ * 不 exit、不重订阅(订阅还活着),更轻,适合 scene 切换后补发。
185
+ *
186
+ * 当前没进入任何状态(init 前)时是 no-op。
187
+ */
188
+ reenterCurrent(payload) {
189
+ if (!this.current)
190
+ return;
191
+ const cfg = this.states[this.current];
192
+ if (!cfg)
193
+ return;
194
+ const from = this.current;
195
+ const to = this.current;
196
+ const bus = pluginSignalBus.getSignalBus();
197
+ if (cfg.onEnter) {
198
+ bus.emit(cfg.onEnter, Object.assign({ from, to, reason: 'reenter' }, (payload || {})));
199
+ }
200
+ if (this.signalChange) {
201
+ bus.emit(this.signalChange, { from, to, reason: 'reenter' });
202
+ }
203
+ }
204
+ get state() {
205
+ return this.current;
206
+ }
207
+ enter(to, reason) {
208
+ var _a;
209
+ const from = this.current;
210
+ this.current = to;
211
+ this.elapsedInState = 0;
212
+ const cfg = this.states[to];
213
+ if (!cfg)
214
+ return;
215
+ if (cfg.onEnter)
216
+ pluginSignalBus.getSignalBus().emit(cfg.onEnter, { from, to, reason });
217
+ if (this.signalChange)
218
+ pluginSignalBus.getSignalBus().emit(this.signalChange, { from, to, reason });
219
+ // 订阅本 state 的 on 信号
220
+ const bus = pluginSignalBus.getSignalBus();
221
+ for (const t of (_a = cfg.transitions) !== null && _a !== void 0 ? _a : []) {
222
+ if (!t.on)
223
+ continue;
224
+ const target = t.to;
225
+ const guard = t.guard;
226
+ const h = bus.on(t.on, () => {
227
+ if (this.current !== to)
228
+ return; // 已经离开
229
+ if (guard && !this.evalGuard(guard))
230
+ return;
231
+ this.goto(target, t.on);
232
+ });
233
+ this.subs.push(h);
234
+ }
235
+ }
236
+ cleanupSubs() {
237
+ for (const h of this.subs)
238
+ h.dispose();
239
+ this.subs = [];
240
+ }
241
+ evalGuard(guard) {
242
+ try {
243
+ // 简单 expression eval,只暴露 ctx
244
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
245
+ const fn = new Function('ctx', `return (${guard});`);
246
+ return Boolean(fn(this.ctx));
247
+ }
248
+ catch (err) {
249
+ // eslint-disable-next-line no-console
250
+ console.warn(`[plugin-state-machine] bad guard "${guard}":`, err);
251
+ return false;
252
+ }
253
+ }
254
+ update(e) {
255
+ this.elapsedInState += e.deltaTime;
256
+ const cfg = this.states[this.current];
257
+ if (!(cfg === null || cfg === void 0 ? void 0 : cfg.transitions))
258
+ return;
259
+ for (const t of cfg.transitions) {
260
+ if (t.after == null)
261
+ continue;
262
+ if (this.elapsedInState < t.after)
263
+ continue;
264
+ if (t.guard && !this.evalGuard(t.guard))
265
+ continue;
266
+ this.goto(t.to, `after:${t.after}`);
267
+ return;
268
+ }
269
+ }
270
+ onDestroy() {
271
+ this.cleanupSubs();
272
+ }
273
+ };
274
+ exports.StateMachine.componentName = 'StateMachine';
275
+ exports.StateMachine = StateMachine_1 = __decorate([
276
+ eva_js.decorators.componentObserver({})
277
+ ], exports.StateMachine);
278
+
279
+ /** scene 切换观测信号:框架不自动 reset 任何 FSM,只 emit 让消费方决定 */
280
+ const FSM_SCENE_SWITCH_SIGNAL = 'fsm:scene-switch';
281
+ class StateMachineSystem extends eva_js.System {
282
+ constructor() {
283
+ super(...arguments);
284
+ this.name = 'StateMachine';
285
+ this.emitSceneSwitch = true;
286
+ this.autoResetOnSceneSwitch = false;
287
+ this.sceneChangedHandler = null;
288
+ }
289
+ init(params) {
290
+ if ((params === null || params === void 0 ? void 0 : params.emitSceneSwitch) === false)
291
+ this.emitSceneSwitch = false;
292
+ if ((params === null || params === void 0 ? void 0 : params.autoResetOnSceneSwitch) === true)
293
+ this.autoResetOnSceneSwitch = true;
294
+ }
295
+ awake() {
296
+ if (!this.emitSceneSwitch && !this.autoResetOnSceneSwitch)
297
+ return;
298
+ if (!this.game)
299
+ return;
300
+ this.sceneChangedHandler = (raw) => {
301
+ // game.emit('sceneChanged', { scene, mode, params }) — 透传整包,
302
+ // 同时把 scene 抽到顶层方便消费方判断
303
+ const scene = raw && typeof raw === 'object' && 'scene' in raw
304
+ ? raw.scene
305
+ : undefined;
306
+ if (this.emitSceneSwitch) {
307
+ const payload = { scene, raw };
308
+ pluginSignalBus.getSignalBus().emit(FSM_SCENE_SWITCH_SIGNAL, payload);
309
+ }
310
+ if (this.autoResetOnSceneSwitch) {
311
+ this.resetAllStateMachines();
312
+ }
313
+ };
314
+ this.game.on('sceneChanged', this.sceneChangedHandler);
315
+ }
316
+ /**
317
+ * 遍历 game.gameObjects 找所有 StateMachine Component 调 reset()。
318
+ * 仅在 `autoResetOnSceneSwitch === true` 时被 awake 钩子调用。
319
+ *
320
+ * 注意:walk 失败时 swallow error 不抛(scene 切换 latency 路径不应被
321
+ * 单个 FSM 异常阻断,fsm.reset 内部已有 fail-safe console.warn)。
322
+ */
323
+ resetAllStateMachines() {
324
+ var _a, _b, _c, _d, _e;
325
+ if (!this.game)
326
+ return;
327
+ const gameObjects = (_a = this.game.gameObjects) !== null && _a !== void 0 ? _a : [];
328
+ for (const go of gameObjects) {
329
+ if (!go || typeof go !== 'object')
330
+ continue;
331
+ const comps = (_b = go.components) !== null && _b !== void 0 ? _b : [];
332
+ for (const comp of comps) {
333
+ if (!comp)
334
+ continue;
335
+ // duck-typing:同时识别 constructor.componentName 与 instance.name
336
+ const componentName = (_e = (_d = (_c = comp === null || comp === void 0 ? void 0 : comp.constructor) === null || _c === void 0 ? void 0 : _c.componentName) !== null && _d !== void 0 ? _d : comp === null || comp === void 0 ? void 0 : comp.name) !== null && _e !== void 0 ? _e : '';
337
+ if (componentName !== 'StateMachine')
338
+ continue;
339
+ if (typeof comp.reset !== 'function')
340
+ continue;
341
+ try {
342
+ comp.reset();
343
+ }
344
+ catch (err) {
345
+ // eslint-disable-next-line no-console
346
+ console.warn('[plugin-state-machine] auto-reset failed:', err);
347
+ }
348
+ }
349
+ }
350
+ }
351
+ onDestroy() {
352
+ if (this.sceneChangedHandler && this.game) {
353
+ this.game.off('sceneChanged', this.sceneChangedHandler);
354
+ }
355
+ this.sceneChangedHandler = null;
356
+ }
357
+ }
358
+ StateMachineSystem.systemName = 'StateMachine';
359
+
360
+ exports.FSM_RESET_SIGNAL = FSM_RESET_SIGNAL;
361
+ exports.FSM_SCENE_SWITCH_SIGNAL = FSM_SCENE_SWITCH_SIGNAL;
362
+ exports.StateMachineSystem = StateMachineSystem;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t,e=require("@eva/eva.js"),n=require("@eva/plugin-signal-bus");"function"==typeof SuppressedError&&SuppressedError;const s="fsm:reset";exports.StateMachine=t=class extends e.Component{constructor(){super(...arguments),this.states={},this.current="",this.elapsedInState=0,this.subs=[],this.ctx={},this.initialState=""}init(t){var e,n;t&&(this.states=null!==(e=t.states)&&void 0!==e?e:{},this.signalChange=t.signalChange,this.ctx=null!==(n=t.context)&&void 0!==n?n:{},t.initial&&this.states[t.initial]&&(this.initialState=t.initial,this.enter(t.initial,"__init__")))}goto(t,e="manual"){var s;const{reason:i,force:o}=this.normalizeGotoOpts(e);if(!this.states[t])return void console.warn(`[plugin-state-machine] no such state: ${t}`);if(this.current===t&&!o)return;const a=this.current;a&&(null===(s=this.states[a])||void 0===s?void 0:s.onExit)&&n.getSignalBus().emit(this.states[a].onExit,{from:a,to:t,reason:i}),this.cleanupSubs(),this.enter(t,i)}normalizeGotoOpts(t){var e;return"string"==typeof t?{reason:t,force:!1}:{reason:null!==(e=t.reason)&&void 0!==e?e:"manual",force:!0===t.force}}reset(e){var i,o;const a=this.initialState;if(!a)return void console.warn("[plugin-state-machine] reset() called but no valid initial state");const r=this.current;r&&(null===(i=this.states[r])||void 0===i?void 0:i.onExit)&&n.getSignalBus().emit(this.states[r].onExit,{from:r,to:a,reason:"reset"}),this.cleanupSubs(),this.current="",this.enter(a,"reset");const c={entityId:null===(o=this.gameObject)||void 0===o?void 0:o.name,fsmName:t.componentName,fromState:r,payload:e};n.getSignalBus().emit(s,c)}reenterCurrent(t){if(!this.current)return;const e=this.states[this.current];if(!e)return;const s=this.current,i=this.current,o=n.getSignalBus();e.onEnter&&o.emit(e.onEnter,Object.assign({from:s,to:i,reason:"reenter"},t||{})),this.signalChange&&o.emit(this.signalChange,{from:s,to:i,reason:"reenter"})}get state(){return this.current}enter(t,e){var s;const i=this.current;this.current=t,this.elapsedInState=0;const o=this.states[t];if(!o)return;o.onEnter&&n.getSignalBus().emit(o.onEnter,{from:i,to:t,reason:e}),this.signalChange&&n.getSignalBus().emit(this.signalChange,{from:i,to:t,reason:e});const a=n.getSignalBus();for(const e of null!==(s=o.transitions)&&void 0!==s?s:[]){if(!e.on)continue;const n=e.to,s=e.guard,i=a.on(e.on,()=>{this.current===t&&(s&&!this.evalGuard(s)||this.goto(n,e.on))});this.subs.push(i)}}cleanupSubs(){for(const t of this.subs)t.dispose();this.subs=[]}evalGuard(t){try{const e=new Function("ctx",`return (${t});`);return Boolean(e(this.ctx))}catch(e){return console.warn(`[plugin-state-machine] bad guard "${t}":`,e),!1}}update(t){this.elapsedInState+=t.deltaTime;const e=this.states[this.current];if(null==e?void 0:e.transitions)for(const t of e.transitions)if(null!=t.after&&!(this.elapsedInState<t.after)&&(!t.guard||this.evalGuard(t.guard)))return void this.goto(t.to,`after:${t.after}`)}onDestroy(){this.cleanupSubs()}},exports.StateMachine.componentName="StateMachine",exports.StateMachine=t=function(t,e,n,s){var i,o=arguments.length,a=o<3?e:null===s?s=Object.getOwnPropertyDescriptor(e,n):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(t,e,n,s);else for(var r=t.length-1;r>=0;r--)(i=t[r])&&(a=(o<3?i(a):o>3?i(e,n,a):i(e,n))||a);return o>3&&a&&Object.defineProperty(e,n,a),a}([e.decorators.componentObserver({})],exports.StateMachine);const i="fsm:scene-switch";class o extends e.System{constructor(){super(...arguments),this.name="StateMachine",this.emitSceneSwitch=!0,this.autoResetOnSceneSwitch=!1,this.sceneChangedHandler=null}init(t){!1===(null==t?void 0:t.emitSceneSwitch)&&(this.emitSceneSwitch=!1),!0===(null==t?void 0:t.autoResetOnSceneSwitch)&&(this.autoResetOnSceneSwitch=!0)}awake(){(this.emitSceneSwitch||this.autoResetOnSceneSwitch)&&this.game&&(this.sceneChangedHandler=t=>{const e=t&&"object"==typeof t&&"scene"in t?t.scene:void 0;if(this.emitSceneSwitch){const s={scene:e,raw:t};n.getSignalBus().emit(i,s)}this.autoResetOnSceneSwitch&&this.resetAllStateMachines()},this.game.on("sceneChanged",this.sceneChangedHandler))}resetAllStateMachines(){var t,e,n,s,i;if(!this.game)return;const o=null!==(t=this.game.gameObjects)&&void 0!==t?t:[];for(const t of o){if(!t||"object"!=typeof t)continue;const o=null!==(e=t.components)&&void 0!==e?e:[];for(const t of o){if(!t)continue;if("StateMachine"===(null!==(i=null!==(s=null===(n=null==t?void 0:t.constructor)||void 0===n?void 0:n.componentName)&&void 0!==s?s:null==t?void 0:t.name)&&void 0!==i?i:"")&&"function"==typeof t.reset)try{t.reset()}catch(t){console.warn("[plugin-state-machine] auto-reset failed:",t)}}}}onDestroy(){this.sceneChangedHandler&&this.game&&this.game.off("sceneChanged",this.sceneChangedHandler),this.sceneChangedHandler=null}}o.systemName="StateMachine",exports.FSM_RESET_SIGNAL=s,exports.FSM_SCENE_SWITCH_SIGNAL=i,exports.StateMachineSystem=o;
@@ -0,0 +1,228 @@
1
+ import { Component } from '@eva/eva.js';
2
+ import { System } from '@eva/eva.js';
3
+
4
+ /** 全局信号名:任意 reset() 都会 emit,供观测与跨实例联动 */
5
+ export declare const FSM_RESET_SIGNAL = "fsm:reset";
6
+
7
+ /** scene 切换观测信号:框架不自动 reset 任何 FSM,只 emit 让消费方决定 */
8
+ export declare const FSM_SCENE_SWITCH_SIGNAL = "fsm:scene-switch";
9
+
10
+ /**
11
+ * 全局信号 `'fsm:reset'` 的 payload。
12
+ * 任意 StateMachine.reset() 调用都会 emit 这条信号,便于消费方观测。
13
+ */
14
+ export declare interface FsmResetPayload {
15
+ /** gameObject.name(在 framework 层是实体身份的最小可得线索) */
16
+ entityId: string | undefined;
17
+ /** 组件名(目前固定 'StateMachine',为未来子类预留) */
18
+ fsmName: string;
19
+ /** reset 前所处的 state(可能是 '' / initial / 任意业务 state) */
20
+ fromState: string;
21
+ /** 调用方透传的 payload(reset() 入参) */
22
+ payload?: any;
23
+ }
24
+
25
+ /**
26
+ * 全局信号 `'fsm:scene-switch'` 的 payload。
27
+ * StateMachineSystem 在 game `sceneChanged` 时 emit,**框架不自动 reset**,
28
+ * 消费方根据自己语义决定是否手动 reset 挂在 globalEntities 上的 FSM。
29
+ */
30
+ export declare interface FsmSceneSwitchPayload {
31
+ /** 新 scene 标识(无法稳定取到时为 undefined) */
32
+ scene?: unknown;
33
+ /** game.emit('sceneChanged', payload) 的原始 payload 透传 */
34
+ raw?: unknown;
35
+ }
36
+
37
+ /** goto() 第二参可选 options;旧调用 goto(to, 'reason') 保留向后兼容 */
38
+ export declare interface GotoOptions {
39
+ /** 迁移原因,落到 onEnter / onExit / signalChange payload 的 reason 字段 */
40
+ reason?: string;
41
+ /**
42
+ * 即便 current === to 也强制走 exit→enter 一遍。
43
+ * 默认 false(no-op short-circuit)。常用于"重入同一 state 重新初始化定时器/订阅"。
44
+ */
45
+ force?: boolean;
46
+ }
47
+
48
+ export declare interface StateConfig {
49
+ /** 进入时 emit 的信号 */
50
+ onEnter?: string;
51
+ /** 退出时 emit 的信号 */
52
+ onExit?: string;
53
+ /** 该状态下的迁移规则 */
54
+ transitions?: TransitionRule[];
55
+ }
56
+
57
+ /**
58
+ * StateMachine 组件 — 简易有限状态机。
59
+ *
60
+ * DSL 用法:
61
+ * ```json
62
+ * {
63
+ * "type": "StateMachine",
64
+ * "props": {
65
+ * "initial": "moving",
66
+ * "states": {
67
+ * "moving": { "onEnter": "monster:state-moving",
68
+ * "transitions": [
69
+ * { "on": "monster:hit", "to": "knockback" },
70
+ * { "after": 3000, "to": "resting" }
71
+ * ] },
72
+ * "resting": { "transitions": [{ "after": 1000, "to": "moving" }] },
73
+ * "knockback":{ "transitions": [{ "after": 800, "to": "resting" }] }
74
+ * },
75
+ * "signalChange": "monster:state-change"
76
+ * }
77
+ * }
78
+ * ```
79
+ *
80
+ * 行为:
81
+ * - 进入状态:emit `onEnter` + `signalChange` { from, to }
82
+ * - `transitions[i].on`: 监听信号,信号触发即迁移
83
+ * - `transitions[i].after`: 进入状态 N ms 后自动迁移
84
+ * - `transitions[i].guard`: JS 表达式,以 `ctx` 为变量,假则跳过该规则
85
+ *
86
+ * 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
87
+ */
88
+ export declare class StateMachine extends Component<StateMachineParams> {
89
+ static componentName: string;
90
+ private states;
91
+ private current;
92
+ private signalChange?;
93
+ private elapsedInState;
94
+ private subs;
95
+ /** 上下文,guard 可读 */
96
+ ctx: Record<string, any>;
97
+ init(params?: StateMachineParams): void;
98
+ /**
99
+ * 主动迁移(代码侧也能调)。
100
+ *
101
+ * 第二参向后兼容两种形式:
102
+ * - `goto('idle', 'manual')` 旧签名,等价于 `{ reason: 'manual' }`
103
+ * - `goto('idle', { reason: 'manual', force: true })` 新签名,`force:true` 时
104
+ * 即便 `current === to` 也走 exit→enter,用于"重入同 state 重新初始化定时器/订阅"
105
+ */
106
+ goto(to: string, opts?: string | GotoOptions): void;
107
+ private normalizeGotoOpts;
108
+ /**
109
+ * 显式 reset:强制回到 initial 并重发 onEnter,清掉所有挂在旧 state 上的订阅。
110
+ *
111
+ * 为什么独立于 goto:
112
+ * - 跨 scene 切换时,挂在 globalEntities 上的 FSM 实例不会被销毁;
113
+ * `current` 保留旧值,旧 state 的 signal subs 仍生效,业务想"重置到 initial
114
+ * 重发 onEnter"时用 `goto(initial)` 会被 short-circuit。
115
+ * - `reset()` 走的是 `current = '' → enter(initial)`,绕过 short-circuit,
116
+ * 并 emit `'fsm:reset'` 让消费方观测。
117
+ * - 如果原 state 配了 `onExit`,会先 emit 一次再清订阅。
118
+ *
119
+ * 框架不会自动调 reset —— 跨 scene 是否要 reset 是消费方语义。
120
+ * StateMachineSystem 只在 sceneChanged 时 emit `'fsm:scene-switch'` 提醒。
121
+ */
122
+ reset(payload?: any): void;
123
+ /**
124
+ * 重发当前状态的 onEnter 信号(不 exit、不改 current、不重新订阅 transitions)。
125
+ *
126
+ * 用于 scene 切换 / restartScene 之后:globalEntities 上的 StateMachine 实例不销毁、
127
+ * 不发生状态转换,所以不会重发 onEnter;但本 scene 重建出的新 controller(如依赖
128
+ * `meta:fsm:enter:PLAYING` 激活的 cannon)需要这个信号才能 active。restartScene
129
+ * 完成后由 GlobalEntitiesManager 对 global StateMachine 调 reenterCurrent(),让新
130
+ * controller 收到 enter 信号(ADR-0017 选项 D)。
131
+ *
132
+ * 与 reset() 区别:reset 回 initial 并清订阅;reenterCurrent 保持当前状态,只补发
133
+ * onEnter(+ signalChange),订阅不动。
134
+ * 与 goto(current, {force}) 区别:force 会先 emit onExit 再重新订阅;reenterCurrent
135
+ * 不 exit、不重订阅(订阅还活着),更轻,适合 scene 切换后补发。
136
+ *
137
+ * 当前没进入任何状态(init 前)时是 no-op。
138
+ */
139
+ reenterCurrent(payload?: any): void;
140
+ /** reset() 用的 initial state 引用;init 时缓存一次,后续不变 */
141
+ private initialState;
142
+ get state(): string;
143
+ private enter;
144
+ private cleanupSubs;
145
+ private evalGuard;
146
+ update(e: {
147
+ deltaTime: number;
148
+ }): void;
149
+ onDestroy(): void;
150
+ }
151
+
152
+ export declare interface StateMachineParams {
153
+ initial: string;
154
+ states: Record<string, StateConfig>;
155
+ /** 状态切换 emit 信号 */
156
+ signalChange?: string;
157
+ /** 上下文变量,可在 guard 中读 */
158
+ context?: Record<string, any>;
159
+ }
160
+
161
+ export declare class StateMachineSystem extends System<StateMachineSystemParams> {
162
+ static systemName: string;
163
+ readonly name = "StateMachine";
164
+ private emitSceneSwitch;
165
+ private autoResetOnSceneSwitch;
166
+ private sceneChangedHandler;
167
+ init(params?: StateMachineSystemParams): void;
168
+ awake(): void;
169
+ /**
170
+ * 遍历 game.gameObjects 找所有 StateMachine Component 调 reset()。
171
+ * 仅在 `autoResetOnSceneSwitch === true` 时被 awake 钩子调用。
172
+ *
173
+ * 注意:walk 失败时 swallow error 不抛(scene 切换 latency 路径不应被
174
+ * 单个 FSM 异常阻断,fsm.reset 内部已有 fail-safe console.warn)。
175
+ */
176
+ private resetAllStateMachines;
177
+ onDestroy(): void;
178
+ }
179
+
180
+ /**
181
+ * StateMachineSystem — 仅用于注册 + scene 切换观测。
182
+ *
183
+ * StateMachine 自身在 Component.update 中驱动。本 System 不主动 tick FSM。
184
+ *
185
+ * 跨 scene 行为:
186
+ * - 挂在 globalEntities 上的 FSM 实例不会被销毁,`current` 保留旧值。
187
+ * - 业务可能希望"切场景后回到 initial 重发 onEnter",但**这是消费方语义**,
188
+ * 框架不能擅自 reset(否则关卡内 FSM 在 retry-scene 时全部清空,反而越权)。
189
+ * - 因此本 System 只在 game `sceneChanged` 时 emit `'fsm:scene-switch'` 信号,
190
+ * 消费方根据自己语义在监听器内调 `fsmComponent.reset()`。
191
+ *
192
+ * 默认行为可通过 `params.emitSceneSwitch = false` 关闭(纯静默注册)。
193
+ */
194
+ declare interface StateMachineSystemParams {
195
+ /**
196
+ * 是否在 game `sceneChanged` 时 emit `'fsm:scene-switch'` 信号。默认 true。
197
+ * 关闭后挂载该 System 与原版空 System 等价。
198
+ */
199
+ emitSceneSwitch?: boolean;
200
+ /**
201
+ * 是否在 game `sceneChanged` 时自动对所有 StateMachine 实例调 `reset()`(ADR-0024B)。
202
+ *
203
+ * **默认 false**(向后兼容,与 ADR-0011/0017 "framework 不自动 reset"契约一致)。
204
+ *
205
+ * 设为 true 时,System 在 sceneChanged 后会:
206
+ * 1. 先 emit `'fsm:scene-switch'` 观测信号(保持既有契约)
207
+ * 2. 再 walk 当前 game.gameObjects,找所有 StateMachine Component 调 `reset()`
208
+ *
209
+ * 注意:scene-scoped 实体上的 FSM 会随 scene 销毁,reset 主要影响 globalEntities
210
+ * 上的 FSM。这是 ctx.fsm.attach(BehaviorContext)在 detach 时的 fallback —
211
+ * BehaviorScript 控制更细粒度的 attach handle 时不需要开启 autoReset。
212
+ */
213
+ autoResetOnSceneSwitch?: boolean;
214
+ }
215
+
216
+ /** 单条迁移规则 */
217
+ export declare interface TransitionRule {
218
+ /** 触发该迁移的信号名 */
219
+ on?: string;
220
+ /** 进入该状态后等待 N ms 自动迁移(可与 on 二选一) */
221
+ after?: number;
222
+ /** 目标状态名 */
223
+ to: string;
224
+ /** 可选:JS 表达式字符串,以 ctx 为上下文 */
225
+ guard?: string;
226
+ }
227
+
228
+ export { }
@@ -0,0 +1,356 @@
1
+ import { Component, decorators, System } from '@eva/eva.js';
2
+ import { getSignalBus } from '@eva/plugin-signal-bus';
3
+
4
+ /******************************************************************************
5
+ Copyright (c) Microsoft Corporation.
6
+
7
+ Permission to use, copy, modify, and/or distribute this software for any
8
+ purpose with or without fee is hereby granted.
9
+
10
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
11
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
12
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
13
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
14
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
15
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16
+ PERFORMANCE OF THIS SOFTWARE.
17
+ ***************************************************************************** */
18
+
19
+ function __decorate(decorators, target, key, desc) {
20
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
21
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
22
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
23
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
24
+ }
25
+
26
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
27
+ var e = new Error(message);
28
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
29
+ };
30
+
31
+ var StateMachine_1;
32
+ /** 全局信号名:任意 reset() 都会 emit,供观测与跨实例联动 */
33
+ const FSM_RESET_SIGNAL = 'fsm:reset';
34
+ /**
35
+ * StateMachine 组件 — 简易有限状态机。
36
+ *
37
+ * DSL 用法:
38
+ * ```json
39
+ * {
40
+ * "type": "StateMachine",
41
+ * "props": {
42
+ * "initial": "moving",
43
+ * "states": {
44
+ * "moving": { "onEnter": "monster:state-moving",
45
+ * "transitions": [
46
+ * { "on": "monster:hit", "to": "knockback" },
47
+ * { "after": 3000, "to": "resting" }
48
+ * ] },
49
+ * "resting": { "transitions": [{ "after": 1000, "to": "moving" }] },
50
+ * "knockback":{ "transitions": [{ "after": 800, "to": "resting" }] }
51
+ * },
52
+ * "signalChange": "monster:state-change"
53
+ * }
54
+ * }
55
+ * ```
56
+ *
57
+ * 行为:
58
+ * - 进入状态:emit `onEnter` + `signalChange` { from, to }
59
+ * - `transitions[i].on`: 监听信号,信号触发即迁移
60
+ * - `transitions[i].after`: 进入状态 N ms 后自动迁移
61
+ * - `transitions[i].guard`: JS 表达式,以 `ctx` 为变量,假则跳过该规则
62
+ *
63
+ * 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
64
+ */
65
+ let StateMachine = StateMachine_1 = class StateMachine extends Component {
66
+ constructor() {
67
+ super(...arguments);
68
+ this.states = {};
69
+ this.current = '';
70
+ this.elapsedInState = 0;
71
+ this.subs = [];
72
+ /** 上下文,guard 可读 */
73
+ this.ctx = {};
74
+ /** reset() 用的 initial state 引用;init 时缓存一次,后续不变 */
75
+ this.initialState = '';
76
+ }
77
+ init(params) {
78
+ var _a, _b;
79
+ if (!params)
80
+ return;
81
+ this.states = (_a = params.states) !== null && _a !== void 0 ? _a : {};
82
+ this.signalChange = params.signalChange;
83
+ this.ctx = (_b = params.context) !== null && _b !== void 0 ? _b : {};
84
+ if (params.initial && this.states[params.initial]) {
85
+ this.initialState = params.initial;
86
+ this.enter(params.initial, '__init__');
87
+ }
88
+ }
89
+ /**
90
+ * 主动迁移(代码侧也能调)。
91
+ *
92
+ * 第二参向后兼容两种形式:
93
+ * - `goto('idle', 'manual')` 旧签名,等价于 `{ reason: 'manual' }`
94
+ * - `goto('idle', { reason: 'manual', force: true })` 新签名,`force:true` 时
95
+ * 即便 `current === to` 也走 exit→enter,用于"重入同 state 重新初始化定时器/订阅"
96
+ */
97
+ goto(to, opts = 'manual') {
98
+ var _a;
99
+ const { reason, force } = this.normalizeGotoOpts(opts);
100
+ if (!this.states[to]) {
101
+ // eslint-disable-next-line no-console
102
+ console.warn(`[plugin-state-machine] no such state: ${to}`);
103
+ return;
104
+ }
105
+ if (this.current === to && !force)
106
+ return;
107
+ const from = this.current;
108
+ if (from && ((_a = this.states[from]) === null || _a === void 0 ? void 0 : _a.onExit)) {
109
+ getSignalBus().emit(this.states[from].onExit, { from, to, reason });
110
+ }
111
+ this.cleanupSubs();
112
+ this.enter(to, reason);
113
+ }
114
+ normalizeGotoOpts(opts) {
115
+ var _a;
116
+ if (typeof opts === 'string')
117
+ return { reason: opts, force: false };
118
+ return {
119
+ reason: (_a = opts.reason) !== null && _a !== void 0 ? _a : 'manual',
120
+ force: opts.force === true,
121
+ };
122
+ }
123
+ /**
124
+ * 显式 reset:强制回到 initial 并重发 onEnter,清掉所有挂在旧 state 上的订阅。
125
+ *
126
+ * 为什么独立于 goto:
127
+ * - 跨 scene 切换时,挂在 globalEntities 上的 FSM 实例不会被销毁;
128
+ * `current` 保留旧值,旧 state 的 signal subs 仍生效,业务想"重置到 initial
129
+ * 重发 onEnter"时用 `goto(initial)` 会被 short-circuit。
130
+ * - `reset()` 走的是 `current = '' → enter(initial)`,绕过 short-circuit,
131
+ * 并 emit `'fsm:reset'` 让消费方观测。
132
+ * - 如果原 state 配了 `onExit`,会先 emit 一次再清订阅。
133
+ *
134
+ * 框架不会自动调 reset —— 跨 scene 是否要 reset 是消费方语义。
135
+ * StateMachineSystem 只在 sceneChanged 时 emit `'fsm:scene-switch'` 提醒。
136
+ */
137
+ reset(payload) {
138
+ var _a, _b;
139
+ const initial = this.initialState;
140
+ if (!initial) {
141
+ // eslint-disable-next-line no-console
142
+ console.warn('[plugin-state-machine] reset() called but no valid initial state');
143
+ return;
144
+ }
145
+ const fromState = this.current;
146
+ // 1. 先发 onExit(如果当前 state 有)
147
+ if (fromState && ((_a = this.states[fromState]) === null || _a === void 0 ? void 0 : _a.onExit)) {
148
+ getSignalBus().emit(this.states[fromState].onExit, {
149
+ from: fromState,
150
+ to: initial,
151
+ reason: 'reset',
152
+ });
153
+ }
154
+ // 2. 清订阅 + 把 current 拉回空串,让 enter() 不被 short-circuit
155
+ this.cleanupSubs();
156
+ this.current = '';
157
+ // 3. 重发 onEnter(走 enter() 标准路径)
158
+ this.enter(initial, 'reset');
159
+ // 4. emit 全局观测信号
160
+ const resetPayload = {
161
+ entityId: (_b = this.gameObject) === null || _b === void 0 ? void 0 : _b.name,
162
+ fsmName: StateMachine_1.componentName,
163
+ fromState,
164
+ payload,
165
+ };
166
+ getSignalBus().emit(FSM_RESET_SIGNAL, resetPayload);
167
+ }
168
+ /**
169
+ * 重发当前状态的 onEnter 信号(不 exit、不改 current、不重新订阅 transitions)。
170
+ *
171
+ * 用于 scene 切换 / restartScene 之后:globalEntities 上的 StateMachine 实例不销毁、
172
+ * 不发生状态转换,所以不会重发 onEnter;但本 scene 重建出的新 controller(如依赖
173
+ * `meta:fsm:enter:PLAYING` 激活的 cannon)需要这个信号才能 active。restartScene
174
+ * 完成后由 GlobalEntitiesManager 对 global StateMachine 调 reenterCurrent(),让新
175
+ * controller 收到 enter 信号(ADR-0017 选项 D)。
176
+ *
177
+ * 与 reset() 区别:reset 回 initial 并清订阅;reenterCurrent 保持当前状态,只补发
178
+ * onEnter(+ signalChange),订阅不动。
179
+ * 与 goto(current, {force}) 区别:force 会先 emit onExit 再重新订阅;reenterCurrent
180
+ * 不 exit、不重订阅(订阅还活着),更轻,适合 scene 切换后补发。
181
+ *
182
+ * 当前没进入任何状态(init 前)时是 no-op。
183
+ */
184
+ reenterCurrent(payload) {
185
+ if (!this.current)
186
+ return;
187
+ const cfg = this.states[this.current];
188
+ if (!cfg)
189
+ return;
190
+ const from = this.current;
191
+ const to = this.current;
192
+ const bus = getSignalBus();
193
+ if (cfg.onEnter) {
194
+ bus.emit(cfg.onEnter, Object.assign({ from, to, reason: 'reenter' }, (payload || {})));
195
+ }
196
+ if (this.signalChange) {
197
+ bus.emit(this.signalChange, { from, to, reason: 'reenter' });
198
+ }
199
+ }
200
+ get state() {
201
+ return this.current;
202
+ }
203
+ enter(to, reason) {
204
+ var _a;
205
+ const from = this.current;
206
+ this.current = to;
207
+ this.elapsedInState = 0;
208
+ const cfg = this.states[to];
209
+ if (!cfg)
210
+ return;
211
+ if (cfg.onEnter)
212
+ getSignalBus().emit(cfg.onEnter, { from, to, reason });
213
+ if (this.signalChange)
214
+ getSignalBus().emit(this.signalChange, { from, to, reason });
215
+ // 订阅本 state 的 on 信号
216
+ const bus = getSignalBus();
217
+ for (const t of (_a = cfg.transitions) !== null && _a !== void 0 ? _a : []) {
218
+ if (!t.on)
219
+ continue;
220
+ const target = t.to;
221
+ const guard = t.guard;
222
+ const h = bus.on(t.on, () => {
223
+ if (this.current !== to)
224
+ return; // 已经离开
225
+ if (guard && !this.evalGuard(guard))
226
+ return;
227
+ this.goto(target, t.on);
228
+ });
229
+ this.subs.push(h);
230
+ }
231
+ }
232
+ cleanupSubs() {
233
+ for (const h of this.subs)
234
+ h.dispose();
235
+ this.subs = [];
236
+ }
237
+ evalGuard(guard) {
238
+ try {
239
+ // 简单 expression eval,只暴露 ctx
240
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
241
+ const fn = new Function('ctx', `return (${guard});`);
242
+ return Boolean(fn(this.ctx));
243
+ }
244
+ catch (err) {
245
+ // eslint-disable-next-line no-console
246
+ console.warn(`[plugin-state-machine] bad guard "${guard}":`, err);
247
+ return false;
248
+ }
249
+ }
250
+ update(e) {
251
+ this.elapsedInState += e.deltaTime;
252
+ const cfg = this.states[this.current];
253
+ if (!(cfg === null || cfg === void 0 ? void 0 : cfg.transitions))
254
+ return;
255
+ for (const t of cfg.transitions) {
256
+ if (t.after == null)
257
+ continue;
258
+ if (this.elapsedInState < t.after)
259
+ continue;
260
+ if (t.guard && !this.evalGuard(t.guard))
261
+ continue;
262
+ this.goto(t.to, `after:${t.after}`);
263
+ return;
264
+ }
265
+ }
266
+ onDestroy() {
267
+ this.cleanupSubs();
268
+ }
269
+ };
270
+ StateMachine.componentName = 'StateMachine';
271
+ StateMachine = StateMachine_1 = __decorate([
272
+ decorators.componentObserver({})
273
+ ], StateMachine);
274
+
275
+ /** scene 切换观测信号:框架不自动 reset 任何 FSM,只 emit 让消费方决定 */
276
+ const FSM_SCENE_SWITCH_SIGNAL = 'fsm:scene-switch';
277
+ class StateMachineSystem extends System {
278
+ constructor() {
279
+ super(...arguments);
280
+ this.name = 'StateMachine';
281
+ this.emitSceneSwitch = true;
282
+ this.autoResetOnSceneSwitch = false;
283
+ this.sceneChangedHandler = null;
284
+ }
285
+ init(params) {
286
+ if ((params === null || params === void 0 ? void 0 : params.emitSceneSwitch) === false)
287
+ this.emitSceneSwitch = false;
288
+ if ((params === null || params === void 0 ? void 0 : params.autoResetOnSceneSwitch) === true)
289
+ this.autoResetOnSceneSwitch = true;
290
+ }
291
+ awake() {
292
+ if (!this.emitSceneSwitch && !this.autoResetOnSceneSwitch)
293
+ return;
294
+ if (!this.game)
295
+ return;
296
+ this.sceneChangedHandler = (raw) => {
297
+ // game.emit('sceneChanged', { scene, mode, params }) — 透传整包,
298
+ // 同时把 scene 抽到顶层方便消费方判断
299
+ const scene = raw && typeof raw === 'object' && 'scene' in raw
300
+ ? raw.scene
301
+ : undefined;
302
+ if (this.emitSceneSwitch) {
303
+ const payload = { scene, raw };
304
+ getSignalBus().emit(FSM_SCENE_SWITCH_SIGNAL, payload);
305
+ }
306
+ if (this.autoResetOnSceneSwitch) {
307
+ this.resetAllStateMachines();
308
+ }
309
+ };
310
+ this.game.on('sceneChanged', this.sceneChangedHandler);
311
+ }
312
+ /**
313
+ * 遍历 game.gameObjects 找所有 StateMachine Component 调 reset()。
314
+ * 仅在 `autoResetOnSceneSwitch === true` 时被 awake 钩子调用。
315
+ *
316
+ * 注意:walk 失败时 swallow error 不抛(scene 切换 latency 路径不应被
317
+ * 单个 FSM 异常阻断,fsm.reset 内部已有 fail-safe console.warn)。
318
+ */
319
+ resetAllStateMachines() {
320
+ var _a, _b, _c, _d, _e;
321
+ if (!this.game)
322
+ return;
323
+ const gameObjects = (_a = this.game.gameObjects) !== null && _a !== void 0 ? _a : [];
324
+ for (const go of gameObjects) {
325
+ if (!go || typeof go !== 'object')
326
+ continue;
327
+ const comps = (_b = go.components) !== null && _b !== void 0 ? _b : [];
328
+ for (const comp of comps) {
329
+ if (!comp)
330
+ continue;
331
+ // duck-typing:同时识别 constructor.componentName 与 instance.name
332
+ const componentName = (_e = (_d = (_c = comp === null || comp === void 0 ? void 0 : comp.constructor) === null || _c === void 0 ? void 0 : _c.componentName) !== null && _d !== void 0 ? _d : comp === null || comp === void 0 ? void 0 : comp.name) !== null && _e !== void 0 ? _e : '';
333
+ if (componentName !== 'StateMachine')
334
+ continue;
335
+ if (typeof comp.reset !== 'function')
336
+ continue;
337
+ try {
338
+ comp.reset();
339
+ }
340
+ catch (err) {
341
+ // eslint-disable-next-line no-console
342
+ console.warn('[plugin-state-machine] auto-reset failed:', err);
343
+ }
344
+ }
345
+ }
346
+ }
347
+ onDestroy() {
348
+ if (this.sceneChangedHandler && this.game) {
349
+ this.game.off('sceneChanged', this.sceneChangedHandler);
350
+ }
351
+ this.sceneChangedHandler = null;
352
+ }
353
+ }
354
+ StateMachineSystem.systemName = 'StateMachine';
355
+
356
+ export { FSM_RESET_SIGNAL, FSM_SCENE_SWITCH_SIGNAL, StateMachine, StateMachineSystem };
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./dist/plugin-state-machine.cjs.prod.js');
5
+ } else {
6
+ module.exports = require('./dist/plugin-state-machine.cjs.js');
7
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@eva/plugin-state-machine",
3
- "version": "2.1.0-beta.1",
3
+ "version": "2.1.0-beta.10",
4
4
  "description": "Finite state machine — DSL 声明 states/transitions/timeouts/guards/actions,emit state-change 信号。",
5
- "main": "lib/index.ts",
6
- "module": "lib/index.ts",
7
- "types": "lib/index.ts",
5
+ "main": "index.js",
6
+ "module": "dist/plugin-state-machine.esm.js",
7
+ "types": "dist/plugin-state-machine.d.ts",
8
8
  "files": [
9
- "lib"
9
+ "index.js",
10
+ "dist"
10
11
  ],
11
12
  "keywords": [
12
13
  "eva.js",
@@ -16,7 +17,7 @@
16
17
  ],
17
18
  "license": "MIT",
18
19
  "dependencies": {
19
- "@eva/eva.js": "2.1.0-beta.1",
20
- "@eva/plugin-signal-bus": "2.1.0-beta.1"
20
+ "@eva/eva.js": "2.1.0-beta.10",
21
+ "@eva/plugin-signal-bus": "2.1.0-beta.10"
21
22
  }
22
23
  }
@@ -1,136 +0,0 @@
1
- import { Component, decorators } from '@eva/eva.js';
2
- import { getSignalBus, SignalHandle } from '@eva/plugin-signal-bus';
3
- import type { StateMachineParams, StateConfig } from './types';
4
-
5
- /**
6
- * StateMachine 组件 — 简易有限状态机。
7
- *
8
- * DSL 用法:
9
- * ```json
10
- * {
11
- * "type": "StateMachine",
12
- * "props": {
13
- * "initial": "moving",
14
- * "states": {
15
- * "moving": { "onEnter": "monster:state-moving",
16
- * "transitions": [
17
- * { "on": "monster:hit", "to": "knockback" },
18
- * { "after": 3000, "to": "resting" }
19
- * ] },
20
- * "resting": { "transitions": [{ "after": 1000, "to": "moving" }] },
21
- * "knockback":{ "transitions": [{ "after": 800, "to": "resting" }] }
22
- * },
23
- * "signalChange": "monster:state-change"
24
- * }
25
- * }
26
- * ```
27
- *
28
- * 行为:
29
- * - 进入状态:emit `onEnter` + `signalChange` { from, to }
30
- * - `transitions[i].on`: 监听信号,信号触发即迁移
31
- * - `transitions[i].after`: 进入状态 N ms 后自动迁移
32
- * - `transitions[i].guard`: JS 表达式,以 `ctx` 为变量,假则跳过该规则
33
- *
34
- * 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
35
- */
36
- @decorators.componentObserver({})
37
- export class StateMachine extends Component<StateMachineParams> {
38
- static componentName = 'StateMachine';
39
-
40
- private states: Record<string, StateConfig> = {};
41
- private current: string = '';
42
- private signalChange?: string;
43
- private elapsedInState = 0;
44
- private subs: SignalHandle[] = [];
45
-
46
- /** 上下文,guard 可读 */
47
- ctx: Record<string, any> = {};
48
-
49
- init(params?: StateMachineParams) {
50
- if (!params) return;
51
- this.states = params.states ?? {};
52
- this.signalChange = params.signalChange;
53
- this.ctx = params.context ?? {};
54
- if (params.initial && this.states[params.initial]) {
55
- this.enter(params.initial, '__init__');
56
- }
57
- }
58
-
59
- /** 主动迁移(代码侧也能调) */
60
- goto(to: string, reason: string = 'manual') {
61
- if (!this.states[to]) {
62
- // eslint-disable-next-line no-console
63
- console.warn(`[plugin-state-machine] no such state: ${to}`);
64
- return;
65
- }
66
- if (this.current === to) return;
67
- const from = this.current;
68
- if (from && this.states[from]?.onExit) {
69
- getSignalBus().emit(this.states[from].onExit!, { from, to, reason });
70
- }
71
- this.cleanupSubs();
72
- this.enter(to, reason);
73
- }
74
-
75
- get state(): string {
76
- return this.current;
77
- }
78
-
79
- private enter(to: string, reason: string) {
80
- const from = this.current;
81
- this.current = to;
82
- this.elapsedInState = 0;
83
- const cfg = this.states[to];
84
- if (!cfg) return;
85
- if (cfg.onEnter) getSignalBus().emit(cfg.onEnter, { from, to, reason });
86
- if (this.signalChange) getSignalBus().emit(this.signalChange, { from, to, reason });
87
- // 订阅本 state 的 on 信号
88
- const bus = getSignalBus();
89
- for (const t of cfg.transitions ?? []) {
90
- if (!t.on) continue;
91
- const target = t.to;
92
- const guard = t.guard;
93
- const h = bus.on(t.on, () => {
94
- if (this.current !== to) return; // 已经离开
95
- if (guard && !this.evalGuard(guard)) return;
96
- this.goto(target, t.on!);
97
- });
98
- this.subs.push(h);
99
- }
100
- }
101
-
102
- private cleanupSubs() {
103
- for (const h of this.subs) h.dispose();
104
- this.subs = [];
105
- }
106
-
107
- private evalGuard(guard: string): boolean {
108
- try {
109
- // 简单 expression eval,只暴露 ctx
110
- // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
111
- const fn = new Function('ctx', `return (${guard});`);
112
- return Boolean(fn(this.ctx));
113
- } catch (err) {
114
- // eslint-disable-next-line no-console
115
- console.warn(`[plugin-state-machine] bad guard "${guard}":`, err);
116
- return false;
117
- }
118
- }
119
-
120
- update(e: { deltaTime: number }) {
121
- this.elapsedInState += e.deltaTime;
122
- const cfg = this.states[this.current];
123
- if (!cfg?.transitions) return;
124
- for (const t of cfg.transitions) {
125
- if (t.after == null) continue;
126
- if (this.elapsedInState < t.after) continue;
127
- if (t.guard && !this.evalGuard(t.guard)) continue;
128
- this.goto(t.to, `after:${t.after}`);
129
- return;
130
- }
131
- }
132
-
133
- onDestroy() {
134
- this.cleanupSubs();
135
- }
136
- }
@@ -1,12 +0,0 @@
1
- import { System } from '@eva/eva.js';
2
-
3
- /**
4
- * StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
5
- *
6
- * 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
7
- * 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
8
- */
9
- export class StateMachineSystem extends System {
10
- static systemName = 'StateMachine';
11
- readonly name = 'StateMachine';
12
- }
package/lib/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export { StateMachine } from './StateMachine';
2
- export { StateMachineSystem } from './StateMachineSystem';
3
- export type { StateMachineParams, StateConfig, TransitionRule } from './types';
package/lib/types.ts DELETED
@@ -1,29 +0,0 @@
1
- /** 单条迁移规则 */
2
- export interface TransitionRule {
3
- /** 触发该迁移的信号名 */
4
- on?: string;
5
- /** 进入该状态后等待 N ms 自动迁移(可与 on 二选一) */
6
- after?: number;
7
- /** 目标状态名 */
8
- to: string;
9
- /** 可选:JS 表达式字符串,以 ctx 为上下文 */
10
- guard?: string;
11
- }
12
-
13
- export interface StateConfig {
14
- /** 进入时 emit 的信号 */
15
- onEnter?: string;
16
- /** 退出时 emit 的信号 */
17
- onExit?: string;
18
- /** 该状态下的迁移规则 */
19
- transitions?: TransitionRule[];
20
- }
21
-
22
- export interface StateMachineParams {
23
- initial: string;
24
- states: Record<string, StateConfig>;
25
- /** 状态切换 emit 信号 */
26
- signalChange?: string;
27
- /** 上下文变量,可在 guard 中读 */
28
- context?: Record<string, any>;
29
- }