@eva/plugin-state-machine 2.1.0-beta.3 → 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
|
-
|
|
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
|
-
|
|
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"),
|
|
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
|
-
|
|
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 —
|
|
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
|
-
*
|
|
79
|
-
* 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
|
|
166
|
+
* 默认行为可通过 `params.emitSceneSwitch = false` 关闭(纯静默注册)。
|
|
80
167
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
21
|
-
"@eva/plugin-signal-bus": "2.1.0-beta.
|
|
20
|
+
"@eva/eva.js": "2.1.0-beta.5",
|
|
21
|
+
"@eva/plugin-signal-bus": "2.1.0-beta.5"
|
|
22
22
|
}
|
|
23
23
|
}
|