refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,23 @@
{
"id": "fsm",
"name": "@esengine/fsm",
"globalKey": "fsm",
"displayName": "State Machine",
"description": "有限状态机 | Finite State Machine",
"version": "1.0.0",
"category": "AI",
"icon": "GitBranch",
"tags": ["fsm", "state", "machine", "ai"],
"isCore": false,
"defaultEnabled": true,
"isEngineModule": true,
"canContainContent": false,
"platforms": ["web", "desktop"],
"dependencies": ["core"],
"exports": {
"components": ["StateMachineComponent"],
"systems": []
},
"outputPath": "dist/index.js",
"pluginExport": "FSMPlugin"
}

View File

@@ -0,0 +1,40 @@
{
"name": "@esengine/fsm",
"version": "1.0.0",
"description": "Finite State Machine for ECS Framework / ECS 框架的有限状态机",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"module.json"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"tslib": "^2.8.1"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/blueprint": "workspace:*",
"@esengine/build-config": "workspace:*",
"@types/node": "^20.19.17",
"rimraf": "^5.0.0",
"tsup": "^8.0.0",
"typescript": "^5.8.3"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,364 @@
/**
* @zh 状态机接口
* @en State Machine Interface
*
* @zh 提供有限状态机的核心接口
* @en Provides core interfaces for finite state machine
*/
// =============================================================================
// 状态配置 | State Configuration
// =============================================================================
/**
* @zh 状态配置
* @en State configuration
*/
export interface StateConfig<TState extends string = string, TContext = unknown> {
/**
* @zh 状态名称
* @en State name
*/
readonly name: TState;
/**
* @zh 进入状态时的回调
* @en Callback when entering state
*/
onEnter?: (context: TContext, from: TState | null) => void;
/**
* @zh 退出状态时的回调
* @en Callback when exiting state
*/
onExit?: (context: TContext, to: TState) => void;
/**
* @zh 状态更新回调
* @en State update callback
*/
onUpdate?: (context: TContext, deltaTime: number) => void;
/**
* @zh 状态标签
* @en State tags
*/
tags?: string[];
/**
* @zh 状态元数据
* @en State metadata
*/
metadata?: Record<string, unknown>;
}
// =============================================================================
// 转换配置 | Transition Configuration
// =============================================================================
/**
* @zh 转换条件函数
* @en Transition condition function
*/
export type TransitionCondition<TContext = unknown> = (context: TContext) => boolean;
/**
* @zh 转换配置
* @en Transition configuration
*/
export interface TransitionConfig<TState extends string = string, TContext = unknown> {
/**
* @zh 源状态
* @en Source state
*/
readonly from: TState;
/**
* @zh 目标状态
* @en Target state
*/
readonly to: TState;
/**
* @zh 转换条件
* @en Transition condition
*/
condition?: TransitionCondition<TContext>;
/**
* @zh 转换优先级(数字越大优先级越高)
* @en Transition priority (higher number = higher priority)
*/
priority?: number;
/**
* @zh 转换名称(用于调试)
* @en Transition name (for debugging)
*/
name?: string;
}
// =============================================================================
// 状态机事件 | State Machine Events
// =============================================================================
/**
* @zh 状态变更事件
* @en State change event
*/
export interface StateChangeEvent<TState extends string = string> {
/**
* @zh 之前的状态
* @en Previous state
*/
readonly from: TState | null;
/**
* @zh 当前状态
* @en Current state
*/
readonly to: TState;
/**
* @zh 时间戳
* @en Timestamp
*/
readonly timestamp: number;
}
/**
* @zh 状态变更监听器
* @en State change listener
*/
export type StateChangeListener<TState extends string = string> = (
event: StateChangeEvent<TState>
) => void;
// =============================================================================
// 状态机接口 | State Machine Interface
// =============================================================================
/**
* @zh 状态机接口
* @en State machine interface
*
* @zh 通用有限状态机,用于角色/AI 状态管理
* @en Generic finite state machine for character/AI state management
*
* @example
* ```typescript
* type PlayerState = 'idle' | 'walk' | 'run' | 'jump' | 'attack';
*
* const fsm = createStateMachine<PlayerState>('idle');
*
* fsm.defineState('idle', {
* onEnter: () => console.log('Entering idle'),
* onExit: () => console.log('Exiting idle')
* });
*
* fsm.defineTransition('idle', 'walk', () => isMoving);
* fsm.defineTransition('walk', 'run', () => isRunning);
*
* fsm.transition('walk'); // 手动转换
* fsm.evaluateTransitions(); // 自动评估条件
* ```
*/
export interface IStateMachine<TState extends string = string, TContext = unknown> {
/**
* @zh 当前状态
* @en Current state
*/
readonly current: TState;
/**
* @zh 之前的状态
* @en Previous state
*/
readonly previous: TState | null;
/**
* @zh 状态机上下文
* @en State machine context
*/
readonly context: TContext;
/**
* @zh 是否正在转换中
* @en Whether a transition is in progress
*/
readonly isTransitioning: boolean;
/**
* @zh 当前状态持续时间(毫秒)
* @en Current state duration in milliseconds
*/
readonly currentStateDuration: number;
// =========================================================================
// 状态定义 | State Definition
// =========================================================================
/**
* @zh 定义状态
* @en Define state
*
* @param state - @zh 状态名称 @en State name
* @param config - @zh 状态配置 @en State configuration
*/
defineState(state: TState, config?: Partial<StateConfig<TState, TContext>>): void;
/**
* @zh 检查状态是否已定义
* @en Check if state is defined
*
* @param state - @zh 状态名称 @en State name
*/
hasState(state: TState): boolean;
/**
* @zh 获取状态配置
* @en Get state configuration
*
* @param state - @zh 状态名称 @en State name
*/
getStateConfig(state: TState): StateConfig<TState, TContext> | undefined;
/**
* @zh 获取所有定义的状态
* @en Get all defined states
*/
getStates(): TState[];
// =========================================================================
// 转换定义 | Transition Definition
// =========================================================================
/**
* @zh 定义转换
* @en Define transition
*
* @param from - @zh 源状态 @en Source state
* @param to - @zh 目标状态 @en Target state
* @param condition - @zh 转换条件 @en Transition condition
* @param priority - @zh 优先级 @en Priority
*/
defineTransition(
from: TState,
to: TState,
condition?: TransitionCondition<TContext>,
priority?: number
): void;
/**
* @zh 移除转换
* @en Remove transition
*
* @param from - @zh 源状态 @en Source state
* @param to - @zh 目标状态 @en Target state
*/
removeTransition(from: TState, to: TState): void;
/**
* @zh 获取从指定状态可用的转换
* @en Get available transitions from state
*
* @param from - @zh 源状态 @en Source state
*/
getTransitionsFrom(from: TState): TransitionConfig<TState, TContext>[];
// =========================================================================
// 转换操作 | Transition Operations
// =========================================================================
/**
* @zh 检查是否可以转换到目标状态
* @en Check if can transition to target state
*
* @param to - @zh 目标状态 @en Target state
*/
canTransition(to: TState): boolean;
/**
* @zh 转换到目标状态
* @en Transition to target state
*
* @param to - @zh 目标状态 @en Target state
* @param force - @zh 强制转换(忽略条件)@en Force transition (ignore condition)
* @returns @zh 是否成功 @en Whether successful
*/
transition(to: TState, force?: boolean): boolean;
/**
* @zh 评估并执行满足条件的转换
* @en Evaluate and execute transitions that meet conditions
*
* @returns @zh 是否发生转换 @en Whether a transition occurred
*/
evaluateTransitions(): boolean;
// =========================================================================
// 生命周期 | Lifecycle
// =========================================================================
/**
* @zh 更新状态机
* @en Update state machine
*
* @param deltaTime - @zh 增量时间(毫秒)@en Delta time in milliseconds
*/
update(deltaTime: number): void;
/**
* @zh 重置状态机到初始状态
* @en Reset state machine to initial state
*
* @param initialState - @zh 初始状态 @en Initial state
*/
reset(initialState?: TState): void;
// =========================================================================
// 事件监听 | Event Listening
// =========================================================================
/**
* @zh 监听状态进入事件
* @en Listen to state enter event
*
* @param state - @zh 状态名称 @en State name
* @param callback - @zh 回调函数 @en Callback function
*/
onEnter(state: TState, callback: (from: TState | null) => void): () => void;
/**
* @zh 监听状态退出事件
* @en Listen to state exit event
*
* @param state - @zh 状态名称 @en State name
* @param callback - @zh 回调函数 @en Callback function
*/
onExit(state: TState, callback: (to: TState) => void): () => void;
/**
* @zh 监听任意状态变更
* @en Listen to any state change
*
* @param callback - @zh 回调函数 @en Callback function
*/
onChange(callback: StateChangeListener<TState>): () => void;
// =========================================================================
// 调试 | Debug
// =========================================================================
/**
* @zh 获取状态历史
* @en Get state history
*/
getHistory(): StateChangeEvent<TState>[];
/**
* @zh 清除历史
* @en Clear history
*/
clearHistory(): void;
}

View File

@@ -0,0 +1,445 @@
/**
* @zh 状态机实现
* @en State Machine Implementation
*
* @zh 提供有限状态机的默认实现
* @en Provides default implementation for finite state machine
*/
import type {
IStateMachine,
StateConfig,
TransitionConfig,
TransitionCondition,
StateChangeEvent,
StateChangeListener
} from './IStateMachine';
// =============================================================================
// 状态机配置 | State Machine Configuration
// =============================================================================
/**
* @zh 状态机配置选项
* @en State machine configuration options
*/
export interface StateMachineOptions<TContext = unknown> {
/**
* @zh 上下文对象
* @en Context object
*/
context?: TContext;
/**
* @zh 最大历史记录数量
* @en Maximum history size
*/
maxHistorySize?: number;
/**
* @zh 是否启用历史记录
* @en Whether to enable history
*/
enableHistory?: boolean;
}
// =============================================================================
// 状态机实现 | State Machine Implementation
// =============================================================================
/**
* @zh 状态机实现
* @en State machine implementation
*/
export class StateMachine<TState extends string = string, TContext = unknown>
implements IStateMachine<TState, TContext>
{
private _current: TState;
private _previous: TState | null = null;
private _context: TContext;
private _isTransitioning = false;
private _stateStartTime = 0;
private states: Map<TState, StateConfig<TState, TContext>> = new Map();
private transitions: Map<TState, TransitionConfig<TState, TContext>[]> = new Map();
private enterListeners: Map<TState, Set<(from: TState | null) => void>> = new Map();
private exitListeners: Map<TState, Set<(to: TState) => void>> = new Map();
private changeListeners: Set<StateChangeListener<TState>> = new Set();
private history: StateChangeEvent<TState>[] = [];
private maxHistorySize: number;
private enableHistory: boolean;
constructor(initialState: TState, options: StateMachineOptions<TContext> = {}) {
this._current = initialState;
this._context = (options.context ?? {}) as TContext;
this.maxHistorySize = options.maxHistorySize ?? 100;
this.enableHistory = options.enableHistory ?? true;
this._stateStartTime = Date.now();
// Auto-define initial state if not defined
this.defineState(initialState);
}
// =========================================================================
// 属性 | Properties
// =========================================================================
get current(): TState {
return this._current;
}
get previous(): TState | null {
return this._previous;
}
get context(): TContext {
return this._context;
}
get isTransitioning(): boolean {
return this._isTransitioning;
}
get currentStateDuration(): number {
return Date.now() - this._stateStartTime;
}
// =========================================================================
// 状态定义 | State Definition
// =========================================================================
defineState(state: TState, config?: Partial<StateConfig<TState, TContext>>): void {
const stateConfig: StateConfig<TState, TContext> = {
name: state,
...config
};
this.states.set(state, stateConfig);
}
hasState(state: TState): boolean {
return this.states.has(state);
}
getStateConfig(state: TState): StateConfig<TState, TContext> | undefined {
return this.states.get(state);
}
getStates(): TState[] {
return Array.from(this.states.keys());
}
// =========================================================================
// 转换定义 | Transition Definition
// =========================================================================
defineTransition(
from: TState,
to: TState,
condition?: TransitionCondition<TContext>,
priority = 0
): void {
if (!this.transitions.has(from)) {
this.transitions.set(from, []);
}
const transitions = this.transitions.get(from)!;
// Remove existing transition with same from/to
const existingIndex = transitions.findIndex(t => t.to === to);
if (existingIndex >= 0) {
transitions.splice(existingIndex, 1);
}
transitions.push({
from,
to,
condition,
priority
});
// Sort by priority (descending)
transitions.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
}
removeTransition(from: TState, to: TState): void {
const transitions = this.transitions.get(from);
if (transitions) {
const index = transitions.findIndex(t => t.to === to);
if (index >= 0) {
transitions.splice(index, 1);
}
}
}
getTransitionsFrom(from: TState): TransitionConfig<TState, TContext>[] {
return this.transitions.get(from) ?? [];
}
// =========================================================================
// 转换操作 | Transition Operations
// =========================================================================
canTransition(to: TState): boolean {
if (this._isTransitioning) {
return false;
}
if (this._current === to) {
return false;
}
const transitions = this.transitions.get(this._current);
if (!transitions) {
return true; // Allow if no restrictions defined
}
const transition = transitions.find(t => t.to === to);
if (!transition) {
return true; // Allow if no specific transition defined
}
if (transition.condition) {
return transition.condition(this._context);
}
return true;
}
transition(to: TState, force = false): boolean {
if (this._isTransitioning) {
console.warn('StateMachine: Cannot transition while already transitioning');
return false;
}
if (this._current === to) {
return false;
}
if (!force && !this.canTransition(to)) {
return false;
}
this.performTransition(to);
return true;
}
evaluateTransitions(): boolean {
if (this._isTransitioning) {
return false;
}
const transitions = this.transitions.get(this._current);
if (!transitions || transitions.length === 0) {
return false;
}
for (const transition of transitions) {
if (!transition.condition || transition.condition(this._context)) {
this.performTransition(transition.to);
return true;
}
}
return false;
}
private performTransition(to: TState): void {
this._isTransitioning = true;
const from = this._current;
// Exit current state
const currentConfig = this.states.get(from);
if (currentConfig?.onExit) {
try {
currentConfig.onExit(this._context, to);
} catch (error) {
console.error(`StateMachine: Error in onExit for state '${from}':`, error);
}
}
// Notify exit listeners
const exitListeners = this.exitListeners.get(from);
if (exitListeners) {
for (const listener of exitListeners) {
try {
listener(to);
} catch (error) {
console.error(`StateMachine: Error in exit listener for state '${from}':`, error);
}
}
}
// Update state
this._previous = from;
this._current = to;
this._stateStartTime = Date.now();
// Record history
if (this.enableHistory) {
const event: StateChangeEvent<TState> = {
from,
to,
timestamp: this._stateStartTime
};
this.history.push(event);
if (this.history.length > this.maxHistorySize) {
this.history.shift();
}
// Notify change listeners
for (const listener of this.changeListeners) {
try {
listener(event);
} catch (error) {
console.error('StateMachine: Error in change listener:', error);
}
}
}
// Enter new state
const newConfig = this.states.get(to);
if (newConfig?.onEnter) {
try {
newConfig.onEnter(this._context, from);
} catch (error) {
console.error(`StateMachine: Error in onEnter for state '${to}':`, error);
}
}
// Notify enter listeners
const enterListeners = this.enterListeners.get(to);
if (enterListeners) {
for (const listener of enterListeners) {
try {
listener(from);
} catch (error) {
console.error(`StateMachine: Error in enter listener for state '${to}':`, error);
}
}
}
this._isTransitioning = false;
}
// =========================================================================
// 生命周期 | Lifecycle
// =========================================================================
update(deltaTime: number): void {
const config = this.states.get(this._current);
if (config?.onUpdate) {
try {
config.onUpdate(this._context, deltaTime);
} catch (error) {
console.error(`StateMachine: Error in onUpdate for state '${this._current}':`, error);
}
}
}
reset(initialState?: TState): void {
const targetState = initialState ?? this._current;
this._previous = null;
this._current = targetState;
this._stateStartTime = Date.now();
this._isTransitioning = false;
this.clearHistory();
}
// =========================================================================
// 事件监听 | Event Listening
// =========================================================================
onEnter(state: TState, callback: (from: TState | null) => void): () => void {
if (!this.enterListeners.has(state)) {
this.enterListeners.set(state, new Set());
}
this.enterListeners.get(state)!.add(callback);
return () => {
this.enterListeners.get(state)?.delete(callback);
};
}
onExit(state: TState, callback: (to: TState) => void): () => void {
if (!this.exitListeners.has(state)) {
this.exitListeners.set(state, new Set());
}
this.exitListeners.get(state)!.add(callback);
return () => {
this.exitListeners.get(state)?.delete(callback);
};
}
onChange(callback: StateChangeListener<TState>): () => void {
this.changeListeners.add(callback);
return () => {
this.changeListeners.delete(callback);
};
}
// =========================================================================
// 调试 | Debug
// =========================================================================
getHistory(): StateChangeEvent<TState>[] {
return [...this.history];
}
clearHistory(): void {
this.history = [];
}
/**
* @zh 获取状态机的调试信息
* @en Get debug info for the state machine
*/
getDebugInfo(): {
current: TState;
previous: TState | null;
duration: number;
stateCount: number;
transitionCount: number;
historySize: number;
} {
let transitionCount = 0;
for (const transitions of this.transitions.values()) {
transitionCount += transitions.length;
}
return {
current: this._current,
previous: this._previous,
duration: this.currentStateDuration,
stateCount: this.states.size,
transitionCount,
historySize: this.history.length
};
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh 创建状态机
* @en Create state machine
*
* @param initialState - @zh 初始状态 @en Initial state
* @param options - @zh 配置选项 @en Configuration options
* @returns @zh 状态机实例 @en State machine instance
*/
export function createStateMachine<TState extends string = string, TContext = unknown>(
initialState: TState,
options?: StateMachineOptions<TContext>
): IStateMachine<TState, TContext> {
return new StateMachine<TState, TContext>(initialState, options);
}

View File

@@ -0,0 +1,60 @@
/**
* @zh @esengine/fsm - 有限状态机
* @en @esengine/fsm - Finite State Machine
*
* @zh 提供通用状态机功能,用于角色/AI 状态管理
* @en Provides generic state machine for character/AI state management
*/
// =============================================================================
// 接口和类型 | Interfaces and Types
// =============================================================================
export type {
StateConfig,
TransitionConfig,
TransitionCondition,
StateChangeEvent,
StateChangeListener,
IStateMachine
} from './IStateMachine';
// =============================================================================
// 实现 | Implementations
// =============================================================================
export type { StateMachineOptions } from './StateMachine';
export { StateMachine, createStateMachine } from './StateMachine';
// =============================================================================
// 服务令牌 | Service Tokens
// =============================================================================
export { StateMachineToken } from './tokens';
// =============================================================================
// 蓝图节点 | Blueprint Nodes
// =============================================================================
export {
// Templates
GetCurrentStateTemplate,
TransitionToTemplate,
CanTransitionTemplate,
IsInStateTemplate,
WasInStateTemplate,
GetStateDurationTemplate,
EvaluateTransitionsTemplate,
ResetStateMachineTemplate,
// Executors
GetCurrentStateExecutor,
TransitionToExecutor,
CanTransitionExecutor,
IsInStateExecutor,
WasInStateExecutor,
GetStateDurationExecutor,
EvaluateTransitionsExecutor,
ResetStateMachineExecutor,
// Collection
StateMachineNodeDefinitions
} from './nodes';

View File

@@ -0,0 +1,497 @@
/**
* @zh 状态机蓝图节点
* @en State Machine Blueprint Nodes
*
* @zh 提供状态机功能的蓝图节点
* @en Provides blueprint nodes for state machine functionality
*/
import type { BlueprintNodeTemplate, BlueprintNode, INodeExecutor, ExecutionResult } from '@esengine/blueprint';
import type { IStateMachine } from '../IStateMachine';
// =============================================================================
// 执行上下文接口 | Execution Context Interface
// =============================================================================
/**
* @zh 状态机上下文
* @en State machine context
*/
interface FSMContext {
stateMachine: IStateMachine<string, unknown>;
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
setOutputs(nodeId: string, outputs: Record<string, unknown>): void;
}
// =============================================================================
// GetCurrentState 节点 | GetCurrentState Node
// =============================================================================
/**
* @zh GetCurrentState 节点模板
* @en GetCurrentState node template
*/
export const GetCurrentStateTemplate: BlueprintNodeTemplate = {
type: 'GetCurrentState',
title: 'Get Current State',
category: 'logic',
description: 'Get current state of the state machine / 获取状态机当前状态',
keywords: ['fsm', 'state', 'current', 'get'],
menuPath: ['State Machine', 'Get Current State'],
isPure: true,
inputs: [],
outputs: [
{
name: 'state',
displayName: 'State',
type: 'string'
},
{
name: 'previous',
displayName: 'Previous',
type: 'string'
},
{
name: 'duration',
displayName: 'Duration (ms)',
type: 'float'
}
],
color: '#8b5a8b'
};
/**
* @zh GetCurrentState 节点执行器
* @en GetCurrentState node executor
*/
export class GetCurrentStateExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as FSMContext;
const fsm = ctx.stateMachine;
return {
outputs: {
state: fsm?.current ?? '',
previous: fsm?.previous ?? '',
duration: fsm?.currentStateDuration ?? 0
}
};
}
}
// =============================================================================
// TransitionTo 节点 | TransitionTo Node
// =============================================================================
/**
* @zh TransitionTo 节点模板
* @en TransitionTo node template
*/
export const TransitionToTemplate: BlueprintNodeTemplate = {
type: 'TransitionTo',
title: 'Transition To',
category: 'logic',
description: 'Transition to a new state / 转换到新状态',
keywords: ['fsm', 'state', 'transition', 'change'],
menuPath: ['State Machine', 'Transition To'],
isPure: false,
inputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
},
{
name: 'state',
displayName: 'Target State',
type: 'string',
defaultValue: ''
},
{
name: 'force',
displayName: 'Force',
type: 'bool',
defaultValue: false
}
],
outputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
},
{
name: 'success',
displayName: 'Success',
type: 'bool'
}
],
color: '#8b5a8b'
};
/**
* @zh TransitionTo 节点执行器
* @en TransitionTo node executor
*/
export class TransitionToExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as FSMContext;
const state = ctx.evaluateInput(node.id, 'state', '') as string;
const force = ctx.evaluateInput(node.id, 'force', false) as boolean;
let success = false;
if (state && ctx.stateMachine) {
success = ctx.stateMachine.transition(state, force);
}
return {
outputs: {
success
},
nextExec: 'exec'
};
}
}
// =============================================================================
// CanTransition 节点 | CanTransition Node
// =============================================================================
/**
* @zh CanTransition 节点模板
* @en CanTransition node template
*/
export const CanTransitionTemplate: BlueprintNodeTemplate = {
type: 'CanTransition',
title: 'Can Transition',
category: 'logic',
description: 'Check if can transition to state / 检查是否可以转换到状态',
keywords: ['fsm', 'state', 'transition', 'can', 'check'],
menuPath: ['State Machine', 'Can Transition'],
isPure: true,
inputs: [
{
name: 'state',
displayName: 'Target State',
type: 'string',
defaultValue: ''
}
],
outputs: [
{
name: 'canTransition',
displayName: 'Can Transition',
type: 'bool'
}
],
color: '#8b5a8b'
};
/**
* @zh CanTransition 节点执行器
* @en CanTransition node executor
*/
export class CanTransitionExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as FSMContext;
const state = ctx.evaluateInput(node.id, 'state', '') as string;
const canTransition = state ? ctx.stateMachine?.canTransition(state) ?? false : false;
return {
outputs: {
canTransition
}
};
}
}
// =============================================================================
// IsInState 节点 | IsInState Node
// =============================================================================
/**
* @zh IsInState 节点模板
* @en IsInState node template
*/
export const IsInStateTemplate: BlueprintNodeTemplate = {
type: 'IsInState',
title: 'Is In State',
category: 'logic',
description: 'Check if currently in a specific state / 检查是否处于特定状态',
keywords: ['fsm', 'state', 'is', 'check', 'current'],
menuPath: ['State Machine', 'Is In State'],
isPure: true,
inputs: [
{
name: 'state',
displayName: 'State',
type: 'string',
defaultValue: ''
}
],
outputs: [
{
name: 'isInState',
displayName: 'Is In State',
type: 'bool'
}
],
color: '#8b5a8b'
};
/**
* @zh IsInState 节点执行器
* @en IsInState node executor
*/
export class IsInStateExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as FSMContext;
const state = ctx.evaluateInput(node.id, 'state', '') as string;
const isInState = state ? ctx.stateMachine?.current === state : false;
return {
outputs: {
isInState
}
};
}
}
// =============================================================================
// WasInState 节点 | WasInState Node
// =============================================================================
/**
* @zh WasInState 节点模板
* @en WasInState node template
*/
export const WasInStateTemplate: BlueprintNodeTemplate = {
type: 'WasInState',
title: 'Was In State',
category: 'logic',
description: 'Check if was previously in a specific state / 检查之前是否处于特定状态',
keywords: ['fsm', 'state', 'was', 'previous', 'check'],
menuPath: ['State Machine', 'Was In State'],
isPure: true,
inputs: [
{
name: 'state',
displayName: 'State',
type: 'string',
defaultValue: ''
}
],
outputs: [
{
name: 'wasInState',
displayName: 'Was In State',
type: 'bool'
}
],
color: '#8b5a8b'
};
/**
* @zh WasInState 节点执行器
* @en WasInState node executor
*/
export class WasInStateExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as FSMContext;
const state = ctx.evaluateInput(node.id, 'state', '') as string;
const wasInState = state ? ctx.stateMachine?.previous === state : false;
return {
outputs: {
wasInState
}
};
}
}
// =============================================================================
// GetStateDuration 节点 | GetStateDuration Node
// =============================================================================
/**
* @zh GetStateDuration 节点模板
* @en GetStateDuration node template
*/
export const GetStateDurationTemplate: BlueprintNodeTemplate = {
type: 'GetStateDuration',
title: 'Get State Duration',
category: 'logic',
description: 'Get how long current state has been active / 获取当前状态持续时间',
keywords: ['fsm', 'state', 'duration', 'time'],
menuPath: ['State Machine', 'Get State Duration'],
isPure: true,
inputs: [],
outputs: [
{
name: 'duration',
displayName: 'Duration (ms)',
type: 'float'
},
{
name: 'seconds',
displayName: 'Seconds',
type: 'float'
}
],
color: '#8b5a8b'
};
/**
* @zh GetStateDuration 节点执行器
* @en GetStateDuration node executor
*/
export class GetStateDurationExecutor implements INodeExecutor {
execute(_node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as FSMContext;
const duration = ctx.stateMachine?.currentStateDuration ?? 0;
return {
outputs: {
duration,
seconds: duration / 1000
}
};
}
}
// =============================================================================
// EvaluateTransitions 节点 | EvaluateTransitions Node
// =============================================================================
/**
* @zh EvaluateTransitions 节点模板
* @en EvaluateTransitions node template
*/
export const EvaluateTransitionsTemplate: BlueprintNodeTemplate = {
type: 'EvaluateTransitions',
title: 'Evaluate Transitions',
category: 'logic',
description: 'Evaluate and execute automatic transitions / 评估并执行自动转换',
keywords: ['fsm', 'state', 'transition', 'evaluate', 'auto'],
menuPath: ['State Machine', 'Evaluate Transitions'],
isPure: false,
inputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
}
],
outputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
},
{
name: 'transitioned',
displayName: 'Transitioned',
type: 'bool'
}
],
color: '#8b5a8b'
};
/**
* @zh EvaluateTransitions 节点执行器
* @en EvaluateTransitions node executor
*/
export class EvaluateTransitionsExecutor implements INodeExecutor {
execute(_node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as FSMContext;
const transitioned = ctx.stateMachine?.evaluateTransitions() ?? false;
return {
outputs: {
transitioned
},
nextExec: 'exec'
};
}
}
// =============================================================================
// ResetStateMachine 节点 | ResetStateMachine Node
// =============================================================================
/**
* @zh ResetStateMachine 节点模板
* @en ResetStateMachine node template
*/
export const ResetStateMachineTemplate: BlueprintNodeTemplate = {
type: 'ResetStateMachine',
title: 'Reset State Machine',
category: 'logic',
description: 'Reset state machine to initial state / 重置状态机到初始状态',
keywords: ['fsm', 'state', 'reset', 'initial'],
menuPath: ['State Machine', 'Reset'],
isPure: false,
inputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
},
{
name: 'state',
displayName: 'Initial State',
type: 'string',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
displayName: '',
type: 'exec'
}
],
color: '#8b5a8b'
};
/**
* @zh ResetStateMachine 节点执行器
* @en ResetStateMachine node executor
*/
export class ResetStateMachineExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: unknown): ExecutionResult {
const ctx = context as FSMContext;
const state = ctx.evaluateInput(node.id, 'state', '') as string;
if (ctx.stateMachine) {
ctx.stateMachine.reset(state || undefined);
}
return {
outputs: {},
nextExec: 'exec'
};
}
}
// =============================================================================
// 节点定义集合 | Node Definition Collection
// =============================================================================
/**
* @zh 状态机节点定义
* @en State machine node definitions
*/
export const StateMachineNodeDefinitions = [
{ template: GetCurrentStateTemplate, executor: new GetCurrentStateExecutor() },
{ template: TransitionToTemplate, executor: new TransitionToExecutor() },
{ template: CanTransitionTemplate, executor: new CanTransitionExecutor() },
{ template: IsInStateTemplate, executor: new IsInStateExecutor() },
{ template: WasInStateTemplate, executor: new WasInStateExecutor() },
{ template: GetStateDurationTemplate, executor: new GetStateDurationExecutor() },
{ template: EvaluateTransitionsTemplate, executor: new EvaluateTransitionsExecutor() },
{ template: ResetStateMachineTemplate, executor: new ResetStateMachineExecutor() }
];

View File

@@ -0,0 +1,27 @@
/**
* @zh 状态机蓝图节点导出
* @en State Machine Blueprint Nodes Export
*/
export {
// Templates
GetCurrentStateTemplate,
TransitionToTemplate,
CanTransitionTemplate,
IsInStateTemplate,
WasInStateTemplate,
GetStateDurationTemplate,
EvaluateTransitionsTemplate,
ResetStateMachineTemplate,
// Executors
GetCurrentStateExecutor,
TransitionToExecutor,
CanTransitionExecutor,
IsInStateExecutor,
WasInStateExecutor,
GetStateDurationExecutor,
EvaluateTransitionsExecutor,
ResetStateMachineExecutor,
// Collection
StateMachineNodeDefinitions
} from './StateMachineNodes';

View File

@@ -0,0 +1,16 @@
/**
* @zh 状态机服务令牌
* @en State Machine Service Tokens
*/
import { createServiceToken } from '@esengine/ecs-framework';
import type { IStateMachine } from './IStateMachine';
/**
* @zh 状态机服务令牌
* @en State machine service token
*
* @zh 用于注入状态机服务
* @en Used for injecting state machine service
*/
export const StateMachineToken = createServiceToken<IStateMachine<string, unknown>>('stateMachine');

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,23 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
],
"references": [
{
"path": "../../framework/core"
},
{
"path": "../../framework/blueprint"
}
]
}

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
sourcemap: true,
clean: true,
external: [
'@esengine/ecs-framework',
'@esengine/blueprint'
],
tsconfig: 'tsconfig.build.json'
});