@eva/plugin-state-machine 2.1.0-beta.4 → 2.1.0-beta.6

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.
@@ -32,6 +32,9 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
32
32
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
33
33
  };
34
34
 
35
+ var StateMachine_1;
36
+ /** 全局信号名:任意 reset() 都会 emit,供观测与跨实例联动 */
37
+ const FSM_RESET_SIGNAL = 'fsm:reset';
35
38
  /**
36
39
  * StateMachine 组件 — 简易有限状态机。
37
40
  *
@@ -63,7 +66,7 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
63
66
  *
64
67
  * 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
65
68
  */
66
- exports.StateMachine = class StateMachine extends eva_js.Component {
69
+ exports.StateMachine = StateMachine_1 = class StateMachine extends eva_js.Component {
67
70
  constructor() {
68
71
  super(...arguments);
69
72
  this.states = {};
@@ -72,6 +75,8 @@ exports.StateMachine = class StateMachine extends eva_js.Component {
72
75
  this.subs = [];
73
76
  /** 上下文,guard 可读 */
74
77
  this.ctx = {};
78
+ /** reset() 用的 initial state 引用;init 时缓存一次,后续不变 */
79
+ this.initialState = '';
75
80
  }
76
81
  init(params) {
77
82
  var _a, _b;
@@ -81,18 +86,27 @@ exports.StateMachine = class StateMachine extends eva_js.Component {
81
86
  this.signalChange = params.signalChange;
82
87
  this.ctx = (_b = params.context) !== null && _b !== void 0 ? _b : {};
83
88
  if (params.initial && this.states[params.initial]) {
89
+ this.initialState = params.initial;
84
90
  this.enter(params.initial, '__init__');
85
91
  }
86
92
  }
87
- /** 主动迁移(代码侧也能调) */
88
- goto(to, reason = 'manual') {
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') {
89
102
  var _a;
103
+ const { reason, force } = this.normalizeGotoOpts(opts);
90
104
  if (!this.states[to]) {
91
105
  // eslint-disable-next-line no-console
92
106
  console.warn(`[plugin-state-machine] no such state: ${to}`);
93
107
  return;
94
108
  }
95
- if (this.current === to)
109
+ if (this.current === to && !force)
96
110
  return;
97
111
  const from = this.current;
98
112
  if (from && ((_a = this.states[from]) === null || _a === void 0 ? void 0 : _a.onExit)) {
@@ -101,6 +115,92 @@ exports.StateMachine = class StateMachine extends eva_js.Component {
101
115
  this.cleanupSubs();
102
116
  this.enter(to, reason);
103
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
+ }
104
204
  get state() {
105
205
  return this.current;
106
206
  }
@@ -172,22 +272,91 @@ exports.StateMachine = class StateMachine extends eva_js.Component {
172
272
  }
173
273
  };
174
274
  exports.StateMachine.componentName = 'StateMachine';
175
- exports.StateMachine = __decorate([
275
+ exports.StateMachine = StateMachine_1 = __decorate([
176
276
  eva_js.decorators.componentObserver({})
177
277
  ], exports.StateMachine);
178
278
 
179
- /**
180
- * StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
181
- *
182
- * 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
183
- * 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
184
- */
279
+ /** scene 切换观测信号:框架不自动 reset 任何 FSM,只 emit 让消费方决定 */
280
+ const FSM_SCENE_SWITCH_SIGNAL = 'fsm:scene-switch';
185
281
  class StateMachineSystem extends eva_js.System {
186
282
  constructor() {
187
283
  super(...arguments);
188
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;
189
356
  }
190
357
  }
191
358
  StateMachineSystem.systemName = 'StateMachine';
192
359
 
360
+ exports.FSM_RESET_SIGNAL = FSM_RESET_SIGNAL;
361
+ exports.FSM_SCENE_SWITCH_SIGNAL = FSM_SCENE_SWITCH_SIGNAL;
193
362
  exports.StateMachineSystem = StateMachineSystem;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t=require("@eva/eva.js"),e=require("@eva/plugin-signal-bus");"function"==typeof SuppressedError&&SuppressedError,exports.StateMachine=class extends t.Component{constructor(){super(...arguments),this.states={},this.current="",this.elapsedInState=0,this.subs=[],this.ctx={}}init(t){var e,s;t&&(this.states=null!==(e=t.states)&&void 0!==e?e:{},this.signalChange=t.signalChange,this.ctx=null!==(s=t.context)&&void 0!==s?s:{},t.initial&&this.states[t.initial]&&this.enter(t.initial,"__init__"))}goto(t,s="manual"){var n;if(!this.states[t])return void console.warn(`[plugin-state-machine] no such state: ${t}`);if(this.current===t)return;const i=this.current;i&&(null===(n=this.states[i])||void 0===n?void 0:n.onExit)&&e.getSignalBus().emit(this.states[i].onExit,{from:i,to:t,reason:s}),this.cleanupSubs(),this.enter(t,s)}get state(){return this.current}enter(t,s){var n;const i=this.current;this.current=t,this.elapsedInState=0;const r=this.states[t];if(!r)return;r.onEnter&&e.getSignalBus().emit(r.onEnter,{from:i,to:t,reason:s}),this.signalChange&&e.getSignalBus().emit(this.signalChange,{from:i,to:t,reason:s});const a=e.getSignalBus();for(const e of null!==(n=r.transitions)&&void 0!==n?n:[]){if(!e.on)continue;const s=e.to,n=e.guard,i=a.on(e.on,()=>{this.current===t&&(n&&!this.evalGuard(n)||this.goto(s,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=function(t,e,s,n){var i,r=arguments.length,a=r<3?e:null===n?n=Object.getOwnPropertyDescriptor(e,s):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(t,e,s,n);else for(var o=t.length-1;o>=0;o--)(i=t[o])&&(a=(r<3?i(a):r>3?i(e,s,a):i(e,s))||a);return r>3&&a&&Object.defineProperty(e,s,a),a}([t.decorators.componentObserver({})],exports.StateMachine);class s extends t.System{constructor(){super(...arguments),this.name="StateMachine"}}s.systemName="StateMachine",exports.StateMachineSystem=s;
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;
@@ -1,6 +1,50 @@
1
1
  import { Component } from '@eva/eva.js';
2
2
  import { System } from '@eva/eva.js';
3
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
+
4
48
  export declare interface StateConfig {
5
49
  /** 进入时 emit 的信号 */
6
50
  onEnter?: string;
@@ -51,8 +95,50 @@ export declare class StateMachine extends Component<StateMachineParams> {
51
95
  /** 上下文,guard 可读 */
52
96
  ctx: Record<string, any>;
53
97
  init(params?: StateMachineParams): void;
54
- /** 主动迁移(代码侧也能调) */
55
- goto(to: string, reason?: string): 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;
56
142
  get state(): string;
57
143
  private enter;
58
144
  private cleanupSubs;
@@ -72,15 +158,59 @@ export declare interface StateMachineParams {
72
158
  context?: Record<string, any>;
73
159
  }
74
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
+
75
180
  /**
76
- * StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
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()`。
77
191
  *
78
- * 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
79
- * 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
192
+ * 默认行为可通过 `params.emitSceneSwitch = false` 关闭(纯静默注册)。
80
193
  */
81
- export declare class StateMachineSystem extends System {
82
- static systemName: string;
83
- readonly name = "StateMachine";
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;
84
214
  }
85
215
 
86
216
  /** 单条迁移规则 */
@@ -28,6 +28,9 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
28
28
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
29
29
  };
30
30
 
31
+ var StateMachine_1;
32
+ /** 全局信号名:任意 reset() 都会 emit,供观测与跨实例联动 */
33
+ const FSM_RESET_SIGNAL = 'fsm:reset';
31
34
  /**
32
35
  * StateMachine 组件 — 简易有限状态机。
33
36
  *
@@ -59,7 +62,7 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
59
62
  *
60
63
  * 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
61
64
  */
62
- let StateMachine = class StateMachine extends Component {
65
+ let StateMachine = StateMachine_1 = class StateMachine extends Component {
63
66
  constructor() {
64
67
  super(...arguments);
65
68
  this.states = {};
@@ -68,6 +71,8 @@ let StateMachine = class StateMachine extends Component {
68
71
  this.subs = [];
69
72
  /** 上下文,guard 可读 */
70
73
  this.ctx = {};
74
+ /** reset() 用的 initial state 引用;init 时缓存一次,后续不变 */
75
+ this.initialState = '';
71
76
  }
72
77
  init(params) {
73
78
  var _a, _b;
@@ -77,18 +82,27 @@ let StateMachine = class StateMachine extends Component {
77
82
  this.signalChange = params.signalChange;
78
83
  this.ctx = (_b = params.context) !== null && _b !== void 0 ? _b : {};
79
84
  if (params.initial && this.states[params.initial]) {
85
+ this.initialState = params.initial;
80
86
  this.enter(params.initial, '__init__');
81
87
  }
82
88
  }
83
- /** 主动迁移(代码侧也能调) */
84
- goto(to, reason = 'manual') {
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') {
85
98
  var _a;
99
+ const { reason, force } = this.normalizeGotoOpts(opts);
86
100
  if (!this.states[to]) {
87
101
  // eslint-disable-next-line no-console
88
102
  console.warn(`[plugin-state-machine] no such state: ${to}`);
89
103
  return;
90
104
  }
91
- if (this.current === to)
105
+ if (this.current === to && !force)
92
106
  return;
93
107
  const from = this.current;
94
108
  if (from && ((_a = this.states[from]) === null || _a === void 0 ? void 0 : _a.onExit)) {
@@ -97,6 +111,92 @@ let StateMachine = class StateMachine extends Component {
97
111
  this.cleanupSubs();
98
112
  this.enter(to, reason);
99
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
+ }
100
200
  get state() {
101
201
  return this.current;
102
202
  }
@@ -168,22 +268,89 @@ let StateMachine = class StateMachine extends Component {
168
268
  }
169
269
  };
170
270
  StateMachine.componentName = 'StateMachine';
171
- StateMachine = __decorate([
271
+ StateMachine = StateMachine_1 = __decorate([
172
272
  decorators.componentObserver({})
173
273
  ], StateMachine);
174
274
 
175
- /**
176
- * StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
177
- *
178
- * 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
179
- * 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
180
- */
275
+ /** scene 切换观测信号:框架不自动 reset 任何 FSM,只 emit 让消费方决定 */
276
+ const FSM_SCENE_SWITCH_SIGNAL = 'fsm:scene-switch';
181
277
  class StateMachineSystem extends System {
182
278
  constructor() {
183
279
  super(...arguments);
184
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;
185
352
  }
186
353
  }
187
354
  StateMachineSystem.systemName = 'StateMachine';
188
355
 
189
- export { StateMachine, StateMachineSystem };
356
+ export { FSM_RESET_SIGNAL, FSM_SCENE_SWITCH_SIGNAL, StateMachine, StateMachineSystem };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eva/plugin-state-machine",
3
- "version": "2.1.0-beta.4",
3
+ "version": "2.1.0-beta.6",
4
4
  "description": "Finite state machine — DSL 声明 states/transitions/timeouts/guards/actions,emit state-change 信号。",
5
5
  "main": "index.js",
6
6
  "module": "dist/plugin-state-machine.esm.js",
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
- "@eva/eva.js": "2.1.0-beta.4",
21
- "@eva/plugin-signal-bus": "2.1.0-beta.4"
20
+ "@eva/eva.js": "2.1.0-beta.6",
21
+ "@eva/plugin-signal-bus": "2.1.0-beta.6"
22
22
  }
23
23
  }