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

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,60 @@ 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
+ }
104
172
  get state() {
105
173
  return this.current;
106
174
  }
@@ -172,22 +240,48 @@ exports.StateMachine = class StateMachine extends eva_js.Component {
172
240
  }
173
241
  };
174
242
  exports.StateMachine.componentName = 'StateMachine';
175
- exports.StateMachine = __decorate([
243
+ exports.StateMachine = StateMachine_1 = __decorate([
176
244
  eva_js.decorators.componentObserver({})
177
245
  ], exports.StateMachine);
178
246
 
179
- /**
180
- * StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
181
- *
182
- * 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
183
- * 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
184
- */
247
+ /** scene 切换观测信号:框架不自动 reset 任何 FSM,只 emit 让消费方决定 */
248
+ const FSM_SCENE_SWITCH_SIGNAL = 'fsm:scene-switch';
185
249
  class StateMachineSystem extends eva_js.System {
186
250
  constructor() {
187
251
  super(...arguments);
188
252
  this.name = 'StateMachine';
253
+ this.emitSceneSwitch = true;
254
+ this.sceneChangedHandler = null;
255
+ }
256
+ init(params) {
257
+ if ((params === null || params === void 0 ? void 0 : params.emitSceneSwitch) === false)
258
+ this.emitSceneSwitch = false;
259
+ }
260
+ awake() {
261
+ if (!this.emitSceneSwitch)
262
+ return;
263
+ if (!this.game)
264
+ return;
265
+ this.sceneChangedHandler = (raw) => {
266
+ // game.emit('sceneChanged', { scene, mode, params }) — 透传整包,
267
+ // 同时把 scene 抽到顶层方便消费方判断
268
+ const scene = raw && typeof raw === 'object' && 'scene' in raw
269
+ ? raw.scene
270
+ : undefined;
271
+ const payload = { scene, raw };
272
+ pluginSignalBus.getSignalBus().emit(FSM_SCENE_SWITCH_SIGNAL, payload);
273
+ };
274
+ this.game.on('sceneChanged', this.sceneChangedHandler);
275
+ }
276
+ onDestroy() {
277
+ if (this.sceneChangedHandler && this.game) {
278
+ this.game.off('sceneChanged', this.sceneChangedHandler);
279
+ }
280
+ this.sceneChangedHandler = null;
189
281
  }
190
282
  }
191
283
  StateMachineSystem.systemName = 'StateMachine';
192
284
 
285
+ exports.FSM_RESET_SIGNAL = FSM_RESET_SIGNAL;
286
+ exports.FSM_SCENE_SWITCH_SIGNAL = FSM_SCENE_SWITCH_SIGNAL;
193
287
  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"),s=require("@eva/plugin-signal-bus");"function"==typeof SuppressedError&&SuppressedError;const n="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,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.initialState=t.initial,this.enter(t.initial,"__init__")))}goto(t,e="manual"){var n;const{reason:i,force:a}=this.normalizeGotoOpts(e);if(!this.states[t])return void console.warn(`[plugin-state-machine] no such state: ${t}`);if(this.current===t&&!a)return;const r=this.current;r&&(null===(n=this.states[r])||void 0===n?void 0:n.onExit)&&s.getSignalBus().emit(this.states[r].onExit,{from:r,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,a;const r=this.initialState;if(!r)return void console.warn("[plugin-state-machine] reset() called but no valid initial state");const o=this.current;o&&(null===(i=this.states[o])||void 0===i?void 0:i.onExit)&&s.getSignalBus().emit(this.states[o].onExit,{from:o,to:r,reason:"reset"}),this.cleanupSubs(),this.current="",this.enter(r,"reset");const c={entityId:null===(a=this.gameObject)||void 0===a?void 0:a.name,fsmName:t.componentName,fromState:o,payload:e};s.getSignalBus().emit(n,c)}get state(){return this.current}enter(t,e){var n;const i=this.current;this.current=t,this.elapsedInState=0;const a=this.states[t];if(!a)return;a.onEnter&&s.getSignalBus().emit(a.onEnter,{from:i,to:t,reason:e}),this.signalChange&&s.getSignalBus().emit(this.signalChange,{from:i,to:t,reason:e});const r=s.getSignalBus();for(const e of null!==(n=a.transitions)&&void 0!==n?n:[]){if(!e.on)continue;const s=e.to,n=e.guard,i=r.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=t=function(t,e,s,n){var i,a=arguments.length,r=a<3?e:null===n?n=Object.getOwnPropertyDescriptor(e,s):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,s,n);else for(var o=t.length-1;o>=0;o--)(i=t[o])&&(r=(a<3?i(r):a>3?i(e,s,r):i(e,s))||r);return a>3&&r&&Object.defineProperty(e,s,r),r}([e.decorators.componentObserver({})],exports.StateMachine);const i="fsm:scene-switch";class a extends e.System{constructor(){super(...arguments),this.name="StateMachine",this.emitSceneSwitch=!0,this.sceneChangedHandler=null}init(t){!1===(null==t?void 0:t.emitSceneSwitch)&&(this.emitSceneSwitch=!1)}awake(){this.emitSceneSwitch&&this.game&&(this.sceneChangedHandler=t=>{const e={scene:t&&"object"==typeof t&&"scene"in t?t.scene:void 0,raw:t};s.getSignalBus().emit(i,e)},this.game.on("sceneChanged",this.sceneChangedHandler))}onDestroy(){this.sceneChangedHandler&&this.game&&this.game.off("sceneChanged",this.sceneChangedHandler),this.sceneChangedHandler=null}}a.systemName="StateMachine",exports.FSM_RESET_SIGNAL=n,exports.FSM_SCENE_SWITCH_SIGNAL=i,exports.StateMachineSystem=a;
@@ -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,33 @@ 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
+ /** reset() 用的 initial state 引用;init 时缓存一次,后续不变 */
124
+ private initialState;
56
125
  get state(): string;
57
126
  private enter;
58
127
  private cleanupSubs;
@@ -72,15 +141,36 @@ export declare interface StateMachineParams {
72
141
  context?: Record<string, any>;
73
142
  }
74
143
 
144
+ export declare class StateMachineSystem extends System<StateMachineSystemParams> {
145
+ static systemName: string;
146
+ readonly name = "StateMachine";
147
+ private emitSceneSwitch;
148
+ private sceneChangedHandler;
149
+ init(params?: StateMachineSystemParams): void;
150
+ awake(): void;
151
+ onDestroy(): void;
152
+ }
153
+
75
154
  /**
76
- * StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
155
+ * StateMachineSystem — 仅用于注册 + scene 切换观测。
156
+ *
157
+ * StateMachine 自身在 Component.update 中驱动。本 System 不主动 tick FSM。
158
+ *
159
+ * 跨 scene 行为:
160
+ * - 挂在 globalEntities 上的 FSM 实例不会被销毁,`current` 保留旧值。
161
+ * - 业务可能希望"切场景后回到 initial 重发 onEnter",但**这是消费方语义**,
162
+ * 框架不能擅自 reset(否则关卡内 FSM 在 retry-scene 时全部清空,反而越权)。
163
+ * - 因此本 System 只在 game `sceneChanged` 时 emit `'fsm:scene-switch'` 信号,
164
+ * 消费方根据自己语义在监听器内调 `fsmComponent.reset()`。
77
165
  *
78
- * 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
79
- * 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
166
+ * 默认行为可通过 `params.emitSceneSwitch = false` 关闭(纯静默注册)。
80
167
  */
81
- export declare class StateMachineSystem extends System {
82
- static systemName: string;
83
- readonly name = "StateMachine";
168
+ declare interface StateMachineSystemParams {
169
+ /**
170
+ * 是否在 game `sceneChanged` 时 emit `'fsm:scene-switch'` 信号。默认 true。
171
+ * 关闭后挂载该 System 与原版空 System 等价。
172
+ */
173
+ emitSceneSwitch?: boolean;
84
174
  }
85
175
 
86
176
  /** 单条迁移规则 */
@@ -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,60 @@ 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
+ }
100
168
  get state() {
101
169
  return this.current;
102
170
  }
@@ -168,22 +236,46 @@ let StateMachine = class StateMachine extends Component {
168
236
  }
169
237
  };
170
238
  StateMachine.componentName = 'StateMachine';
171
- StateMachine = __decorate([
239
+ StateMachine = StateMachine_1 = __decorate([
172
240
  decorators.componentObserver({})
173
241
  ], StateMachine);
174
242
 
175
- /**
176
- * StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
177
- *
178
- * 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
179
- * 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
180
- */
243
+ /** scene 切换观测信号:框架不自动 reset 任何 FSM,只 emit 让消费方决定 */
244
+ const FSM_SCENE_SWITCH_SIGNAL = 'fsm:scene-switch';
181
245
  class StateMachineSystem extends System {
182
246
  constructor() {
183
247
  super(...arguments);
184
248
  this.name = 'StateMachine';
249
+ this.emitSceneSwitch = true;
250
+ this.sceneChangedHandler = null;
251
+ }
252
+ init(params) {
253
+ if ((params === null || params === void 0 ? void 0 : params.emitSceneSwitch) === false)
254
+ this.emitSceneSwitch = false;
255
+ }
256
+ awake() {
257
+ if (!this.emitSceneSwitch)
258
+ return;
259
+ if (!this.game)
260
+ return;
261
+ this.sceneChangedHandler = (raw) => {
262
+ // game.emit('sceneChanged', { scene, mode, params }) — 透传整包,
263
+ // 同时把 scene 抽到顶层方便消费方判断
264
+ const scene = raw && typeof raw === 'object' && 'scene' in raw
265
+ ? raw.scene
266
+ : undefined;
267
+ const payload = { scene, raw };
268
+ getSignalBus().emit(FSM_SCENE_SWITCH_SIGNAL, payload);
269
+ };
270
+ this.game.on('sceneChanged', this.sceneChangedHandler);
271
+ }
272
+ onDestroy() {
273
+ if (this.sceneChangedHandler && this.game) {
274
+ this.game.off('sceneChanged', this.sceneChangedHandler);
275
+ }
276
+ this.sceneChangedHandler = null;
185
277
  }
186
278
  }
187
279
  StateMachineSystem.systemName = 'StateMachine';
188
280
 
189
- export { StateMachine, StateMachineSystem };
281
+ 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.5",
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.5",
21
+ "@eva/plugin-signal-bus": "2.1.0-beta.5"
22
22
  }
23
23
  }