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

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,193 @@
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
+ /**
36
+ * StateMachine 组件 — 简易有限状态机。
37
+ *
38
+ * DSL 用法:
39
+ * ```json
40
+ * {
41
+ * "type": "StateMachine",
42
+ * "props": {
43
+ * "initial": "moving",
44
+ * "states": {
45
+ * "moving": { "onEnter": "monster:state-moving",
46
+ * "transitions": [
47
+ * { "on": "monster:hit", "to": "knockback" },
48
+ * { "after": 3000, "to": "resting" }
49
+ * ] },
50
+ * "resting": { "transitions": [{ "after": 1000, "to": "moving" }] },
51
+ * "knockback":{ "transitions": [{ "after": 800, "to": "resting" }] }
52
+ * },
53
+ * "signalChange": "monster:state-change"
54
+ * }
55
+ * }
56
+ * ```
57
+ *
58
+ * 行为:
59
+ * - 进入状态:emit `onEnter` + `signalChange` { from, to }
60
+ * - `transitions[i].on`: 监听信号,信号触发即迁移
61
+ * - `transitions[i].after`: 进入状态 N ms 后自动迁移
62
+ * - `transitions[i].guard`: JS 表达式,以 `ctx` 为变量,假则跳过该规则
63
+ *
64
+ * 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
65
+ */
66
+ exports.StateMachine = class StateMachine extends eva_js.Component {
67
+ constructor() {
68
+ super(...arguments);
69
+ this.states = {};
70
+ this.current = '';
71
+ this.elapsedInState = 0;
72
+ this.subs = [];
73
+ /** 上下文,guard 可读 */
74
+ this.ctx = {};
75
+ }
76
+ init(params) {
77
+ var _a, _b;
78
+ if (!params)
79
+ return;
80
+ this.states = (_a = params.states) !== null && _a !== void 0 ? _a : {};
81
+ this.signalChange = params.signalChange;
82
+ this.ctx = (_b = params.context) !== null && _b !== void 0 ? _b : {};
83
+ if (params.initial && this.states[params.initial]) {
84
+ this.enter(params.initial, '__init__');
85
+ }
86
+ }
87
+ /** 主动迁移(代码侧也能调) */
88
+ goto(to, reason = 'manual') {
89
+ var _a;
90
+ if (!this.states[to]) {
91
+ // eslint-disable-next-line no-console
92
+ console.warn(`[plugin-state-machine] no such state: ${to}`);
93
+ return;
94
+ }
95
+ if (this.current === to)
96
+ return;
97
+ const from = this.current;
98
+ if (from && ((_a = this.states[from]) === null || _a === void 0 ? void 0 : _a.onExit)) {
99
+ pluginSignalBus.getSignalBus().emit(this.states[from].onExit, { from, to, reason });
100
+ }
101
+ this.cleanupSubs();
102
+ this.enter(to, reason);
103
+ }
104
+ get state() {
105
+ return this.current;
106
+ }
107
+ enter(to, reason) {
108
+ var _a;
109
+ const from = this.current;
110
+ this.current = to;
111
+ this.elapsedInState = 0;
112
+ const cfg = this.states[to];
113
+ if (!cfg)
114
+ return;
115
+ if (cfg.onEnter)
116
+ pluginSignalBus.getSignalBus().emit(cfg.onEnter, { from, to, reason });
117
+ if (this.signalChange)
118
+ pluginSignalBus.getSignalBus().emit(this.signalChange, { from, to, reason });
119
+ // 订阅本 state 的 on 信号
120
+ const bus = pluginSignalBus.getSignalBus();
121
+ for (const t of (_a = cfg.transitions) !== null && _a !== void 0 ? _a : []) {
122
+ if (!t.on)
123
+ continue;
124
+ const target = t.to;
125
+ const guard = t.guard;
126
+ const h = bus.on(t.on, () => {
127
+ if (this.current !== to)
128
+ return; // 已经离开
129
+ if (guard && !this.evalGuard(guard))
130
+ return;
131
+ this.goto(target, t.on);
132
+ });
133
+ this.subs.push(h);
134
+ }
135
+ }
136
+ cleanupSubs() {
137
+ for (const h of this.subs)
138
+ h.dispose();
139
+ this.subs = [];
140
+ }
141
+ evalGuard(guard) {
142
+ try {
143
+ // 简单 expression eval,只暴露 ctx
144
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
145
+ const fn = new Function('ctx', `return (${guard});`);
146
+ return Boolean(fn(this.ctx));
147
+ }
148
+ catch (err) {
149
+ // eslint-disable-next-line no-console
150
+ console.warn(`[plugin-state-machine] bad guard "${guard}":`, err);
151
+ return false;
152
+ }
153
+ }
154
+ update(e) {
155
+ this.elapsedInState += e.deltaTime;
156
+ const cfg = this.states[this.current];
157
+ if (!(cfg === null || cfg === void 0 ? void 0 : cfg.transitions))
158
+ return;
159
+ for (const t of cfg.transitions) {
160
+ if (t.after == null)
161
+ continue;
162
+ if (this.elapsedInState < t.after)
163
+ continue;
164
+ if (t.guard && !this.evalGuard(t.guard))
165
+ continue;
166
+ this.goto(t.to, `after:${t.after}`);
167
+ return;
168
+ }
169
+ }
170
+ onDestroy() {
171
+ this.cleanupSubs();
172
+ }
173
+ };
174
+ exports.StateMachine.componentName = 'StateMachine';
175
+ exports.StateMachine = __decorate([
176
+ eva_js.decorators.componentObserver({})
177
+ ], exports.StateMachine);
178
+
179
+ /**
180
+ * StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
181
+ *
182
+ * 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
183
+ * 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
184
+ */
185
+ class StateMachineSystem extends eva_js.System {
186
+ constructor() {
187
+ super(...arguments);
188
+ this.name = 'StateMachine';
189
+ }
190
+ }
191
+ StateMachineSystem.systemName = 'StateMachine';
192
+
193
+ exports.StateMachineSystem = StateMachineSystem;
@@ -0,0 +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;
@@ -0,0 +1,98 @@
1
+ import { Component } from '@eva/eva.js';
2
+ import { System } from '@eva/eva.js';
3
+
4
+ export declare interface StateConfig {
5
+ /** 进入时 emit 的信号 */
6
+ onEnter?: string;
7
+ /** 退出时 emit 的信号 */
8
+ onExit?: string;
9
+ /** 该状态下的迁移规则 */
10
+ transitions?: TransitionRule[];
11
+ }
12
+
13
+ /**
14
+ * StateMachine 组件 — 简易有限状态机。
15
+ *
16
+ * DSL 用法:
17
+ * ```json
18
+ * {
19
+ * "type": "StateMachine",
20
+ * "props": {
21
+ * "initial": "moving",
22
+ * "states": {
23
+ * "moving": { "onEnter": "monster:state-moving",
24
+ * "transitions": [
25
+ * { "on": "monster:hit", "to": "knockback" },
26
+ * { "after": 3000, "to": "resting" }
27
+ * ] },
28
+ * "resting": { "transitions": [{ "after": 1000, "to": "moving" }] },
29
+ * "knockback":{ "transitions": [{ "after": 800, "to": "resting" }] }
30
+ * },
31
+ * "signalChange": "monster:state-change"
32
+ * }
33
+ * }
34
+ * ```
35
+ *
36
+ * 行为:
37
+ * - 进入状态:emit `onEnter` + `signalChange` { from, to }
38
+ * - `transitions[i].on`: 监听信号,信号触发即迁移
39
+ * - `transitions[i].after`: 进入状态 N ms 后自动迁移
40
+ * - `transitions[i].guard`: JS 表达式,以 `ctx` 为变量,假则跳过该规则
41
+ *
42
+ * 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
43
+ */
44
+ export declare class StateMachine extends Component<StateMachineParams> {
45
+ static componentName: string;
46
+ private states;
47
+ private current;
48
+ private signalChange?;
49
+ private elapsedInState;
50
+ private subs;
51
+ /** 上下文,guard 可读 */
52
+ ctx: Record<string, any>;
53
+ init(params?: StateMachineParams): void;
54
+ /** 主动迁移(代码侧也能调) */
55
+ goto(to: string, reason?: string): void;
56
+ get state(): string;
57
+ private enter;
58
+ private cleanupSubs;
59
+ private evalGuard;
60
+ update(e: {
61
+ deltaTime: number;
62
+ }): void;
63
+ onDestroy(): void;
64
+ }
65
+
66
+ export declare interface StateMachineParams {
67
+ initial: string;
68
+ states: Record<string, StateConfig>;
69
+ /** 状态切换 emit 信号 */
70
+ signalChange?: string;
71
+ /** 上下文变量,可在 guard 中读 */
72
+ context?: Record<string, any>;
73
+ }
74
+
75
+ /**
76
+ * StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
77
+ *
78
+ * 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
79
+ * 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
80
+ */
81
+ export declare class StateMachineSystem extends System {
82
+ static systemName: string;
83
+ readonly name = "StateMachine";
84
+ }
85
+
86
+ /** 单条迁移规则 */
87
+ export declare interface TransitionRule {
88
+ /** 触发该迁移的信号名 */
89
+ on?: string;
90
+ /** 进入该状态后等待 N ms 自动迁移(可与 on 二选一) */
91
+ after?: number;
92
+ /** 目标状态名 */
93
+ to: string;
94
+ /** 可选:JS 表达式字符串,以 ctx 为上下文 */
95
+ guard?: string;
96
+ }
97
+
98
+ export { }
@@ -0,0 +1,189 @@
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
+ /**
32
+ * StateMachine 组件 — 简易有限状态机。
33
+ *
34
+ * DSL 用法:
35
+ * ```json
36
+ * {
37
+ * "type": "StateMachine",
38
+ * "props": {
39
+ * "initial": "moving",
40
+ * "states": {
41
+ * "moving": { "onEnter": "monster:state-moving",
42
+ * "transitions": [
43
+ * { "on": "monster:hit", "to": "knockback" },
44
+ * { "after": 3000, "to": "resting" }
45
+ * ] },
46
+ * "resting": { "transitions": [{ "after": 1000, "to": "moving" }] },
47
+ * "knockback":{ "transitions": [{ "after": 800, "to": "resting" }] }
48
+ * },
49
+ * "signalChange": "monster:state-change"
50
+ * }
51
+ * }
52
+ * ```
53
+ *
54
+ * 行为:
55
+ * - 进入状态:emit `onEnter` + `signalChange` { from, to }
56
+ * - `transitions[i].on`: 监听信号,信号触发即迁移
57
+ * - `transitions[i].after`: 进入状态 N ms 后自动迁移
58
+ * - `transitions[i].guard`: JS 表达式,以 `ctx` 为变量,假则跳过该规则
59
+ *
60
+ * 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
61
+ */
62
+ let StateMachine = class StateMachine extends Component {
63
+ constructor() {
64
+ super(...arguments);
65
+ this.states = {};
66
+ this.current = '';
67
+ this.elapsedInState = 0;
68
+ this.subs = [];
69
+ /** 上下文,guard 可读 */
70
+ this.ctx = {};
71
+ }
72
+ init(params) {
73
+ var _a, _b;
74
+ if (!params)
75
+ return;
76
+ this.states = (_a = params.states) !== null && _a !== void 0 ? _a : {};
77
+ this.signalChange = params.signalChange;
78
+ this.ctx = (_b = params.context) !== null && _b !== void 0 ? _b : {};
79
+ if (params.initial && this.states[params.initial]) {
80
+ this.enter(params.initial, '__init__');
81
+ }
82
+ }
83
+ /** 主动迁移(代码侧也能调) */
84
+ goto(to, reason = 'manual') {
85
+ var _a;
86
+ if (!this.states[to]) {
87
+ // eslint-disable-next-line no-console
88
+ console.warn(`[plugin-state-machine] no such state: ${to}`);
89
+ return;
90
+ }
91
+ if (this.current === to)
92
+ return;
93
+ const from = this.current;
94
+ if (from && ((_a = this.states[from]) === null || _a === void 0 ? void 0 : _a.onExit)) {
95
+ getSignalBus().emit(this.states[from].onExit, { from, to, reason });
96
+ }
97
+ this.cleanupSubs();
98
+ this.enter(to, reason);
99
+ }
100
+ get state() {
101
+ return this.current;
102
+ }
103
+ enter(to, reason) {
104
+ var _a;
105
+ const from = this.current;
106
+ this.current = to;
107
+ this.elapsedInState = 0;
108
+ const cfg = this.states[to];
109
+ if (!cfg)
110
+ return;
111
+ if (cfg.onEnter)
112
+ getSignalBus().emit(cfg.onEnter, { from, to, reason });
113
+ if (this.signalChange)
114
+ getSignalBus().emit(this.signalChange, { from, to, reason });
115
+ // 订阅本 state 的 on 信号
116
+ const bus = getSignalBus();
117
+ for (const t of (_a = cfg.transitions) !== null && _a !== void 0 ? _a : []) {
118
+ if (!t.on)
119
+ continue;
120
+ const target = t.to;
121
+ const guard = t.guard;
122
+ const h = bus.on(t.on, () => {
123
+ if (this.current !== to)
124
+ return; // 已经离开
125
+ if (guard && !this.evalGuard(guard))
126
+ return;
127
+ this.goto(target, t.on);
128
+ });
129
+ this.subs.push(h);
130
+ }
131
+ }
132
+ cleanupSubs() {
133
+ for (const h of this.subs)
134
+ h.dispose();
135
+ this.subs = [];
136
+ }
137
+ evalGuard(guard) {
138
+ try {
139
+ // 简单 expression eval,只暴露 ctx
140
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
141
+ const fn = new Function('ctx', `return (${guard});`);
142
+ return Boolean(fn(this.ctx));
143
+ }
144
+ catch (err) {
145
+ // eslint-disable-next-line no-console
146
+ console.warn(`[plugin-state-machine] bad guard "${guard}":`, err);
147
+ return false;
148
+ }
149
+ }
150
+ update(e) {
151
+ this.elapsedInState += e.deltaTime;
152
+ const cfg = this.states[this.current];
153
+ if (!(cfg === null || cfg === void 0 ? void 0 : cfg.transitions))
154
+ return;
155
+ for (const t of cfg.transitions) {
156
+ if (t.after == null)
157
+ continue;
158
+ if (this.elapsedInState < t.after)
159
+ continue;
160
+ if (t.guard && !this.evalGuard(t.guard))
161
+ continue;
162
+ this.goto(t.to, `after:${t.after}`);
163
+ return;
164
+ }
165
+ }
166
+ onDestroy() {
167
+ this.cleanupSubs();
168
+ }
169
+ };
170
+ StateMachine.componentName = 'StateMachine';
171
+ StateMachine = __decorate([
172
+ decorators.componentObserver({})
173
+ ], StateMachine);
174
+
175
+ /**
176
+ * StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
177
+ *
178
+ * 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
179
+ * 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
180
+ */
181
+ class StateMachineSystem extends System {
182
+ constructor() {
183
+ super(...arguments);
184
+ this.name = 'StateMachine';
185
+ }
186
+ }
187
+ StateMachineSystem.systemName = 'StateMachine';
188
+
189
+ export { 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.3",
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.3",
21
+ "@eva/plugin-signal-bus": "2.1.0-beta.3"
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
- }