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,43 @@
{
"id": "blueprint",
"name": "@esengine/blueprint",
"displayName": "Blueprint",
"description": "Visual scripting system | 可视化脚本系统",
"version": "1.0.0",
"category": "AI",
"icon": "Workflow",
"tags": [
"visual",
"scripting",
"blueprint",
"nodes"
],
"isCore": false,
"defaultEnabled": false,
"isEngineModule": true,
"canContainContent": true,
"platforms": [
"web",
"desktop"
],
"dependencies": [
"core"
],
"exports": {
"components": [
"BlueprintComponent"
],
"systems": [
"BlueprintSystem"
],
"other": [
"Blueprint",
"BlueprintNode",
"BlueprintGraph"
]
},
"editorPackage": "@esengine/blueprint-editor",
"requiresWasm": false,
"outputPath": "dist/index.js",
"pluginExport": "BlueprintPlugin"
}

View File

@@ -0,0 +1,58 @@
{
"name": "@esengine/blueprint",
"version": "1.0.0",
"description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"clean": "rimraf dist tsconfig.tsbuildinfo",
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit"
},
"keywords": [
"ecs",
"blueprint",
"visual-scripting",
"game-engine",
"cocos",
"laya",
"esengine"
],
"author": "yhh",
"license": "MIT",
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/build-config": "workspace:*",
"@types/node": "^20.19.17",
"rimraf": "^5.0.0",
"tsup": "^8.0.0",
"typescript": "^5.8.3"
},
"peerDependencies": {
"@esengine/ecs-framework": "workspace:*"
},
"dependencies": {
"tslib": "^2.8.1"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/esengine/esengine.git",
"directory": "packages/framework/blueprint"
}
}

View File

@@ -0,0 +1,30 @@
{
"id": "@esengine/blueprint",
"name": "Blueprint System",
"version": "1.0.0",
"description": "Visual scripting system for creating game logic without code",
"category": "scripting",
"loadingPhase": "default",
"enabledByDefault": true,
"canContainContent": true,
"isEnginePlugin": false,
"modules": [
{
"name": "BlueprintRuntime",
"type": "runtime",
"entry": "./src/runtime.ts"
},
{
"name": "BlueprintEditor",
"type": "editor",
"entry": "./src/editor/index.ts"
}
],
"dependencies": [
{
"id": "@esengine/core",
"version": ">=1.0.0"
}
],
"icon": "Workflow"
}

View File

@@ -0,0 +1,575 @@
/**
* @zh 蓝图组合器接口和实现
* @en Blueprint Composer Interface and Implementation
*
* @zh 将多个蓝图片段组合成一个完整的蓝图
* @en Composes multiple blueprint fragments into a complete blueprint
*/
import type { BlueprintAsset, BlueprintVariable } from '../types/blueprint';
import type { BlueprintNode, BlueprintConnection } from '../types/nodes';
import type { IBlueprintFragment } from './BlueprintFragment';
// =============================================================================
// 槽位定义 | Slot Definition
// =============================================================================
/**
* @zh 片段槽位
* @en Fragment slot
*
* @zh 组合器中放置片段的位置
* @en A position in the composer where a fragment is placed
*/
export interface FragmentSlot {
/**
* @zh 槽位 ID
* @en Slot ID
*/
readonly id: string;
/**
* @zh 槽位名称
* @en Slot name
*/
readonly name: string;
/**
* @zh 放置的片段
* @en Placed fragment
*/
readonly fragment: IBlueprintFragment;
/**
* @zh 在组合图中的位置偏移
* @en Position offset in the composed graph
*/
readonly position: { x: number; y: number };
}
/**
* @zh 槽位间连接
* @en Connection between slots
*/
export interface SlotConnection {
/**
* @zh 连接 ID
* @en Connection ID
*/
readonly id: string;
/**
* @zh 源槽位 ID
* @en Source slot ID
*/
readonly fromSlotId: string;
/**
* @zh 源引脚名称
* @en Source pin name
*/
readonly fromPin: string;
/**
* @zh 目标槽位 ID
* @en Target slot ID
*/
readonly toSlotId: string;
/**
* @zh 目标引脚名称
* @en Target pin name
*/
readonly toPin: string;
}
// =============================================================================
// 组合器接口 | Composer Interface
// =============================================================================
/**
* @zh 蓝图组合器接口
* @en Blueprint composer interface
*
* @zh 用于将多个蓝图片段组合成一个完整蓝图
* @en Used to compose multiple blueprint fragments into a complete blueprint
*/
export interface IBlueprintComposer {
/**
* @zh 组合器名称
* @en Composer name
*/
readonly name: string;
/**
* @zh 获取所有槽位
* @en Get all slots
*/
getSlots(): FragmentSlot[];
/**
* @zh 获取所有连接
* @en Get all connections
*/
getConnections(): SlotConnection[];
/**
* @zh 添加片段到槽位
* @en Add fragment to slot
*
* @param fragment - @zh 蓝图片段 @en Blueprint fragment
* @param slotId - @zh 槽位 ID @en Slot ID
* @param options - @zh 选项 @en Options
*/
addFragment(
fragment: IBlueprintFragment,
slotId: string,
options?: {
name?: string;
position?: { x: number; y: number };
}
): void;
/**
* @zh 移除槽位
* @en Remove slot
*
* @param slotId - @zh 槽位 ID @en Slot ID
*/
removeSlot(slotId: string): void;
/**
* @zh 连接两个槽位的引脚
* @en Connect pins between two slots
*
* @param fromSlotId - @zh 源槽位 ID @en Source slot ID
* @param fromPin - @zh 源引脚名称 @en Source pin name
* @param toSlotId - @zh 目标槽位 ID @en Target slot ID
* @param toPin - @zh 目标引脚名称 @en Target pin name
*/
connect(
fromSlotId: string,
fromPin: string,
toSlotId: string,
toPin: string
): void;
/**
* @zh 断开连接
* @en Disconnect
*
* @param connectionId - @zh 连接 ID @en Connection ID
*/
disconnect(connectionId: string): void;
/**
* @zh 验证组合是否有效
* @en Validate if the composition is valid
*/
validate(): CompositionValidationResult;
/**
* @zh 编译成蓝图资产
* @en Compile into blueprint asset
*/
compile(): BlueprintAsset;
/**
* @zh 清空组合器
* @en Clear the composer
*/
clear(): void;
}
// =============================================================================
// 验证结果 | Validation Result
// =============================================================================
/**
* @zh 组合验证结果
* @en Composition validation result
*/
export interface CompositionValidationResult {
/**
* @zh 是否有效
* @en Whether valid
*/
readonly isValid: boolean;
/**
* @zh 错误列表
* @en Error list
*/
readonly errors: CompositionError[];
/**
* @zh 警告列表
* @en Warning list
*/
readonly warnings: CompositionWarning[];
}
/**
* @zh 组合错误
* @en Composition error
*/
export interface CompositionError {
readonly type: 'missing-connection' | 'type-mismatch' | 'cycle-detected' | 'invalid-slot';
readonly message: string;
readonly slotId?: string;
readonly pinName?: string;
}
/**
* @zh 组合警告
* @en Composition warning
*/
export interface CompositionWarning {
readonly type: 'unused-output' | 'unconnected-input';
readonly message: string;
readonly slotId?: string;
readonly pinName?: string;
}
// =============================================================================
// 组合器实现 | Composer Implementation
// =============================================================================
/**
* @zh 蓝图组合器实现
* @en Blueprint composer implementation
*/
export class BlueprintComposer implements IBlueprintComposer {
readonly name: string;
private slots: Map<string, FragmentSlot> = new Map();
private connections: Map<string, SlotConnection> = new Map();
private connectionIdCounter = 0;
constructor(name: string) {
this.name = name;
}
getSlots(): FragmentSlot[] {
return Array.from(this.slots.values());
}
getConnections(): SlotConnection[] {
return Array.from(this.connections.values());
}
addFragment(
fragment: IBlueprintFragment,
slotId: string,
options?: {
name?: string;
position?: { x: number; y: number };
}
): void {
if (this.slots.has(slotId)) {
throw new Error(`Slot '${slotId}' already exists`);
}
const slot: FragmentSlot = {
id: slotId,
name: options?.name ?? fragment.name,
fragment,
position: options?.position ?? { x: 0, y: 0 }
};
this.slots.set(slotId, slot);
}
removeSlot(slotId: string): void {
if (!this.slots.has(slotId)) {
return;
}
// Remove all connections involving this slot
const toRemove: string[] = [];
for (const [id, conn] of this.connections) {
if (conn.fromSlotId === slotId || conn.toSlotId === slotId) {
toRemove.push(id);
}
}
for (const id of toRemove) {
this.connections.delete(id);
}
this.slots.delete(slotId);
}
connect(
fromSlotId: string,
fromPin: string,
toSlotId: string,
toPin: string
): void {
const fromSlot = this.slots.get(fromSlotId);
const toSlot = this.slots.get(toSlotId);
if (!fromSlot) {
throw new Error(`Source slot '${fromSlotId}' not found`);
}
if (!toSlot) {
throw new Error(`Target slot '${toSlotId}' not found`);
}
const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === fromPin);
const toPinDef = toSlot.fragment.inputs.find(p => p.name === toPin);
if (!fromPinDef) {
throw new Error(`Output pin '${fromPin}' not found in slot '${fromSlotId}'`);
}
if (!toPinDef) {
throw new Error(`Input pin '${toPin}' not found in slot '${toSlotId}'`);
}
const connectionId = `conn_${++this.connectionIdCounter}`;
const connection: SlotConnection = {
id: connectionId,
fromSlotId,
fromPin,
toSlotId,
toPin
};
this.connections.set(connectionId, connection);
}
disconnect(connectionId: string): void {
this.connections.delete(connectionId);
}
validate(): CompositionValidationResult {
const errors: CompositionError[] = [];
const warnings: CompositionWarning[] = [];
// Check for required inputs without connections
for (const slot of this.slots.values()) {
for (const input of slot.fragment.inputs) {
const hasConnection = Array.from(this.connections.values()).some(
c => c.toSlotId === slot.id && c.toPin === input.name
);
if (!hasConnection && input.defaultValue === undefined) {
warnings.push({
type: 'unconnected-input',
message: `Input '${input.name}' in slot '${slot.id}' is not connected`,
slotId: slot.id,
pinName: input.name
});
}
}
// Check for unused outputs
for (const output of slot.fragment.outputs) {
const hasConnection = Array.from(this.connections.values()).some(
c => c.fromSlotId === slot.id && c.fromPin === output.name
);
if (!hasConnection) {
warnings.push({
type: 'unused-output',
message: `Output '${output.name}' in slot '${slot.id}' is not connected`,
slotId: slot.id,
pinName: output.name
});
}
}
}
// Check type compatibility
for (const conn of this.connections.values()) {
const fromSlot = this.slots.get(conn.fromSlotId);
const toSlot = this.slots.get(conn.toSlotId);
if (!fromSlot || !toSlot) {
errors.push({
type: 'invalid-slot',
message: `Invalid slot reference in connection '${conn.id}'`
});
continue;
}
const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === conn.fromPin);
const toPinDef = toSlot.fragment.inputs.find(p => p.name === conn.toPin);
if (fromPinDef && toPinDef && fromPinDef.type !== toPinDef.type) {
if (fromPinDef.type !== 'any' && toPinDef.type !== 'any') {
errors.push({
type: 'type-mismatch',
message: `Type mismatch: '${fromPinDef.type}' -> '${toPinDef.type}' in connection '${conn.id}'`
});
}
}
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
compile(): BlueprintAsset {
const nodes: BlueprintNode[] = [];
const connections: BlueprintConnection[] = [];
const variables: BlueprintVariable[] = [];
const nodeIdMap = new Map<string, Map<string, string>>();
// Copy nodes from each fragment with new IDs
let nodeIdCounter = 0;
for (const slot of this.slots.values()) {
const slotNodeMap = new Map<string, string>();
nodeIdMap.set(slot.id, slotNodeMap);
for (const node of slot.fragment.graph.nodes) {
const newNodeId = `node_${++nodeIdCounter}`;
slotNodeMap.set(node.id, newNodeId);
nodes.push({
...node,
id: newNodeId,
position: {
x: node.position.x + slot.position.x,
y: node.position.y + slot.position.y
}
});
}
// Copy internal connections
for (const conn of slot.fragment.graph.connections) {
const newFromId = slotNodeMap.get(conn.fromNodeId);
const newToId = slotNodeMap.get(conn.toNodeId);
if (newFromId && newToId) {
connections.push({
...conn,
id: `conn_internal_${connections.length}`,
fromNodeId: newFromId,
toNodeId: newToId
});
}
}
// Copy variables (with slot prefix to avoid conflicts)
for (const variable of slot.fragment.graph.variables) {
variables.push({
...variable,
name: `${slot.id}_${variable.name}`
});
}
}
// Create connections between slots based on exposed pins
for (const slotConn of this.connections.values()) {
const fromSlot = this.slots.get(slotConn.fromSlotId);
const toSlot = this.slots.get(slotConn.toSlotId);
if (!fromSlot || !toSlot) continue;
const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === slotConn.fromPin);
const toPinDef = toSlot.fragment.inputs.find(p => p.name === slotConn.toPin);
if (!fromPinDef || !toPinDef) continue;
const fromNodeMap = nodeIdMap.get(slotConn.fromSlotId);
const toNodeMap = nodeIdMap.get(slotConn.toSlotId);
if (!fromNodeMap || !toNodeMap) continue;
const fromNodeId = fromNodeMap.get(fromPinDef.internalNodeId);
const toNodeId = toNodeMap.get(toPinDef.internalNodeId);
if (fromNodeId && toNodeId) {
connections.push({
id: `conn_slot_${connections.length}`,
fromNodeId,
fromPin: fromPinDef.internalPinName,
toNodeId,
toPin: toPinDef.internalPinName
});
}
}
return {
version: 1,
type: 'blueprint',
metadata: {
name: this.name,
description: `Composed from ${this.slots.size} fragments`,
createdAt: Date.now(),
modifiedAt: Date.now()
},
variables,
nodes,
connections
};
}
clear(): void {
this.slots.clear();
this.connections.clear();
this.connectionIdCounter = 0;
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh 创建蓝图组合器
* @en Create blueprint composer
*/
export function createComposer(name: string): IBlueprintComposer {
return new BlueprintComposer(name);
}
// =============================================================================
// 组合资产格式 | Composition Asset Format
// =============================================================================
/**
* @zh 蓝图组合资产格式
* @en Blueprint composition asset format
*/
export interface BlueprintCompositionAsset {
/**
* @zh 格式版本
* @en Format version
*/
version: number;
/**
* @zh 资产类型标识
* @en Asset type identifier
*/
type: 'blueprint-composition';
/**
* @zh 组合名称
* @en Composition name
*/
name: string;
/**
* @zh 槽位数据
* @en Slot data
*/
slots: Array<{
id: string;
name: string;
fragmentId: string;
position: { x: number; y: number };
}>;
/**
* @zh 连接数据
* @en Connection data
*/
connections: SlotConnection[];
}

View File

@@ -0,0 +1,351 @@
/**
* @zh 蓝图片段接口和实现
* @en Blueprint Fragment Interface and Implementation
*
* @zh 定义可重用的蓝图片段,用于组合系统
* @en Defines reusable blueprint fragments for the composition system
*/
import type { BlueprintAsset } from '../types/blueprint';
import type { BlueprintPinType } from '../types/pins';
// =============================================================================
// 暴露引脚定义 | Exposed Pin Definition
// =============================================================================
/**
* @zh 暴露引脚定义
* @en Exposed pin definition
*
* @zh 片段对外暴露的引脚,可与其他片段连接
* @en Pins exposed by the fragment that can be connected to other fragments
*/
export interface ExposedPin {
/**
* @zh 引脚名称
* @en Pin name
*/
readonly name: string;
/**
* @zh 显示名称
* @en Display name
*/
readonly displayName: string;
/**
* @zh 引脚类型
* @en Pin type
*/
readonly type: BlueprintPinType;
/**
* @zh 引脚方向
* @en Pin direction
*/
readonly direction: 'input' | 'output';
/**
* @zh 描述
* @en Description
*/
readonly description?: string;
/**
* @zh 默认值(仅输入引脚)
* @en Default value (input pins only)
*/
readonly defaultValue?: unknown;
/**
* @zh 关联的内部节点 ID
* @en Associated internal node ID
*/
readonly internalNodeId: string;
/**
* @zh 关联的内部引脚名称
* @en Associated internal pin name
*/
readonly internalPinName: string;
}
// =============================================================================
// 蓝图片段接口 | Blueprint Fragment Interface
// =============================================================================
/**
* @zh 蓝图片段接口
* @en Blueprint fragment interface
*
* @zh 代表一个可重用的蓝图逻辑单元,如技能、卡牌效果等
* @en Represents a reusable unit of blueprint logic, such as skills, card effects, etc.
*/
export interface IBlueprintFragment {
/**
* @zh 片段唯一标识
* @en Fragment unique identifier
*/
readonly id: string;
/**
* @zh 片段名称
* @en Fragment name
*/
readonly name: string;
/**
* @zh 片段描述
* @en Fragment description
*/
readonly description?: string;
/**
* @zh 片段分类
* @en Fragment category
*/
readonly category?: string;
/**
* @zh 片段标签
* @en Fragment tags
*/
readonly tags?: string[];
/**
* @zh 暴露的输入引脚
* @en Exposed input pins
*/
readonly inputs: ExposedPin[];
/**
* @zh 暴露的输出引脚
* @en Exposed output pins
*/
readonly outputs: ExposedPin[];
/**
* @zh 内部蓝图图
* @en Internal blueprint graph
*/
readonly graph: BlueprintAsset;
/**
* @zh 片段版本
* @en Fragment version
*/
readonly version?: string;
/**
* @zh 图标名称
* @en Icon name
*/
readonly icon?: string;
/**
* @zh 颜色(用于可视化)
* @en Color (for visualization)
*/
readonly color?: string;
}
// =============================================================================
// 蓝图片段实现 | Blueprint Fragment Implementation
// =============================================================================
/**
* @zh 蓝图片段配置
* @en Blueprint fragment configuration
*/
export interface BlueprintFragmentConfig {
id: string;
name: string;
description?: string;
category?: string;
tags?: string[];
inputs?: ExposedPin[];
outputs?: ExposedPin[];
graph: BlueprintAsset;
version?: string;
icon?: string;
color?: string;
}
/**
* @zh 蓝图片段实现
* @en Blueprint fragment implementation
*/
export class BlueprintFragment implements IBlueprintFragment {
readonly id: string;
readonly name: string;
readonly description?: string;
readonly category?: string;
readonly tags?: string[];
readonly inputs: ExposedPin[];
readonly outputs: ExposedPin[];
readonly graph: BlueprintAsset;
readonly version?: string;
readonly icon?: string;
readonly color?: string;
constructor(config: BlueprintFragmentConfig) {
this.id = config.id;
this.name = config.name;
this.description = config.description;
this.category = config.category;
this.tags = config.tags;
this.inputs = config.inputs ?? [];
this.outputs = config.outputs ?? [];
this.graph = config.graph;
this.version = config.version;
this.icon = config.icon;
this.color = config.color;
}
/**
* @zh 获取所有暴露引脚
* @en Get all exposed pins
*/
getAllExposedPins(): ExposedPin[] {
return [...this.inputs, ...this.outputs];
}
/**
* @zh 通过名称查找输入引脚
* @en Find input pin by name
*/
findInput(name: string): ExposedPin | undefined {
return this.inputs.find(p => p.name === name);
}
/**
* @zh 通过名称查找输出引脚
* @en Find output pin by name
*/
findOutput(name: string): ExposedPin | undefined {
return this.outputs.find(p => p.name === name);
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh 创建暴露引脚
* @en Create exposed pin
*/
export function createExposedPin(
name: string,
type: BlueprintPinType,
direction: 'input' | 'output',
internalNodeId: string,
internalPinName: string,
options?: {
displayName?: string;
description?: string;
defaultValue?: unknown;
}
): ExposedPin {
return {
name,
displayName: options?.displayName ?? name,
type,
direction,
description: options?.description,
defaultValue: options?.defaultValue,
internalNodeId,
internalPinName
};
}
/**
* @zh 创建蓝图片段
* @en Create blueprint fragment
*/
export function createFragment(config: BlueprintFragmentConfig): IBlueprintFragment {
return new BlueprintFragment(config);
}
// =============================================================================
// 片段资产格式 | Fragment Asset Format
// =============================================================================
/**
* @zh 蓝图片段资产格式
* @en Blueprint fragment asset format
*
* @zh 用于序列化和反序列化片段
* @en Used for serializing and deserializing fragments
*/
export interface BlueprintFragmentAsset {
/**
* @zh 格式版本
* @en Format version
*/
version: number;
/**
* @zh 资产类型标识
* @en Asset type identifier
*/
type: 'blueprint-fragment';
/**
* @zh 片段数据
* @en Fragment data
*/
fragment: {
id: string;
name: string;
description?: string;
category?: string;
tags?: string[];
inputs: ExposedPin[];
outputs: ExposedPin[];
version?: string;
icon?: string;
color?: string;
};
/**
* @zh 内部蓝图图
* @en Internal blueprint graph
*/
graph: BlueprintAsset;
}
/**
* @zh 从资产创建片段
* @en Create fragment from asset
*/
export function fragmentFromAsset(asset: BlueprintFragmentAsset): IBlueprintFragment {
return new BlueprintFragment({
...asset.fragment,
graph: asset.graph
});
}
/**
* @zh 将片段转为资产
* @en Convert fragment to asset
*/
export function fragmentToAsset(fragment: IBlueprintFragment): BlueprintFragmentAsset {
return {
version: 1,
type: 'blueprint-fragment',
fragment: {
id: fragment.id,
name: fragment.name,
description: fragment.description,
category: fragment.category,
tags: fragment.tags,
inputs: fragment.inputs,
outputs: fragment.outputs,
version: fragment.version,
icon: fragment.icon,
color: fragment.color
},
graph: fragment.graph
};
}

View File

@@ -0,0 +1,208 @@
/**
* @zh 片段注册表
* @en Fragment Registry
*
* @zh 管理和查询蓝图片段
* @en Manages and queries blueprint fragments
*/
import type { IBlueprintFragment } from './BlueprintFragment';
// =============================================================================
// 片段注册表接口 | Fragment Registry Interface
// =============================================================================
/**
* @zh 片段过滤器
* @en Fragment filter
*/
export interface FragmentFilter {
/**
* @zh 按分类过滤
* @en Filter by category
*/
category?: string;
/**
* @zh 按标签过滤(任意匹配)
* @en Filter by tags (any match)
*/
tags?: string[];
/**
* @zh 按名称搜索
* @en Search by name
*/
search?: string;
}
/**
* @zh 片段注册表接口
* @en Fragment registry interface
*/
export interface IFragmentRegistry {
/**
* @zh 注册片段
* @en Register fragment
*/
register(fragment: IBlueprintFragment): void;
/**
* @zh 注销片段
* @en Unregister fragment
*/
unregister(id: string): void;
/**
* @zh 获取片段
* @en Get fragment
*/
get(id: string): IBlueprintFragment | undefined;
/**
* @zh 检查片段是否存在
* @en Check if fragment exists
*/
has(id: string): boolean;
/**
* @zh 获取所有片段
* @en Get all fragments
*/
getAll(): IBlueprintFragment[];
/**
* @zh 按条件过滤片段
* @en Filter fragments by criteria
*/
filter(filter: FragmentFilter): IBlueprintFragment[];
/**
* @zh 获取所有分类
* @en Get all categories
*/
getCategories(): string[];
/**
* @zh 获取所有标签
* @en Get all tags
*/
getTags(): string[];
/**
* @zh 清空注册表
* @en Clear registry
*/
clear(): void;
}
// =============================================================================
// 片段注册表实现 | Fragment Registry Implementation
// =============================================================================
/**
* @zh 片段注册表实现
* @en Fragment registry implementation
*/
export class FragmentRegistry implements IFragmentRegistry {
private fragments: Map<string, IBlueprintFragment> = new Map();
register(fragment: IBlueprintFragment): void {
if (this.fragments.has(fragment.id)) {
console.warn(`Fragment '${fragment.id}' already registered, overwriting`);
}
this.fragments.set(fragment.id, fragment);
}
unregister(id: string): void {
this.fragments.delete(id);
}
get(id: string): IBlueprintFragment | undefined {
return this.fragments.get(id);
}
has(id: string): boolean {
return this.fragments.has(id);
}
getAll(): IBlueprintFragment[] {
return Array.from(this.fragments.values());
}
filter(filter: FragmentFilter): IBlueprintFragment[] {
let results = this.getAll();
if (filter.category) {
results = results.filter(f => f.category === filter.category);
}
if (filter.tags && filter.tags.length > 0) {
results = results.filter(f =>
f.tags && filter.tags!.some(t => f.tags!.includes(t))
);
}
if (filter.search) {
const searchLower = filter.search.toLowerCase();
results = results.filter(f =>
f.name.toLowerCase().includes(searchLower) ||
f.description?.toLowerCase().includes(searchLower)
);
}
return results;
}
getCategories(): string[] {
const categories = new Set<string>();
for (const fragment of this.fragments.values()) {
if (fragment.category) {
categories.add(fragment.category);
}
}
return Array.from(categories).sort();
}
getTags(): string[] {
const tags = new Set<string>();
for (const fragment of this.fragments.values()) {
if (fragment.tags) {
for (const tag of fragment.tags) {
tags.add(tag);
}
}
}
return Array.from(tags).sort();
}
clear(): void {
this.fragments.clear();
}
/**
* @zh 获取片段数量
* @en Get fragment count
*/
get size(): number {
return this.fragments.size;
}
}
// =============================================================================
// 单例实例 | Singleton Instance
// =============================================================================
/**
* @zh 默认片段注册表实例
* @en Default fragment registry instance
*/
export const defaultFragmentRegistry = new FragmentRegistry();
/**
* @zh 创建片段注册表
* @en Create fragment registry
*/
export function createFragmentRegistry(): IFragmentRegistry {
return new FragmentRegistry();
}

View File

@@ -0,0 +1,57 @@
/**
* @zh 蓝图组合系统导出
* @en Blueprint Composition System Export
*/
// =============================================================================
// 片段 | Fragment
// =============================================================================
export type {
ExposedPin,
IBlueprintFragment,
BlueprintFragmentConfig,
BlueprintFragmentAsset
} from './BlueprintFragment';
export {
BlueprintFragment,
createExposedPin,
createFragment,
fragmentFromAsset,
fragmentToAsset
} from './BlueprintFragment';
// =============================================================================
// 组合器 | Composer
// =============================================================================
export type {
FragmentSlot,
SlotConnection,
IBlueprintComposer,
CompositionValidationResult,
CompositionError,
CompositionWarning,
BlueprintCompositionAsset
} from './BlueprintComposer';
export {
BlueprintComposer,
createComposer
} from './BlueprintComposer';
// =============================================================================
// 注册表 | Registry
// =============================================================================
export type {
FragmentFilter,
IFragmentRegistry
} from './FragmentRegistry';
export {
FragmentRegistry,
defaultFragmentRegistry,
createFragmentRegistry
} from './FragmentRegistry';

View File

@@ -0,0 +1,67 @@
/**
* @zh ESEngine 蓝图插件
* @en ESEngine Blueprint Plugin
*
* @zh 此文件包含与 ESEngine 引擎核心集成的代码。
* 使用 Cocos/Laya 等其他引擎时不需要此文件。
*
* @en This file contains code for integrating with ESEngine engine-core.
* Not needed when using other engines like Cocos/Laya.
*/
import type { IRuntimePlugin, ModuleManifest, IRuntimeModule } from '@esengine/engine-core';
/**
* @zh 蓝图运行时模块
* @en Blueprint Runtime Module
*
* @zh 注意:蓝图使用自定义系统 (IBlueprintSystem) 而非 EntitySystem
* 因此这里不实现 createSystems。蓝图系统应使用 createBlueprintSystem(scene) 手动创建。
*
* @en Note: Blueprint uses a custom system (IBlueprintSystem) instead of EntitySystem,
* so createSystems is not implemented here. Blueprint systems should be created
* manually using createBlueprintSystem(scene).
*/
class BlueprintRuntimeModule implements IRuntimeModule {
async onInitialize(): Promise<void> {
// Blueprint system initialization
}
onDestroy(): void {
// Cleanup
}
}
/**
* @zh 蓝图的插件清单
* @en Plugin manifest for Blueprint
*/
const manifest: ModuleManifest = {
id: 'blueprint',
name: '@esengine/blueprint',
displayName: 'Blueprint',
version: '1.0.0',
description: '可视化脚本系统',
category: 'AI',
icon: 'Workflow',
isCore: false,
defaultEnabled: false,
isEngineModule: true,
dependencies: ['core'],
exports: {
components: ['BlueprintComponent'],
systems: ['BlueprintSystem']
},
requiresWasm: false
};
/**
* @zh 蓝图插件
* @en Blueprint Plugin
*/
export const BlueprintPlugin: IRuntimePlugin = {
manifest,
runtimeModule: new BlueprintRuntimeModule()
};
export { BlueprintRuntimeModule };

View File

@@ -0,0 +1,37 @@
/**
* @zh ESEngine 集成入口
* @en ESEngine integration entry point
*
* @zh 此模块包含与 ESEngine 引擎核心集成所需的所有代码。
* 使用 Cocos/Laya 等其他引擎时,只需导入主模块即可。
*
* @en This module contains all code required for ESEngine engine-core integration.
* When using other engines like Cocos/Laya, just import the main module.
*
* @example ESEngine 使用方式 / ESEngine usage:
* ```typescript
* import { BlueprintPlugin } from '@esengine/blueprint/esengine';
*
* // Register with ESEngine plugin system
* engine.registerPlugin(BlueprintPlugin);
* ```
*
* @example Cocos/Laya 使用方式 / Cocos/Laya usage:
* ```typescript
* import {
* createBlueprintSystem,
* createBlueprintComponentData
* } from '@esengine/blueprint';
*
* // Create blueprint system for your scene
* const blueprintSystem = createBlueprintSystem(scene);
*
* // Add to your game loop
* function update(dt) {
* blueprintSystem.process(blueprintEntities, dt);
* }
* ```
*/
// Runtime module and plugin
export { BlueprintPlugin, BlueprintRuntimeModule } from './BlueprintPlugin';

View File

@@ -0,0 +1,67 @@
/**
* @esengine/blueprint - Visual scripting system for ECS Framework
*
* @zh 蓝图可视化脚本系统 - 可与任何 ECS 框架配合使用
* @en Visual scripting system - works with any ECS framework
*
* @zh 此包是通用的可视化脚本实现,可以与任何 ECS 框架配合使用。
* 对于 ESEngine 集成,请从 '@esengine/blueprint/esengine' 导入插件。
*
* @en This package is a generic visual scripting implementation that works with any ECS framework.
* For ESEngine integration, import the plugin from '@esengine/blueprint/esengine'.
*
* @example Cocos/Laya/通用 ECS 使用方式:
* ```typescript
* import {
* createBlueprintSystem,
* createBlueprintComponentData
* } from '@esengine/blueprint';
*
* // Create blueprint system for your scene
* const blueprintSystem = createBlueprintSystem(scene);
*
* // Create component data
* const componentData = createBlueprintComponentData();
* componentData.blueprintAsset = loadedAsset;
*
* // Add to your game loop
* function update(dt) {
* blueprintSystem.process(blueprintEntities, dt);
* }
* ```
*
* @packageDocumentation
*/
// Types
export * from './types';
// Runtime
export * from './runtime';
// Triggers
export * from './triggers';
// Composition
export * from './composition';
// Nodes (import to register)
import './nodes';
// Re-export commonly used items
export { NodeRegistry, RegisterNode } from './runtime/NodeRegistry';
export { BlueprintVM } from './runtime/BlueprintVM';
export {
createBlueprintComponentData,
initializeBlueprintVM,
startBlueprint,
stopBlueprint,
tickBlueprint,
cleanupBlueprint
} from './runtime/BlueprintComponent';
export {
createBlueprintSystem,
triggerBlueprintEvent,
triggerCustomBlueprintEvent
} from './runtime/BlueprintSystem';
export { createEmptyBlueprint, validateBlueprintAsset } from './types/blueprint';

View File

@@ -0,0 +1,91 @@
/**
* Print Node - Outputs a message for debugging
* 打印节点 - 输出调试消息
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* Print node template
* Print 节点模板
*/
export const PrintTemplate: BlueprintNodeTemplate = {
type: 'Print',
title: 'Print String',
category: 'debug',
color: '#785EF0',
description: 'Prints a message to the console for debugging (打印消息到控制台用于调试)',
keywords: ['log', 'debug', 'console', 'output', 'print'],
inputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'message',
type: 'string',
displayName: 'Message',
defaultValue: 'Hello Blueprint!'
},
{
name: 'printToScreen',
type: 'bool',
displayName: 'Print to Screen',
defaultValue: true
},
{
name: 'duration',
type: 'float',
displayName: 'Duration',
defaultValue: 2.0
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
}
]
};
/**
* Print node executor
* Print 节点执行器
*/
@RegisterNode(PrintTemplate)
export class PrintExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const message = context.evaluateInput(node.id, 'message', 'Hello Blueprint!');
const printToScreen = context.evaluateInput(node.id, 'printToScreen', true);
const duration = context.evaluateInput(node.id, 'duration', 2.0);
// Console output
// 控制台输出
console.log(`[Blueprint] ${message}`);
// Screen output via event (handled by runtime)
// 通过事件输出到屏幕(由运行时处理)
if (printToScreen) {
const event = new CustomEvent('blueprint:print', {
detail: {
message: String(message),
duration: Number(duration),
entityId: context.entity.id,
entityName: context.entity.name
}
});
if (typeof window !== 'undefined') {
window.dispatchEvent(event);
}
}
return {
nextExec: 'exec'
};
}
}

View File

@@ -0,0 +1,6 @@
/**
* Debug Nodes - Tools for debugging blueprints
* 调试节点 - 蓝图调试工具
*/
export * from './Print';

View File

@@ -0,0 +1,44 @@
/**
* Event Begin Play Node - Triggered when the blueprint starts
* 开始播放事件节点 - 蓝图启动时触发
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* EventBeginPlay node template
* EventBeginPlay 节点模板
*/
export const EventBeginPlayTemplate: BlueprintNodeTemplate = {
type: 'EventBeginPlay',
title: 'Event Begin Play',
category: 'event',
color: '#CC0000',
description: 'Triggered once when the blueprint starts executing (蓝图开始执行时触发一次)',
keywords: ['start', 'begin', 'init', 'event'],
inputs: [],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
}
]
};
/**
* EventBeginPlay node executor
* EventBeginPlay 节点执行器
*/
@RegisterNode(EventBeginPlayTemplate)
export class EventBeginPlayExecutor implements INodeExecutor {
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
// Event nodes just trigger execution flow
// 事件节点只触发执行流
return {
nextExec: 'exec'
};
}
}

View File

@@ -0,0 +1,118 @@
/**
* @zh 碰撞事件节点 - 碰撞发生时触发
* @en Event Collision Node - Triggered on collision events
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventCollisionEnter 节点模板
* @en EventCollisionEnter node template
*/
export const EventCollisionEnterTemplate: BlueprintNodeTemplate = {
type: 'EventCollisionEnter',
title: 'Event Collision Enter',
category: 'event',
color: '#CC0000',
description: 'Triggered when collision starts / 碰撞开始时触发',
keywords: ['collision', 'enter', 'hit', 'overlap', 'event'],
menuPath: ['Event', 'Collision', 'Enter'],
inputs: [],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'otherEntityId',
type: 'string',
displayName: 'Other Entity'
},
{
name: 'pointX',
type: 'float',
displayName: 'Point X'
},
{
name: 'pointY',
type: 'float',
displayName: 'Point Y'
},
{
name: 'normalX',
type: 'float',
displayName: 'Normal X'
},
{
name: 'normalY',
type: 'float',
displayName: 'Normal Y'
}
]
};
/**
* @zh EventCollisionEnter 节点执行器
* @en EventCollisionEnter node executor
*/
@RegisterNode(EventCollisionEnterTemplate)
export class EventCollisionEnterExecutor implements INodeExecutor {
execute(_node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
otherEntityId: '',
pointX: 0,
pointY: 0,
normalX: 0,
normalY: 0
}
};
}
}
/**
* @zh EventCollisionExit 节点模板
* @en EventCollisionExit node template
*/
export const EventCollisionExitTemplate: BlueprintNodeTemplate = {
type: 'EventCollisionExit',
title: 'Event Collision Exit',
category: 'event',
color: '#CC0000',
description: 'Triggered when collision ends / 碰撞结束时触发',
keywords: ['collision', 'exit', 'end', 'separate', 'event'],
menuPath: ['Event', 'Collision', 'Exit'],
inputs: [],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'otherEntityId',
type: 'string',
displayName: 'Other Entity'
}
]
};
/**
* @zh EventCollisionExit 节点执行器
* @en EventCollisionExit node executor
*/
@RegisterNode(EventCollisionExitTemplate)
export class EventCollisionExitExecutor implements INodeExecutor {
execute(_node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
otherEntityId: ''
}
};
}
}

View File

@@ -0,0 +1,42 @@
/**
* Event End Play Node - Triggered when the blueprint stops
* 结束播放事件节点 - 蓝图停止时触发
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* EventEndPlay node template
* EventEndPlay 节点模板
*/
export const EventEndPlayTemplate: BlueprintNodeTemplate = {
type: 'EventEndPlay',
title: 'Event End Play',
category: 'event',
color: '#CC0000',
description: 'Triggered once when the blueprint stops executing (蓝图停止执行时触发一次)',
keywords: ['stop', 'end', 'destroy', 'event'],
inputs: [],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
}
]
};
/**
* EventEndPlay node executor
* EventEndPlay 节点执行器
*/
@RegisterNode(EventEndPlayTemplate)
export class EventEndPlayExecutor implements INodeExecutor {
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
return {
nextExec: 'exec'
};
}
}

View File

@@ -0,0 +1,79 @@
/**
* @zh 输入事件节点 - 输入触发时触发
* @en Event Input Node - Triggered on input events
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventInput 节点模板
* @en EventInput node template
*/
export const EventInputTemplate: BlueprintNodeTemplate = {
type: 'EventInput',
title: 'Event Input',
category: 'event',
color: '#CC0000',
description: 'Triggered when input action occurs / 输入动作发生时触发',
keywords: ['input', 'key', 'button', 'action', 'event'],
menuPath: ['Event', 'Input'],
inputs: [
{
name: 'action',
type: 'string',
displayName: 'Action',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'action',
type: 'string',
displayName: 'Action'
},
{
name: 'value',
type: 'float',
displayName: 'Value'
},
{
name: 'pressed',
type: 'bool',
displayName: 'Pressed'
},
{
name: 'released',
type: 'bool',
displayName: 'Released'
}
]
};
/**
* @zh EventInput 节点执行器
* @en EventInput node executor
*
* @zh 注意:事件节点的输出由 VM 在触发时通过 setOutputs 设置
* @en Note: Event node outputs are set by VM via setOutputs when triggered
*/
@RegisterNode(EventInputTemplate)
export class EventInputExecutor implements INodeExecutor {
execute(node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
action: node.data?.action ?? '',
value: 0,
pressed: false,
released: false
}
};
}
}

View File

@@ -0,0 +1,70 @@
/**
* @zh 消息事件节点 - 接收消息时触发
* @en Event Message Node - Triggered when message is received
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventMessage 节点模板
* @en EventMessage node template
*/
export const EventMessageTemplate: BlueprintNodeTemplate = {
type: 'EventMessage',
title: 'Event Message',
category: 'event',
color: '#CC0000',
description: 'Triggered when a message is received / 接收到消息时触发',
keywords: ['message', 'receive', 'broadcast', 'event', 'signal'],
menuPath: ['Event', 'Message'],
inputs: [
{
name: 'messageName',
type: 'string',
displayName: 'Message Name',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'messageName',
type: 'string',
displayName: 'Message'
},
{
name: 'senderId',
type: 'string',
displayName: 'Sender ID'
},
{
name: 'payload',
type: 'any',
displayName: 'Payload'
}
]
};
/**
* @zh EventMessage 节点执行器
* @en EventMessage node executor
*/
@RegisterNode(EventMessageTemplate)
export class EventMessageExecutor implements INodeExecutor {
execute(node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
messageName: node.data?.messageName ?? '',
senderId: '',
payload: null
}
};
}
}

View File

@@ -0,0 +1,132 @@
/**
* @zh 状态事件节点 - 状态机状态变化时触发
* @en Event State Node - Triggered on state machine state changes
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventStateEnter 节点模板
* @en EventStateEnter node template
*/
export const EventStateEnterTemplate: BlueprintNodeTemplate = {
type: 'EventStateEnter',
title: 'Event State Enter',
category: 'event',
color: '#CC0000',
description: 'Triggered when entering a state / 进入状态时触发',
keywords: ['state', 'enter', 'fsm', 'machine', 'event'],
menuPath: ['Event', 'State', 'Enter'],
inputs: [
{
name: 'stateName',
type: 'string',
displayName: 'State Name',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'stateMachineId',
type: 'string',
displayName: 'State Machine'
},
{
name: 'currentState',
type: 'string',
displayName: 'Current State'
},
{
name: 'previousState',
type: 'string',
displayName: 'Previous State'
}
]
};
/**
* @zh EventStateEnter 节点执行器
* @en EventStateEnter node executor
*/
@RegisterNode(EventStateEnterTemplate)
export class EventStateEnterExecutor implements INodeExecutor {
execute(node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
stateMachineId: '',
currentState: node.data?.stateName ?? '',
previousState: ''
}
};
}
}
/**
* @zh EventStateExit 节点模板
* @en EventStateExit node template
*/
export const EventStateExitTemplate: BlueprintNodeTemplate = {
type: 'EventStateExit',
title: 'Event State Exit',
category: 'event',
color: '#CC0000',
description: 'Triggered when exiting a state / 退出状态时触发',
keywords: ['state', 'exit', 'leave', 'fsm', 'machine', 'event'],
menuPath: ['Event', 'State', 'Exit'],
inputs: [
{
name: 'stateName',
type: 'string',
displayName: 'State Name',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'stateMachineId',
type: 'string',
displayName: 'State Machine'
},
{
name: 'currentState',
type: 'string',
displayName: 'Current State'
},
{
name: 'previousState',
type: 'string',
displayName: 'Previous State'
}
]
};
/**
* @zh EventStateExit 节点执行器
* @en EventStateExit node executor
*/
@RegisterNode(EventStateExitTemplate)
export class EventStateExitExecutor implements INodeExecutor {
execute(node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
stateMachineId: '',
currentState: '',
previousState: node.data?.stateName ?? ''
}
};
}
}

View File

@@ -0,0 +1,50 @@
/**
* Event Tick Node - Triggered every frame
* 每帧事件节点 - 每帧触发
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* EventTick node template
* EventTick 节点模板
*/
export const EventTickTemplate: BlueprintNodeTemplate = {
type: 'EventTick',
title: 'Event Tick',
category: 'event',
color: '#CC0000',
description: 'Triggered every frame during execution (执行期间每帧触发)',
keywords: ['update', 'frame', 'tick', 'event'],
inputs: [],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'deltaTime',
type: 'float',
displayName: 'Delta Seconds'
}
]
};
/**
* EventTick node executor
* EventTick 节点执行器
*/
@RegisterNode(EventTickTemplate)
export class EventTickExecutor implements INodeExecutor {
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
deltaTime: context.deltaTime
}
};
}
}

View File

@@ -0,0 +1,70 @@
/**
* @zh 定时器事件节点 - 定时器触发时调用
* @en Event Timer Node - Triggered when timer fires
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventTimer 节点模板
* @en EventTimer node template
*/
export const EventTimerTemplate: BlueprintNodeTemplate = {
type: 'EventTimer',
title: 'Event Timer',
category: 'event',
color: '#CC0000',
description: 'Triggered when a timer fires / 定时器触发时执行',
keywords: ['timer', 'delay', 'schedule', 'event', 'interval'],
menuPath: ['Event', 'Timer'],
inputs: [
{
name: 'timerId',
type: 'string',
displayName: 'Timer ID',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'timerId',
type: 'string',
displayName: 'Timer ID'
},
{
name: 'isRepeating',
type: 'bool',
displayName: 'Is Repeating'
},
{
name: 'timesFired',
type: 'int',
displayName: 'Times Fired'
}
]
};
/**
* @zh EventTimer 节点执行器
* @en EventTimer node executor
*/
@RegisterNode(EventTimerTemplate)
export class EventTimerExecutor implements INodeExecutor {
execute(node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
timerId: node.data?.timerId ?? '',
isRepeating: false,
timesFired: 0
}
};
}
}

View File

@@ -0,0 +1,16 @@
/**
* @zh 事件节点 - 蓝图执行的入口点
* @en Event Nodes - Entry points for blueprint execution
*/
// 生命周期事件 | Lifecycle events
export * from './EventBeginPlay';
export * from './EventTick';
export * from './EventEndPlay';
// 触发器事件 | Trigger events
export * from './EventInput';
export * from './EventCollision';
export * from './EventMessage';
export * from './EventTimer';
export * from './EventState';

View File

@@ -0,0 +1,11 @@
/**
* Blueprint Nodes - All node definitions and executors
* 蓝图节点 - 所有节点定义和执行器
*/
// Import all nodes to trigger registration
// 导入所有节点以触发注册
export * from './events';
export * from './debug';
export * from './time';
export * from './math';

View File

@@ -0,0 +1,122 @@
/**
* Math Operation Nodes - Basic arithmetic operations
* 数学运算节点 - 基础算术运算
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
// Add Node (加法节点)
export const AddTemplate: BlueprintNodeTemplate = {
type: 'Add',
title: 'Add',
category: 'math',
color: '#4CAF50',
description: 'Adds two numbers together (将两个数字相加)',
keywords: ['add', 'plus', 'sum', '+', 'math'],
isPure: true,
inputs: [
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
],
outputs: [
{ name: 'result', type: 'float', displayName: 'Result' }
]
};
@RegisterNode(AddTemplate)
export class AddExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const a = Number(context.evaluateInput(node.id, 'a', 0));
const b = Number(context.evaluateInput(node.id, 'b', 0));
return { outputs: { result: a + b } };
}
}
// Subtract Node (减法节点)
export const SubtractTemplate: BlueprintNodeTemplate = {
type: 'Subtract',
title: 'Subtract',
category: 'math',
color: '#4CAF50',
description: 'Subtracts B from A (从 A 减去 B)',
keywords: ['subtract', 'minus', '-', 'math'],
isPure: true,
inputs: [
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
],
outputs: [
{ name: 'result', type: 'float', displayName: 'Result' }
]
};
@RegisterNode(SubtractTemplate)
export class SubtractExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const a = Number(context.evaluateInput(node.id, 'a', 0));
const b = Number(context.evaluateInput(node.id, 'b', 0));
return { outputs: { result: a - b } };
}
}
// Multiply Node (乘法节点)
export const MultiplyTemplate: BlueprintNodeTemplate = {
type: 'Multiply',
title: 'Multiply',
category: 'math',
color: '#4CAF50',
description: 'Multiplies two numbers (将两个数字相乘)',
keywords: ['multiply', 'times', '*', 'math'],
isPure: true,
inputs: [
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 1 }
],
outputs: [
{ name: 'result', type: 'float', displayName: 'Result' }
]
};
@RegisterNode(MultiplyTemplate)
export class MultiplyExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const a = Number(context.evaluateInput(node.id, 'a', 0));
const b = Number(context.evaluateInput(node.id, 'b', 1));
return { outputs: { result: a * b } };
}
}
// Divide Node (除法节点)
export const DivideTemplate: BlueprintNodeTemplate = {
type: 'Divide',
title: 'Divide',
category: 'math',
color: '#4CAF50',
description: 'Divides A by B (A 除以 B)',
keywords: ['divide', '/', 'math'],
isPure: true,
inputs: [
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 1 }
],
outputs: [
{ name: 'result', type: 'float', displayName: 'Result' }
]
};
@RegisterNode(DivideTemplate)
export class DivideExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const a = Number(context.evaluateInput(node.id, 'a', 0));
const b = Number(context.evaluateInput(node.id, 'b', 1));
// Prevent division by zero (防止除零)
if (b === 0) {
return { outputs: { result: 0 } };
}
return { outputs: { result: a / b } };
}
}

View File

@@ -0,0 +1,6 @@
/**
* Math Nodes - Mathematical operation nodes
* 数学节点 - 数学运算节点
*/
export * from './MathOperations';

View File

@@ -0,0 +1,57 @@
/**
* Delay Node - Pauses execution for a specified duration
* 延迟节点 - 暂停执行指定的时长
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* Delay node template
* Delay 节点模板
*/
export const DelayTemplate: BlueprintNodeTemplate = {
type: 'Delay',
title: 'Delay',
category: 'flow',
color: '#FFFFFF',
description: 'Pauses execution for a specified number of seconds (暂停执行指定的秒数)',
keywords: ['wait', 'delay', 'pause', 'sleep', 'timer'],
inputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'duration',
type: 'float',
displayName: 'Duration',
defaultValue: 1.0
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: 'Completed'
}
]
};
/**
* Delay node executor
* Delay 节点执行器
*/
@RegisterNode(DelayTemplate)
export class DelayExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const duration = context.evaluateInput(node.id, 'duration', 1.0) as number;
return {
nextExec: 'exec',
delay: duration
};
}
}

View File

@@ -0,0 +1,45 @@
/**
* Get Delta Time Node - Returns the time since last frame
* 获取增量时间节点 - 返回上一帧以来的时间
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* GetDeltaTime node template
* GetDeltaTime 节点模板
*/
export const GetDeltaTimeTemplate: BlueprintNodeTemplate = {
type: 'GetDeltaTime',
title: 'Get Delta Time',
category: 'time',
color: '#4FC3F7',
description: 'Returns the time elapsed since the last frame in seconds (返回上一帧以来经过的时间,单位秒)',
keywords: ['delta', 'time', 'frame', 'dt'],
isPure: true,
inputs: [],
outputs: [
{
name: 'deltaTime',
type: 'float',
displayName: 'Delta Seconds'
}
]
};
/**
* GetDeltaTime node executor
* GetDeltaTime 节点执行器
*/
@RegisterNode(GetDeltaTimeTemplate)
export class GetDeltaTimeExecutor implements INodeExecutor {
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
return {
outputs: {
deltaTime: context.deltaTime
}
};
}
}

View File

@@ -0,0 +1,45 @@
/**
* Get Time Node - Returns the total time since blueprint started
* 获取时间节点 - 返回蓝图启动以来的总时间
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* GetTime node template
* GetTime 节点模板
*/
export const GetTimeTemplate: BlueprintNodeTemplate = {
type: 'GetTime',
title: 'Get Game Time',
category: 'time',
color: '#4FC3F7',
description: 'Returns the total time since the blueprint started in seconds (返回蓝图启动以来的总时间,单位秒)',
keywords: ['time', 'total', 'elapsed', 'game'],
isPure: true,
inputs: [],
outputs: [
{
name: 'time',
type: 'float',
displayName: 'Seconds'
}
]
};
/**
* GetTime node executor
* GetTime 节点执行器
*/
@RegisterNode(GetTimeTemplate)
export class GetTimeExecutor implements INodeExecutor {
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
return {
outputs: {
time: context.time
}
};
}
}

View File

@@ -0,0 +1,8 @@
/**
* Time Nodes - Time-related utility nodes
* 时间节点 - 时间相关的工具节点
*/
export * from './GetDeltaTime';
export * from './GetTime';
export * from './Delay';

View File

@@ -0,0 +1,116 @@
/**
* Blueprint Component - Attaches a blueprint to an entity
* 蓝图组件 - 将蓝图附加到实体
*/
import type { Entity, IScene } from '@esengine/ecs-framework';
import { BlueprintAsset } from '../types/blueprint';
import { BlueprintVM } from './BlueprintVM';
/**
* Component interface for ECS integration
* 用于 ECS 集成的组件接口
*/
export interface IBlueprintComponent {
/** Entity ID this component belongs to (此组件所属的实体ID) */
entityId: number | null;
/** Blueprint asset reference (蓝图资产引用) */
blueprintAsset: BlueprintAsset | null;
/** Blueprint asset path for serialization (用于序列化的蓝图资产路径) */
blueprintPath: string;
/** Auto-start execution when entity is created (实体创建时自动开始执行) */
autoStart: boolean;
/** Enable debug mode for VM (启用 VM 调试模式) */
debug: boolean;
/** Runtime VM instance (运行时 VM 实例) */
vm: BlueprintVM | null;
/** Whether the blueprint has started (蓝图是否已启动) */
isStarted: boolean;
}
/**
* Creates a blueprint component data object
* 创建蓝图组件数据对象
*/
export function createBlueprintComponentData(): IBlueprintComponent {
return {
entityId: null,
blueprintAsset: null,
blueprintPath: '',
autoStart: true,
debug: false,
vm: null,
isStarted: false
};
}
/**
* Initialize the VM for a blueprint component
* 为蓝图组件初始化 VM
*/
export function initializeBlueprintVM(
component: IBlueprintComponent,
entity: Entity,
scene: IScene
): void {
if (!component.blueprintAsset) {
return;
}
// Create VM instance
// 创建 VM 实例
component.vm = new BlueprintVM(component.blueprintAsset, entity, scene);
component.vm.debug = component.debug;
}
/**
* Start blueprint execution
* 开始蓝图执行
*/
export function startBlueprint(component: IBlueprintComponent): void {
if (component.vm && !component.isStarted) {
component.vm.start();
component.isStarted = true;
}
}
/**
* Stop blueprint execution
* 停止蓝图执行
*/
export function stopBlueprint(component: IBlueprintComponent): void {
if (component.vm && component.isStarted) {
component.vm.stop();
component.isStarted = false;
}
}
/**
* Update blueprint execution
* 更新蓝图执行
*/
export function tickBlueprint(component: IBlueprintComponent, deltaTime: number): void {
if (component.vm && component.isStarted) {
component.vm.tick(deltaTime);
}
}
/**
* Clean up blueprint resources
* 清理蓝图资源
*/
export function cleanupBlueprint(component: IBlueprintComponent): void {
if (component.vm) {
if (component.isStarted) {
component.vm.stop();
}
component.vm = null;
component.isStarted = false;
}
}

View File

@@ -0,0 +1,121 @@
/**
* Blueprint Execution System - Manages blueprint lifecycle and execution
* 蓝图执行系统 - 管理蓝图生命周期和执行
*/
import type { Entity, IScene } from '@esengine/ecs-framework';
import {
IBlueprintComponent,
initializeBlueprintVM,
startBlueprint,
tickBlueprint,
cleanupBlueprint
} from './BlueprintComponent';
/**
* Blueprint system interface for engine integration
* 用于引擎集成的蓝图系统接口
*/
export interface IBlueprintSystem {
/** Process entities with blueprint components (处理带有蓝图组件的实体) */
process(entities: IBlueprintEntity[], deltaTime: number): void;
/** Called when entity is added to system (实体添加到系统时调用) */
onEntityAdded(entity: IBlueprintEntity): void;
/** Called when entity is removed from system (实体从系统移除时调用) */
onEntityRemoved(entity: IBlueprintEntity): void;
}
/**
* Entity with blueprint component
* 带有蓝图组件的实体
*/
export interface IBlueprintEntity extends Entity {
/** Blueprint component data (蓝图组件数据) */
blueprintComponent: IBlueprintComponent;
}
/**
* Creates a blueprint execution system
* 创建蓝图执行系统
*/
export function createBlueprintSystem(scene: IScene): IBlueprintSystem {
return {
process(entities: IBlueprintEntity[], deltaTime: number): void {
for (const entity of entities) {
const component = entity.blueprintComponent;
// Skip if no blueprint asset loaded
// 如果没有加载蓝图资产则跳过
if (!component.blueprintAsset) {
continue;
}
// Initialize VM if needed
// 如果需要则初始化 VM
if (!component.vm) {
initializeBlueprintVM(component, entity, scene);
}
// Auto-start if enabled
// 如果启用则自动启动
if (component.autoStart && !component.isStarted) {
startBlueprint(component);
}
// Tick the blueprint
// 更新蓝图
tickBlueprint(component, deltaTime);
}
},
onEntityAdded(entity: IBlueprintEntity): void {
const component = entity.blueprintComponent;
if (component.blueprintAsset) {
initializeBlueprintVM(component, entity, scene);
if (component.autoStart) {
startBlueprint(component);
}
}
},
onEntityRemoved(entity: IBlueprintEntity): void {
cleanupBlueprint(entity.blueprintComponent);
}
};
}
/**
* Utility to manually trigger blueprint events
* 手动触发蓝图事件的工具
*/
export function triggerBlueprintEvent(
entity: IBlueprintEntity,
eventType: string,
data?: Record<string, unknown>
): void {
const vm = entity.blueprintComponent.vm;
if (vm && entity.blueprintComponent.isStarted) {
vm.triggerEvent(eventType, data);
}
}
/**
* Utility to trigger custom events by name
* 按名称触发自定义事件的工具
*/
export function triggerCustomBlueprintEvent(
entity: IBlueprintEntity,
eventName: string,
data?: Record<string, unknown>
): void {
const vm = entity.blueprintComponent.vm;
if (vm && entity.blueprintComponent.isStarted) {
vm.triggerCustomEvent(eventName, data);
}
}

View File

@@ -0,0 +1,336 @@
/**
* Blueprint Virtual Machine - Executes blueprint graphs
* 蓝图虚拟机 - 执行蓝图图
*/
import type { Entity, IScene } from '@esengine/ecs-framework';
import { BlueprintNode } from '../types/nodes';
import { BlueprintAsset } from '../types/blueprint';
import { ExecutionContext, ExecutionResult } from './ExecutionContext';
import { NodeRegistry } from './NodeRegistry';
/**
* Pending execution frame (for delayed/async execution)
* 待处理的执行帧(用于延迟/异步执行)
*/
interface PendingExecution {
nodeId: string;
execPin: string;
resumeTime: number;
}
/**
* Event trigger types
* 事件触发类型
*/
export type EventType =
| 'BeginPlay'
| 'Tick'
| 'EndPlay'
| 'Collision'
| 'TriggerEnter'
| 'TriggerExit'
| 'Custom';
/**
* Blueprint Virtual Machine
* 蓝图虚拟机
*/
export class BlueprintVM {
/** Execution context (执行上下文) */
private _context: ExecutionContext;
/** Pending executions (delayed nodes) (待处理的执行) */
private _pendingExecutions: PendingExecution[] = [];
/** Event node cache by type (按类型缓存的事件节点) */
private _eventNodes: Map<string, BlueprintNode[]> = new Map();
/** Whether the VM is running (VM 是否运行中) */
private _isRunning: boolean = false;
/** Current execution time (当前执行时间) */
private _currentTime: number = 0;
/** Maximum execution steps per frame (每帧最大执行步骤) */
private _maxStepsPerFrame: number = 1000;
/** Debug mode (调试模式) */
debug: boolean = false;
constructor(blueprint: BlueprintAsset, entity: Entity, scene: IScene) {
this._context = new ExecutionContext(blueprint, entity, scene);
this._cacheEventNodes();
}
get context(): ExecutionContext {
return this._context;
}
get isRunning(): boolean {
return this._isRunning;
}
/**
* Cache event nodes by type for quick lookup
* 按类型缓存事件节点以便快速查找
*/
private _cacheEventNodes(): void {
for (const node of this._context.blueprint.nodes) {
// Event nodes start with "Event"
// 事件节点以 "Event" 开头
if (node.type.startsWith('Event')) {
const eventType = node.type;
if (!this._eventNodes.has(eventType)) {
this._eventNodes.set(eventType, []);
}
this._eventNodes.get(eventType)!.push(node);
}
}
}
/**
* Start the VM
* 启动 VM
*/
start(): void {
this._isRunning = true;
this._currentTime = 0;
// Trigger BeginPlay event
// 触发 BeginPlay 事件
this.triggerEvent('EventBeginPlay');
}
/**
* Stop the VM
* 停止 VM
*/
stop(): void {
// Trigger EndPlay event
// 触发 EndPlay 事件
this.triggerEvent('EventEndPlay');
this._isRunning = false;
this._pendingExecutions = [];
}
/**
* Pause the VM
* 暂停 VM
*/
pause(): void {
this._isRunning = false;
}
/**
* Resume the VM
* 恢复 VM
*/
resume(): void {
this._isRunning = true;
}
/**
* Update the VM (called every frame)
* 更新 VM每帧调用
*/
tick(deltaTime: number): void {
if (!this._isRunning) return;
this._currentTime += deltaTime;
this._context.deltaTime = deltaTime;
this._context.time = this._currentTime;
// Process pending delayed executions
// 处理待处理的延迟执行
this._processPendingExecutions();
// Trigger Tick event
// 触发 Tick 事件
this.triggerEvent('EventTick');
}
/**
* Trigger an event by type
* 按类型触发事件
*/
triggerEvent(eventType: string, data?: Record<string, unknown>): void {
const eventNodes = this._eventNodes.get(eventType);
if (!eventNodes) return;
for (const node of eventNodes) {
this._executeFromNode(node, 'exec', data);
}
}
/**
* Trigger a custom event by name
* 按名称触发自定义事件
*/
triggerCustomEvent(eventName: string, data?: Record<string, unknown>): void {
const eventNodes = this._eventNodes.get('EventCustom');
if (!eventNodes) return;
for (const node of eventNodes) {
if (node.data.eventName === eventName) {
this._executeFromNode(node, 'exec', data);
}
}
}
/**
* Execute from a starting node
* 从起始节点执行
*/
private _executeFromNode(
startNode: BlueprintNode,
startPin: string,
eventData?: Record<string, unknown>
): void {
// Clear output cache for new execution
// 为新执行清除输出缓存
this._context.clearOutputCache();
// Set event data as node outputs
// 设置事件数据为节点输出
if (eventData) {
this._context.setOutputs(startNode.id, eventData);
}
// Follow execution chain
// 跟随执行链
let currentNodeId: string | null = startNode.id;
let currentPin: string = startPin;
let steps = 0;
while (currentNodeId && steps < this._maxStepsPerFrame) {
steps++;
// Get connected nodes from current exec pin
// 从当前执行引脚获取连接的节点
const connections = this._context.getConnectionsFromPin(currentNodeId, currentPin);
if (connections.length === 0) {
// No more connections, end execution
// 没有更多连接,结束执行
break;
}
// Execute connected node
// 执行连接的节点
const nextConn = connections[0];
const result = this._executeNode(nextConn.toNodeId);
if (result.error) {
console.error(`Blueprint error in node ${nextConn.toNodeId}: ${result.error}`);
break;
}
if (result.delay && result.delay > 0) {
// Schedule delayed execution
// 安排延迟执行
this._pendingExecutions.push({
nodeId: nextConn.toNodeId,
execPin: result.nextExec ?? 'exec',
resumeTime: this._currentTime + result.delay
});
break;
}
if (result.yield) {
// Yield execution until next frame
// 暂停执行直到下一帧
break;
}
if (result.nextExec === null) {
// Explicitly stop execution
// 显式停止执行
break;
}
// Continue to next node
// 继续到下一个节点
currentNodeId = nextConn.toNodeId;
currentPin = result.nextExec ?? 'exec';
}
if (steps >= this._maxStepsPerFrame) {
console.warn('Blueprint execution exceeded maximum steps, possible infinite loop');
}
}
/**
* Execute a single node
* 执行单个节点
*/
private _executeNode(nodeId: string): ExecutionResult {
const node = this._context.getNode(nodeId);
if (!node) {
return { error: `Node not found: ${nodeId}` };
}
const executor = NodeRegistry.instance.getExecutor(node.type);
if (!executor) {
return { error: `No executor for node type: ${node.type}` };
}
try {
if (this.debug) {
console.log(`[Blueprint] Executing: ${node.type} (${nodeId})`);
}
const result = executor.execute(node, this._context);
// Cache outputs
// 缓存输出
if (result.outputs) {
this._context.setOutputs(nodeId, result.outputs);
}
return result;
} catch (error) {
return { error: `Execution error: ${error}` };
}
}
/**
* Process pending delayed executions
* 处理待处理的延迟执行
*/
private _processPendingExecutions(): void {
const stillPending: PendingExecution[] = [];
for (const pending of this._pendingExecutions) {
if (this._currentTime >= pending.resumeTime) {
// Resume execution
// 恢复执行
const node = this._context.getNode(pending.nodeId);
if (node) {
this._executeFromNode(node, pending.execPin);
}
} else {
stillPending.push(pending);
}
}
this._pendingExecutions = stillPending;
}
/**
* Get instance variables for serialization
* 获取实例变量用于序列化
*/
getInstanceVariables(): Map<string, unknown> {
return this._context.getInstanceVariables();
}
/**
* Set instance variables from serialization
* 从序列化设置实例变量
*/
setInstanceVariables(variables: Map<string, unknown>): void {
this._context.setInstanceVariables(variables);
}
}

View File

@@ -0,0 +1,270 @@
/**
* Execution Context - Runtime context for blueprint execution
* 执行上下文 - 蓝图执行的运行时上下文
*/
import type { Entity, IScene } from '@esengine/ecs-framework';
import { BlueprintNode, BlueprintConnection } from '../types/nodes';
import { BlueprintAsset } from '../types/blueprint';
/**
* Result of node execution
* 节点执行的结果
*/
export interface ExecutionResult {
/**
* Next exec pin to follow (null to stop, undefined to continue default)
* 下一个要执行的引脚null 停止undefined 继续默认)
*/
nextExec?: string | null;
/**
* Output values by pin name
* 按引脚名称的输出值
*/
outputs?: Record<string, unknown>;
/**
* Whether to yield execution (for async operations)
* 是否暂停执行(用于异步操作)
*/
yield?: boolean;
/**
* Delay before continuing (in seconds)
* 继续前的延迟(秒)
*/
delay?: number;
/**
* Error message if execution failed
* 执行失败时的错误消息
*/
error?: string;
}
/**
* Execution context provides access to runtime services
* 执行上下文提供对运行时服务的访问
*/
export class ExecutionContext {
/** Current blueprint asset (当前蓝图资产) */
readonly blueprint: BlueprintAsset;
/** Owner entity (所有者实体) */
readonly entity: Entity;
/** Current scene (当前场景) */
readonly scene: IScene;
/** Frame delta time (帧增量时间) */
deltaTime: number = 0;
/** Total time since start (开始以来的总时间) */
time: number = 0;
/** Instance variables (实例变量) */
private _instanceVariables: Map<string, unknown> = new Map();
/** Local variables (per-execution) (局部变量,每次执行) */
private _localVariables: Map<string, unknown> = new Map();
/** Global variables (shared) (全局变量,共享) */
private static _globalVariables: Map<string, unknown> = new Map();
/** Node output cache for current execution (当前执行的节点输出缓存) */
private _outputCache: Map<string, Record<string, unknown>> = new Map();
/** Connection lookup by target (按目标的连接查找) */
private _connectionsByTarget: Map<string, BlueprintConnection[]> = new Map();
/** Connection lookup by source (按源的连接查找) */
private _connectionsBySource: Map<string, BlueprintConnection[]> = new Map();
constructor(blueprint: BlueprintAsset, entity: Entity, scene: IScene) {
this.blueprint = blueprint;
this.entity = entity;
this.scene = scene;
// Initialize instance variables with defaults
// 使用默认值初始化实例变量
for (const variable of blueprint.variables) {
if (variable.scope === 'instance') {
this._instanceVariables.set(variable.name, variable.defaultValue);
}
}
// Build connection lookup maps
// 构建连接查找映射
this._buildConnectionMaps();
}
private _buildConnectionMaps(): void {
for (const conn of this.blueprint.connections) {
// By target
const targetKey = `${conn.toNodeId}.${conn.toPin}`;
if (!this._connectionsByTarget.has(targetKey)) {
this._connectionsByTarget.set(targetKey, []);
}
this._connectionsByTarget.get(targetKey)!.push(conn);
// By source
const sourceKey = `${conn.fromNodeId}.${conn.fromPin}`;
if (!this._connectionsBySource.has(sourceKey)) {
this._connectionsBySource.set(sourceKey, []);
}
this._connectionsBySource.get(sourceKey)!.push(conn);
}
}
/**
* Get a node by ID
* 通过ID获取节点
*/
getNode(nodeId: string): BlueprintNode | undefined {
return this.blueprint.nodes.find(n => n.id === nodeId);
}
/**
* Get connections to a target pin
* 获取到目标引脚的连接
*/
getConnectionsToPin(nodeId: string, pinName: string): BlueprintConnection[] {
return this._connectionsByTarget.get(`${nodeId}.${pinName}`) ?? [];
}
/**
* Get connections from a source pin
* 获取从源引脚的连接
*/
getConnectionsFromPin(nodeId: string, pinName: string): BlueprintConnection[] {
return this._connectionsBySource.get(`${nodeId}.${pinName}`) ?? [];
}
/**
* Evaluate an input pin value (follows connections or uses default)
* 计算输入引脚值(跟随连接或使用默认值)
*/
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown {
const connections = this.getConnectionsToPin(nodeId, pinName);
if (connections.length === 0) {
// Use default from node data or provided default
// 使用节点数据的默认值或提供的默认值
const node = this.getNode(nodeId);
return node?.data[pinName] ?? defaultValue;
}
// Get value from connected output
// 从连接的输出获取值
const conn = connections[0];
const cachedOutputs = this._outputCache.get(conn.fromNodeId);
if (cachedOutputs && conn.fromPin in cachedOutputs) {
return cachedOutputs[conn.fromPin];
}
// Need to execute the source node first (lazy evaluation)
// 需要先执行源节点(延迟求值)
return defaultValue;
}
/**
* Set output values for a node (cached for current execution)
* 设置节点的输出值(为当前执行缓存)
*/
setOutputs(nodeId: string, outputs: Record<string, unknown>): void {
this._outputCache.set(nodeId, outputs);
}
/**
* Get cached outputs for a node
* 获取节点的缓存输出
*/
getOutputs(nodeId: string): Record<string, unknown> | undefined {
return this._outputCache.get(nodeId);
}
/**
* Clear output cache (call at start of new execution)
* 清除输出缓存(在新执行开始时调用)
*/
clearOutputCache(): void {
this._outputCache.clear();
this._localVariables.clear();
}
/**
* Get a variable value
* 获取变量值
*/
getVariable(name: string): unknown {
// Check local first, then instance, then global
// 先检查局部,然后实例,然后全局
if (this._localVariables.has(name)) {
return this._localVariables.get(name);
}
if (this._instanceVariables.has(name)) {
return this._instanceVariables.get(name);
}
if (ExecutionContext._globalVariables.has(name)) {
return ExecutionContext._globalVariables.get(name);
}
// Return default from variable definition
// 返回变量定义的默认值
const varDef = this.blueprint.variables.find(v => v.name === name);
return varDef?.defaultValue;
}
/**
* Set a variable value
* 设置变量值
*/
setVariable(name: string, value: unknown): void {
const varDef = this.blueprint.variables.find(v => v.name === name);
if (!varDef) {
// Treat unknown variables as local
// 将未知变量视为局部变量
this._localVariables.set(name, value);
return;
}
switch (varDef.scope) {
case 'local':
this._localVariables.set(name, value);
break;
case 'instance':
this._instanceVariables.set(name, value);
break;
case 'global':
ExecutionContext._globalVariables.set(name, value);
break;
}
}
/**
* Get all instance variables (for serialization)
* 获取所有实例变量(用于序列化)
*/
getInstanceVariables(): Map<string, unknown> {
return new Map(this._instanceVariables);
}
/**
* Set instance variables (for deserialization)
* 设置实例变量(用于反序列化)
*/
setInstanceVariables(variables: Map<string, unknown>): void {
this._instanceVariables = new Map(variables);
}
/**
* Clear global variables (for scene reset)
* 清除全局变量(用于场景重置)
*/
static clearGlobalVariables(): void {
ExecutionContext._globalVariables.clear();
}
}

View File

@@ -0,0 +1,151 @@
/**
* Node Registry - Manages node templates and executors
* 节点注册表 - 管理节点模板和执行器
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../types/nodes';
import { ExecutionContext, ExecutionResult } from './ExecutionContext';
/**
* Node executor interface - implements the logic for a node type
* 节点执行器接口 - 实现节点类型的逻辑
*/
export interface INodeExecutor {
/**
* Execute the node
* 执行节点
*
* @param node - Node instance (节点实例)
* @param context - Execution context (执行上下文)
* @returns Execution result (执行结果)
*/
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult;
}
/**
* Node definition combines template with executor
* 节点定义组合模板和执行器
*/
export interface NodeDefinition {
template: BlueprintNodeTemplate;
executor: INodeExecutor;
}
/**
* Node Registry - singleton that holds all registered node types
* 节点注册表 - 持有所有注册节点类型的单例
*/
export class NodeRegistry {
private static _instance: NodeRegistry;
private _nodes: Map<string, NodeDefinition> = new Map();
private constructor() {}
static get instance(): NodeRegistry {
if (!NodeRegistry._instance) {
NodeRegistry._instance = new NodeRegistry();
}
return NodeRegistry._instance;
}
/**
* Register a node type
* 注册节点类型
*/
register(template: BlueprintNodeTemplate, executor: INodeExecutor): void {
if (this._nodes.has(template.type)) {
console.warn(`Node type "${template.type}" is already registered, overwriting`);
}
this._nodes.set(template.type, { template, executor });
}
/**
* Get a node definition by type
* 通过类型获取节点定义
*/
get(type: string): NodeDefinition | undefined {
return this._nodes.get(type);
}
/**
* Get node template by type
* 通过类型获取节点模板
*/
getTemplate(type: string): BlueprintNodeTemplate | undefined {
return this._nodes.get(type)?.template;
}
/**
* Get node executor by type
* 通过类型获取节点执行器
*/
getExecutor(type: string): INodeExecutor | undefined {
return this._nodes.get(type)?.executor;
}
/**
* Check if a node type is registered
* 检查节点类型是否已注册
*/
has(type: string): boolean {
return this._nodes.has(type);
}
/**
* Get all registered templates
* 获取所有注册的模板
*/
getAllTemplates(): BlueprintNodeTemplate[] {
return Array.from(this._nodes.values()).map(d => d.template);
}
/**
* Get templates by category
* 按类别获取模板
*/
getTemplatesByCategory(category: string): BlueprintNodeTemplate[] {
return this.getAllTemplates().filter(t => t.category === category);
}
/**
* Search templates by keyword
* 按关键词搜索模板
*/
searchTemplates(keyword: string): BlueprintNodeTemplate[] {
const lower = keyword.toLowerCase();
return this.getAllTemplates().filter(t =>
t.title.toLowerCase().includes(lower) ||
t.type.toLowerCase().includes(lower) ||
t.keywords?.some(k => k.toLowerCase().includes(lower)) ||
t.description?.toLowerCase().includes(lower)
);
}
/**
* Clear all registrations (for testing)
* 清除所有注册(用于测试)
*/
clear(): void {
this._nodes.clear();
}
}
/**
* Decorator for registering node executors
* 用于注册节点执行器的装饰器
*
* @example
* ```typescript
* @RegisterNode(EventTickTemplate)
* class EventTickExecutor implements INodeExecutor {
* execute(node, context) { ... }
* }
* ```
*/
export function RegisterNode(template: BlueprintNodeTemplate) {
return function<T extends new () => INodeExecutor>(constructor: T) {
const executor = new constructor();
NodeRegistry.instance.register(template, executor);
return constructor;
};
}

View File

@@ -0,0 +1,10 @@
/**
* Blueprint Runtime - Execution engine for blueprints
* 蓝图运行时 - 蓝图执行引擎
*/
export * from './ExecutionContext';
export * from './NodeRegistry';
export * from './BlueprintVM';
export * from './BlueprintComponent';
export * from './BlueprintSystem';

View File

@@ -0,0 +1,22 @@
/**
* Blueprint 模块服务令牌
* Blueprint module service tokens
*
* 定义 blueprint 模块导出的服务令牌。
* Defines service tokens exported by blueprint module.
*
* 注意:当前 Blueprint 模块主要通过组件和系统工作,
* 暂时没有需要通过服务令牌暴露的全局服务。
* 未来可能添加 BlueprintDebuggerToken 等。
*
* Note: Blueprint module currently works mainly through components and systems,
* no global services need to be exposed via service tokens yet.
* May add BlueprintDebuggerToken etc. in the future.
*/
// 当前无服务令牌
// No service tokens currently
// 预留导出位置,便于将来扩展
// Reserved export location for future extension
export {};

View File

@@ -0,0 +1,497 @@
/**
* @zh 蓝图触发器
* @en Blueprint Trigger
*
* @zh 定义触发器的核心实现
* @en Defines core trigger implementation
*/
import type { TriggerType, ITriggerContext } from './TriggerTypes';
import type { ITriggerCondition } from './TriggerCondition';
import { AlwaysTrueCondition } from './TriggerCondition';
// =============================================================================
// 触发器接口 | Trigger Interface
// =============================================================================
/**
* @zh 触发器回调函数类型
* @en Trigger callback function type
*/
export type TriggerCallback = (context: ITriggerContext) => void;
/**
* @zh 蓝图触发器接口
* @en Blueprint trigger interface
*/
export interface IBlueprintTrigger {
/**
* @zh 触发器唯一标识
* @en Trigger unique identifier
*/
readonly id: string;
/**
* @zh 触发器类型
* @en Trigger type
*/
readonly type: TriggerType;
/**
* @zh 触发器条件
* @en Trigger conditions
*/
readonly condition: ITriggerCondition;
/**
* @zh 是否启用
* @en Is enabled
*/
enabled: boolean;
/**
* @zh 优先级(越高越先执行)
* @en Priority (higher executes first)
*/
readonly priority: number;
/**
* @zh 检查是否应该触发
* @en Check if should fire
*/
shouldFire(context: ITriggerContext): boolean;
/**
* @zh 执行触发器
* @en Execute trigger
*/
fire(context: ITriggerContext): void;
}
/**
* @zh 触发器配置
* @en Trigger configuration
*/
export interface TriggerConfig {
/**
* @zh 触发器 ID
* @en Trigger ID
*/
id?: string;
/**
* @zh 触发器类型
* @en Trigger type
*/
type: TriggerType;
/**
* @zh 触发条件
* @en Trigger condition
*/
condition?: ITriggerCondition;
/**
* @zh 是否启用
* @en Is enabled
*/
enabled?: boolean;
/**
* @zh 优先级
* @en Priority
*/
priority?: number;
/**
* @zh 回调函数
* @en Callback function
*/
callback?: TriggerCallback;
}
// =============================================================================
// 触发器实现 | Trigger Implementation
// =============================================================================
let _triggerId = 0;
/**
* @zh 生成唯一触发器 ID
* @en Generate unique trigger ID
*/
function generateTriggerId(): string {
return `trigger_${++_triggerId}`;
}
/**
* @zh 蓝图触发器实现
* @en Blueprint trigger implementation
*/
export class BlueprintTrigger implements IBlueprintTrigger {
readonly id: string;
readonly type: TriggerType;
readonly condition: ITriggerCondition;
readonly priority: number;
enabled: boolean;
private readonly _callback?: TriggerCallback;
private readonly _callbacks: Set<TriggerCallback> = new Set();
constructor(config: TriggerConfig) {
this.id = config.id ?? generateTriggerId();
this.type = config.type;
this.condition = config.condition ?? new AlwaysTrueCondition();
this.priority = config.priority ?? 0;
this.enabled = config.enabled ?? true;
this._callback = config.callback;
}
/**
* @zh 检查是否应该触发
* @en Check if should fire
*/
shouldFire(context: ITriggerContext): boolean {
if (!this.enabled) {
return false;
}
if (context.type !== this.type && this.type !== 'custom') {
return false;
}
return this.condition.evaluate(context);
}
/**
* @zh 执行触发器
* @en Execute trigger
*/
fire(context: ITriggerContext): void {
if (this._callback) {
this._callback(context);
}
for (const callback of this._callbacks) {
callback(context);
}
}
/**
* @zh 添加回调
* @en Add callback
*/
addCallback(callback: TriggerCallback): void {
this._callbacks.add(callback);
}
/**
* @zh 移除回调
* @en Remove callback
*/
removeCallback(callback: TriggerCallback): void {
this._callbacks.delete(callback);
}
/**
* @zh 清除所有回调
* @en Clear all callbacks
*/
clearCallbacks(): void {
this._callbacks.clear();
}
}
// =============================================================================
// 触发器注册表 | Trigger Registry
// =============================================================================
/**
* @zh 触发器注册表接口
* @en Trigger registry interface
*/
export interface ITriggerRegistry {
/**
* @zh 注册触发器
* @en Register trigger
*/
register(trigger: IBlueprintTrigger): void;
/**
* @zh 注销触发器
* @en Unregister trigger
*/
unregister(triggerId: string): boolean;
/**
* @zh 获取触发器
* @en Get trigger
*/
get(triggerId: string): IBlueprintTrigger | undefined;
/**
* @zh 获取所有触发器
* @en Get all triggers
*/
getAll(): IBlueprintTrigger[];
/**
* @zh 按类型获取触发器
* @en Get triggers by type
*/
getByType(type: TriggerType): IBlueprintTrigger[];
/**
* @zh 清除所有触发器
* @en Clear all triggers
*/
clear(): void;
}
/**
* @zh 触发器注册表实现
* @en Trigger registry implementation
*/
export class TriggerRegistry implements ITriggerRegistry {
private readonly _triggers: Map<string, IBlueprintTrigger> = new Map();
private readonly _triggersByType: Map<TriggerType, Set<string>> = new Map();
/**
* @zh 注册触发器
* @en Register trigger
*/
register(trigger: IBlueprintTrigger): void {
if (this._triggers.has(trigger.id)) {
console.warn(`Trigger ${trigger.id} already registered, overwriting`);
}
this._triggers.set(trigger.id, trigger);
if (!this._triggersByType.has(trigger.type)) {
this._triggersByType.set(trigger.type, new Set());
}
this._triggersByType.get(trigger.type)!.add(trigger.id);
}
/**
* @zh 注销触发器
* @en Unregister trigger
*/
unregister(triggerId: string): boolean {
const trigger = this._triggers.get(triggerId);
if (!trigger) {
return false;
}
this._triggers.delete(triggerId);
const typeSet = this._triggersByType.get(trigger.type);
if (typeSet) {
typeSet.delete(triggerId);
}
return true;
}
/**
* @zh 获取触发器
* @en Get trigger
*/
get(triggerId: string): IBlueprintTrigger | undefined {
return this._triggers.get(triggerId);
}
/**
* @zh 获取所有触发器
* @en Get all triggers
*/
getAll(): IBlueprintTrigger[] {
return Array.from(this._triggers.values());
}
/**
* @zh 按类型获取触发器
* @en Get triggers by type
*/
getByType(type: TriggerType): IBlueprintTrigger[] {
const typeSet = this._triggersByType.get(type);
if (!typeSet) {
return [];
}
const triggers: IBlueprintTrigger[] = [];
for (const id of typeSet) {
const trigger = this._triggers.get(id);
if (trigger) {
triggers.push(trigger);
}
}
return triggers.sort((a, b) => b.priority - a.priority);
}
/**
* @zh 清除所有触发器
* @en Clear all triggers
*/
clear(): void {
this._triggers.clear();
this._triggersByType.clear();
}
/**
* @zh 获取触发器数量
* @en Get trigger count
*/
get count(): number {
return this._triggers.size;
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh 创建触发器
* @en Create trigger
*/
export function createTrigger(config: TriggerConfig): BlueprintTrigger {
return new BlueprintTrigger(config);
}
/**
* @zh 创建 Tick 触发器
* @en Create tick trigger
*/
export function createTickTrigger(
callback?: TriggerCallback,
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
): BlueprintTrigger {
return new BlueprintTrigger({
id: options?.id,
type: 'tick',
condition: options?.condition,
priority: options?.priority,
callback
});
}
/**
* @zh 创建输入触发器
* @en Create input trigger
*/
export function createInputTrigger(
callback?: TriggerCallback,
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
): BlueprintTrigger {
return new BlueprintTrigger({
id: options?.id,
type: 'input',
condition: options?.condition,
priority: options?.priority,
callback
});
}
/**
* @zh 创建碰撞触发器
* @en Create collision trigger
*/
export function createCollisionTrigger(
callback?: TriggerCallback,
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
): BlueprintTrigger {
return new BlueprintTrigger({
id: options?.id,
type: 'collision',
condition: options?.condition,
priority: options?.priority,
callback
});
}
/**
* @zh 创建消息触发器
* @en Create message trigger
*/
export function createMessageTrigger(
callback?: TriggerCallback,
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
): BlueprintTrigger {
return new BlueprintTrigger({
id: options?.id,
type: 'message',
condition: options?.condition,
priority: options?.priority,
callback
});
}
/**
* @zh 创建定时器触发器
* @en Create timer trigger
*/
export function createTimerTrigger(
callback?: TriggerCallback,
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
): BlueprintTrigger {
return new BlueprintTrigger({
id: options?.id,
type: 'timer',
condition: options?.condition,
priority: options?.priority,
callback
});
}
/**
* @zh 创建状态进入触发器
* @en Create state enter trigger
*/
export function createStateEnterTrigger(
callback?: TriggerCallback,
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
): BlueprintTrigger {
return new BlueprintTrigger({
id: options?.id,
type: 'stateEnter',
condition: options?.condition,
priority: options?.priority,
callback
});
}
/**
* @zh 创建状态退出触发器
* @en Create state exit trigger
*/
export function createStateExitTrigger(
callback?: TriggerCallback,
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
): BlueprintTrigger {
return new BlueprintTrigger({
id: options?.id,
type: 'stateExit',
condition: options?.condition,
priority: options?.priority,
callback
});
}
/**
* @zh 创建自定义触发器
* @en Create custom trigger
*/
export function createCustomTrigger(
callback?: TriggerCallback,
options?: { id?: string; condition?: ITriggerCondition; priority?: number }
): BlueprintTrigger {
return new BlueprintTrigger({
id: options?.id,
type: 'custom',
condition: options?.condition,
priority: options?.priority,
callback
});
}

View File

@@ -0,0 +1,479 @@
/**
* @zh 触发器条件系统
* @en Trigger Condition System
*
* @zh 提供触发器触发前的条件检查能力
* @en Provides condition checking before trigger fires
*/
import type {
ITriggerContext,
TriggerType,
IInputTriggerContext,
IMessageTriggerContext,
IStateTriggerContext,
ITimerTriggerContext,
ICollisionTriggerContext,
ICustomTriggerContext
} from './TriggerTypes';
// =============================================================================
// 条件接口 | Condition Interface
// =============================================================================
/**
* @zh 触发器条件接口
* @en Trigger condition interface
*/
export interface ITriggerCondition {
/**
* @zh 条件类型标识
* @en Condition type identifier
*/
readonly type: string;
/**
* @zh 评估条件是否满足
* @en Evaluate if condition is met
*
* @param context - @zh 触发器上下文 @en Trigger context
* @returns @zh 条件是否满足 @en Whether condition is met
*/
evaluate(context: ITriggerContext): boolean;
}
/**
* @zh 条件组合逻辑
* @en Condition combination logic
*/
export type ConditionLogic = 'and' | 'or';
// =============================================================================
// 复合条件 | Composite Conditions
// =============================================================================
/**
* @zh 复合条件 - 组合多个条件
* @en Composite condition - combines multiple conditions
*/
export class CompositeCondition implements ITriggerCondition {
readonly type = 'composite';
constructor(
private readonly _conditions: ITriggerCondition[],
private readonly _logic: ConditionLogic = 'and'
) {}
evaluate(context: ITriggerContext): boolean {
if (this._conditions.length === 0) {
return true;
}
if (this._logic === 'and') {
return this._conditions.every(c => c.evaluate(context));
} else {
return this._conditions.some(c => c.evaluate(context));
}
}
}
/**
* @zh 非条件 - 取反
* @en Not condition - negates
*/
export class NotCondition implements ITriggerCondition {
readonly type = 'not';
constructor(private readonly _condition: ITriggerCondition) {}
evaluate(context: ITriggerContext): boolean {
return !this._condition.evaluate(context);
}
}
// =============================================================================
// 通用条件 | Generic Conditions
// =============================================================================
/**
* @zh 始终为真的条件
* @en Always true condition
*/
export class AlwaysTrueCondition implements ITriggerCondition {
readonly type = 'alwaysTrue';
evaluate(_context: ITriggerContext): boolean {
return true;
}
}
/**
* @zh 始终为假的条件
* @en Always false condition
*/
export class AlwaysFalseCondition implements ITriggerCondition {
readonly type = 'alwaysFalse';
evaluate(_context: ITriggerContext): boolean {
return false;
}
}
/**
* @zh 触发器类型条件
* @en Trigger type condition
*/
export class TriggerTypeCondition implements ITriggerCondition {
readonly type = 'triggerType';
constructor(private readonly _allowedTypes: TriggerType[]) {}
evaluate(context: ITriggerContext): boolean {
return this._allowedTypes.includes(context.type);
}
}
/**
* @zh 实体 ID 条件
* @en Entity ID condition
*/
export class EntityIdCondition implements ITriggerCondition {
readonly type = 'entityId';
constructor(
private readonly _entityId: string,
private readonly _checkSource: boolean = true
) {}
evaluate(context: ITriggerContext): boolean {
if (this._checkSource) {
return context.sourceEntityId === this._entityId;
}
return false;
}
}
/**
* @zh 自定义函数条件
* @en Custom function condition
*/
export class FunctionCondition implements ITriggerCondition {
readonly type = 'function';
constructor(
private readonly _predicate: (context: ITriggerContext) => boolean
) {}
evaluate(context: ITriggerContext): boolean {
return this._predicate(context);
}
}
// =============================================================================
// 特定类型条件 | Type-Specific Conditions
// =============================================================================
/**
* @zh 输入动作条件
* @en Input action condition
*/
export class InputActionCondition implements ITriggerCondition {
readonly type = 'inputAction';
constructor(
private readonly _action: string,
private readonly _checkPressed?: boolean,
private readonly _checkReleased?: boolean
) {}
evaluate(context: ITriggerContext): boolean {
if (context.type !== 'input') {
return false;
}
const inputContext = context as unknown as IInputTriggerContext;
if (inputContext.action !== this._action) {
return false;
}
if (this._checkPressed !== undefined && inputContext.pressed !== this._checkPressed) {
return false;
}
if (this._checkReleased !== undefined && inputContext.released !== this._checkReleased) {
return false;
}
return true;
}
}
/**
* @zh 消息名称条件
* @en Message name condition
*/
export class MessageNameCondition implements ITriggerCondition {
readonly type = 'messageName';
constructor(private readonly _messageName: string) {}
evaluate(context: ITriggerContext): boolean {
if (context.type !== 'message') {
return false;
}
const messageContext = context as unknown as IMessageTriggerContext;
return messageContext.messageName === this._messageName;
}
}
/**
* @zh 状态名称条件
* @en State name condition
*/
export class StateNameCondition implements ITriggerCondition {
readonly type = 'stateName';
constructor(
private readonly _stateName: string,
private readonly _checkCurrent: boolean = true
) {}
evaluate(context: ITriggerContext): boolean {
if (context.type !== 'stateEnter' && context.type !== 'stateExit') {
return false;
}
const stateContext = context as unknown as IStateTriggerContext;
if (this._checkCurrent) {
return stateContext.currentState === this._stateName;
} else {
return stateContext.previousState === this._stateName;
}
}
}
/**
* @zh 定时器 ID 条件
* @en Timer ID condition
*/
export class TimerIdCondition implements ITriggerCondition {
readonly type = 'timerId';
constructor(private readonly _timerId: string) {}
evaluate(context: ITriggerContext): boolean {
if (context.type !== 'timer') {
return false;
}
const timerContext = context as unknown as ITimerTriggerContext;
return timerContext.timerId === this._timerId;
}
}
/**
* @zh 碰撞实体条件
* @en Collision entity condition
*/
export class CollisionEntityCondition implements ITriggerCondition {
readonly type = 'collisionEntity';
constructor(
private readonly _otherEntityId?: string,
private readonly _checkEnter?: boolean,
private readonly _checkExit?: boolean
) {}
evaluate(context: ITriggerContext): boolean {
if (context.type !== 'collision') {
return false;
}
const collisionContext = context as unknown as ICollisionTriggerContext;
if (this._otherEntityId !== undefined && collisionContext.otherEntityId !== this._otherEntityId) {
return false;
}
if (this._checkEnter !== undefined && collisionContext.isEnter !== this._checkEnter) {
return false;
}
if (this._checkExit !== undefined && collisionContext.isExit !== this._checkExit) {
return false;
}
return true;
}
}
/**
* @zh 自定义事件名称条件
* @en Custom event name condition
*/
export class CustomEventCondition implements ITriggerCondition {
readonly type = 'customEvent';
constructor(private readonly _eventName: string) {}
evaluate(context: ITriggerContext): boolean {
if (context.type !== 'custom') {
return false;
}
const customContext = context as unknown as ICustomTriggerContext;
return customContext.eventName === this._eventName;
}
}
// =============================================================================
// 条件构建器 | Condition Builder
// =============================================================================
/**
* @zh 条件构建器 - 链式 API
* @en Condition builder - fluent API
*/
export class ConditionBuilder {
private _conditions: ITriggerCondition[] = [];
private _logic: ConditionLogic = 'and';
/**
* @zh 设置组合逻辑为 AND
* @en Set combination logic to AND
*/
and(): this {
this._logic = 'and';
return this;
}
/**
* @zh 设置组合逻辑为 OR
* @en Set combination logic to OR
*/
or(): this {
this._logic = 'or';
return this;
}
/**
* @zh 添加触发器类型条件
* @en Add trigger type condition
*/
ofType(...types: TriggerType[]): this {
this._conditions.push(new TriggerTypeCondition(types));
return this;
}
/**
* @zh 添加实体 ID 条件
* @en Add entity ID condition
*/
fromEntity(entityId: string): this {
this._conditions.push(new EntityIdCondition(entityId));
return this;
}
/**
* @zh 添加输入动作条件
* @en Add input action condition
*/
onInput(action: string, options?: { pressed?: boolean; released?: boolean }): this {
this._conditions.push(new InputActionCondition(action, options?.pressed, options?.released));
return this;
}
/**
* @zh 添加消息条件
* @en Add message condition
*/
onMessage(messageName: string): this {
this._conditions.push(new MessageNameCondition(messageName));
return this;
}
/**
* @zh 添加状态条件
* @en Add state condition
*/
onState(stateName: string, checkCurrent: boolean = true): this {
this._conditions.push(new StateNameCondition(stateName, checkCurrent));
return this;
}
/**
* @zh 添加定时器条件
* @en Add timer condition
*/
onTimer(timerId: string): this {
this._conditions.push(new TimerIdCondition(timerId));
return this;
}
/**
* @zh 添加碰撞条件
* @en Add collision condition
*/
onCollision(options?: { entityId?: string; isEnter?: boolean; isExit?: boolean }): this {
this._conditions.push(new CollisionEntityCondition(
options?.entityId,
options?.isEnter,
options?.isExit
));
return this;
}
/**
* @zh 添加自定义事件条件
* @en Add custom event condition
*/
onCustomEvent(eventName: string): this {
this._conditions.push(new CustomEventCondition(eventName));
return this;
}
/**
* @zh 添加自定义函数条件
* @en Add custom function condition
*/
where(predicate: (context: ITriggerContext) => boolean): this {
this._conditions.push(new FunctionCondition(predicate));
return this;
}
/**
* @zh 添加取反条件
* @en Add negated condition
*/
not(condition: ITriggerCondition): this {
this._conditions.push(new NotCondition(condition));
return this;
}
/**
* @zh 构建条件
* @en Build condition
*/
build(): ITriggerCondition {
if (this._conditions.length === 0) {
return new AlwaysTrueCondition();
}
if (this._conditions.length === 1) {
return this._conditions[0];
}
return new CompositeCondition(this._conditions, this._logic);
}
}
/**
* @zh 创建条件构建器
* @en Create condition builder
*/
export function condition(): ConditionBuilder {
return new ConditionBuilder();
}

View File

@@ -0,0 +1,461 @@
/**
* @zh 触发器调度器
* @en Trigger Dispatcher
*
* @zh 负责分发触发器事件到订阅者
* @en Responsible for dispatching trigger events to subscribers
*/
import type { TriggerType, ITriggerContext } from './TriggerTypes';
import type { IBlueprintTrigger, ITriggerRegistry, TriggerCallback } from './BlueprintTrigger';
import { TriggerRegistry } from './BlueprintTrigger';
// =============================================================================
// 调度器接口 | Dispatcher Interface
// =============================================================================
/**
* @zh 触发结果
* @en Trigger result
*/
export interface TriggerResult {
/**
* @zh 触发器 ID
* @en Trigger ID
*/
triggerId: string;
/**
* @zh 是否成功
* @en Is successful
*/
success: boolean;
/**
* @zh 错误信息
* @en Error message
*/
error?: string;
}
/**
* @zh 调度结果
* @en Dispatch result
*/
export interface DispatchResult {
/**
* @zh 上下文
* @en Context
*/
context: ITriggerContext;
/**
* @zh 触发的触发器数量
* @en Number of triggers fired
*/
triggeredCount: number;
/**
* @zh 各触发器结果
* @en Results of each trigger
*/
results: TriggerResult[];
}
/**
* @zh 触发器调度器接口
* @en Trigger dispatcher interface
*/
export interface ITriggerDispatcher {
/**
* @zh 调度触发器
* @en Dispatch trigger
*/
dispatch(context: ITriggerContext): DispatchResult;
/**
* @zh 异步调度触发器
* @en Async dispatch trigger
*/
dispatchAsync(context: ITriggerContext): Promise<DispatchResult>;
/**
* @zh 订阅触发器类型
* @en Subscribe to trigger type
*/
subscribe(type: TriggerType, callback: TriggerCallback): () => void;
/**
* @zh 取消订阅
* @en Unsubscribe
*/
unsubscribe(type: TriggerType, callback: TriggerCallback): void;
/**
* @zh 获取注册表
* @en Get registry
*/
readonly registry: ITriggerRegistry;
}
// =============================================================================
// 调度器实现 | Dispatcher Implementation
// =============================================================================
/**
* @zh 触发器调度器实现
* @en Trigger dispatcher implementation
*/
export class TriggerDispatcher implements ITriggerDispatcher {
private readonly _registry: ITriggerRegistry;
private readonly _typeSubscribers: Map<TriggerType, Set<TriggerCallback>> = new Map();
private readonly _globalSubscribers: Set<TriggerCallback> = new Set();
private _isDispatching: boolean = false;
private _pendingContexts: ITriggerContext[] = [];
constructor(registry?: ITriggerRegistry) {
this._registry = registry ?? new TriggerRegistry();
}
get registry(): ITriggerRegistry {
return this._registry;
}
/**
* @zh 调度触发器
* @en Dispatch trigger
*/
dispatch(context: ITriggerContext): DispatchResult {
if (this._isDispatching) {
this._pendingContexts.push(context);
return {
context,
triggeredCount: 0,
results: []
};
}
this._isDispatching = true;
try {
const result = this._doDispatch(context);
while (this._pendingContexts.length > 0) {
const pendingContext = this._pendingContexts.shift()!;
this._doDispatch(pendingContext);
}
return result;
} finally {
this._isDispatching = false;
}
}
/**
* @zh 执行调度
* @en Do dispatch
*/
private _doDispatch(context: ITriggerContext): DispatchResult {
const results: TriggerResult[] = [];
let triggeredCount = 0;
const triggers = this._registry.getByType(context.type);
for (const trigger of triggers) {
if (trigger.shouldFire(context)) {
try {
trigger.fire(context);
triggeredCount++;
results.push({
triggerId: trigger.id,
success: true
});
} catch (error) {
results.push({
triggerId: trigger.id,
success: false,
error: error instanceof Error ? error.message : String(error)
});
}
}
}
this._notifySubscribers(context);
return {
context,
triggeredCount,
results
};
}
/**
* @zh 通知订阅者
* @en Notify subscribers
*/
private _notifySubscribers(context: ITriggerContext): void {
const typeSubscribers = this._typeSubscribers.get(context.type);
if (typeSubscribers) {
for (const callback of typeSubscribers) {
try {
callback(context);
} catch (error) {
console.error(`Trigger subscriber error: ${error}`);
}
}
}
for (const callback of this._globalSubscribers) {
try {
callback(context);
} catch (error) {
console.error(`Global trigger subscriber error: ${error}`);
}
}
}
/**
* @zh 异步调度触发器
* @en Async dispatch trigger
*/
async dispatchAsync(context: ITriggerContext): Promise<DispatchResult> {
return new Promise((resolve) => {
queueMicrotask(() => {
resolve(this.dispatch(context));
});
});
}
/**
* @zh 订阅触发器类型
* @en Subscribe to trigger type
*/
subscribe(type: TriggerType, callback: TriggerCallback): () => void {
if (!this._typeSubscribers.has(type)) {
this._typeSubscribers.set(type, new Set());
}
this._typeSubscribers.get(type)!.add(callback);
return () => this.unsubscribe(type, callback);
}
/**
* @zh 取消订阅
* @en Unsubscribe
*/
unsubscribe(type: TriggerType, callback: TriggerCallback): void {
const subscribers = this._typeSubscribers.get(type);
if (subscribers) {
subscribers.delete(callback);
}
}
/**
* @zh 订阅所有触发器
* @en Subscribe to all triggers
*/
subscribeAll(callback: TriggerCallback): () => void {
this._globalSubscribers.add(callback);
return () => this.unsubscribeAll(callback);
}
/**
* @zh 取消订阅所有
* @en Unsubscribe from all
*/
unsubscribeAll(callback: TriggerCallback): void {
this._globalSubscribers.delete(callback);
}
/**
* @zh 清除所有订阅
* @en Clear all subscriptions
*/
clearSubscriptions(): void {
this._typeSubscribers.clear();
this._globalSubscribers.clear();
}
}
// =============================================================================
// 实体触发器管理器 | Entity Trigger Manager
// =============================================================================
/**
* @zh 实体触发器管理器接口
* @en Entity trigger manager interface
*/
export interface IEntityTriggerManager {
/**
* @zh 为实体注册触发器
* @en Register trigger for entity
*/
registerForEntity(entityId: string, trigger: IBlueprintTrigger): void;
/**
* @zh 注销实体的触发器
* @en Unregister trigger from entity
*/
unregisterFromEntity(entityId: string, triggerId: string): boolean;
/**
* @zh 获取实体的所有触发器
* @en Get all triggers for entity
*/
getEntityTriggers(entityId: string): IBlueprintTrigger[];
/**
* @zh 清除实体的所有触发器
* @en Clear all triggers for entity
*/
clearEntityTriggers(entityId: string): void;
/**
* @zh 调度器
* @en Dispatcher
*/
readonly dispatcher: ITriggerDispatcher;
}
/**
* @zh 实体触发器管理器实现
* @en Entity trigger manager implementation
*/
export class EntityTriggerManager implements IEntityTriggerManager {
private readonly _dispatcher: ITriggerDispatcher;
private readonly _entityTriggers: Map<string, Set<string>> = new Map();
constructor(dispatcher?: ITriggerDispatcher) {
this._dispatcher = dispatcher ?? new TriggerDispatcher();
}
get dispatcher(): ITriggerDispatcher {
return this._dispatcher;
}
/**
* @zh 为实体注册触发器
* @en Register trigger for entity
*/
registerForEntity(entityId: string, trigger: IBlueprintTrigger): void {
this._dispatcher.registry.register(trigger);
if (!this._entityTriggers.has(entityId)) {
this._entityTriggers.set(entityId, new Set());
}
this._entityTriggers.get(entityId)!.add(trigger.id);
}
/**
* @zh 注销实体的触发器
* @en Unregister trigger from entity
*/
unregisterFromEntity(entityId: string, triggerId: string): boolean {
const entitySet = this._entityTriggers.get(entityId);
if (!entitySet) {
return false;
}
if (!entitySet.has(triggerId)) {
return false;
}
entitySet.delete(triggerId);
return this._dispatcher.registry.unregister(triggerId);
}
/**
* @zh 获取实体的所有触发器
* @en Get all triggers for entity
*/
getEntityTriggers(entityId: string): IBlueprintTrigger[] {
const entitySet = this._entityTriggers.get(entityId);
if (!entitySet) {
return [];
}
const triggers: IBlueprintTrigger[] = [];
for (const triggerId of entitySet) {
const trigger = this._dispatcher.registry.get(triggerId);
if (trigger) {
triggers.push(trigger);
}
}
return triggers;
}
/**
* @zh 清除实体的所有触发器
* @en Clear all triggers for entity
*/
clearEntityTriggers(entityId: string): void {
const entitySet = this._entityTriggers.get(entityId);
if (!entitySet) {
return;
}
for (const triggerId of entitySet) {
this._dispatcher.registry.unregister(triggerId);
}
this._entityTriggers.delete(entityId);
}
/**
* @zh 调度触发器到实体
* @en Dispatch trigger to entity
*/
dispatchToEntity(entityId: string, context: ITriggerContext): DispatchResult {
const entityTriggers = this.getEntityTriggers(entityId);
const results: TriggerResult[] = [];
let triggeredCount = 0;
for (const trigger of entityTriggers) {
if (trigger.shouldFire(context)) {
try {
trigger.fire(context);
triggeredCount++;
results.push({
triggerId: trigger.id,
success: true
});
} catch (error) {
results.push({
triggerId: trigger.id,
success: false,
error: error instanceof Error ? error.message : String(error)
});
}
}
}
return {
context,
triggeredCount,
results
};
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh 创建触发器调度器
* @en Create trigger dispatcher
*/
export function createTriggerDispatcher(registry?: ITriggerRegistry): TriggerDispatcher {
return new TriggerDispatcher(registry);
}
/**
* @zh 创建实体触发器管理器
* @en Create entity trigger manager
*/
export function createEntityTriggerManager(dispatcher?: ITriggerDispatcher): EntityTriggerManager {
return new EntityTriggerManager(dispatcher);
}

View File

@@ -0,0 +1,400 @@
/**
* @zh 蓝图触发器类型定义
* @en Blueprint Trigger Type Definitions
*
* @zh 定义触发器的核心类型和接口
* @en Defines core types and interfaces for triggers
*/
// =============================================================================
// 触发器类型 | Trigger Types
// =============================================================================
/**
* @zh 触发器类型枚举
* @en Trigger type enumeration
*/
export type TriggerType =
| 'tick' // 每帧触发 | Every frame
| 'input' // 输入事件 | Input event
| 'collision' // 碰撞事件 | Collision event
| 'message' // 消息事件 | Message event
| 'timer' // 定时器事件 | Timer event
| 'stateEnter' // 状态进入 | State enter
| 'stateExit' // 状态退出 | State exit
| 'custom'; // 自定义事件 | Custom event
/**
* @zh 触发器类型常量
* @en Trigger type constants
*/
export const TriggerTypes = {
TICK: 'tick' as const,
INPUT: 'input' as const,
COLLISION: 'collision' as const,
MESSAGE: 'message' as const,
TIMER: 'timer' as const,
STATE_ENTER: 'stateEnter' as const,
STATE_EXIT: 'stateExit' as const,
CUSTOM: 'custom' as const
} as const;
// =============================================================================
// 触发器上下文 | Trigger Context
// =============================================================================
/**
* @zh 触发器上下文基础接口
* @en Trigger context base interface
*/
export interface ITriggerContext {
/**
* @zh 触发器类型
* @en Trigger type
*/
readonly type: TriggerType;
/**
* @zh 触发时间戳
* @en Trigger timestamp
*/
readonly timestamp: number;
/**
* @zh 触发源实体 ID
* @en Source entity ID
*/
readonly sourceEntityId?: string;
/**
* @zh 附加数据
* @en Additional data
*/
readonly data?: Record<string, unknown>;
}
/**
* @zh Tick 触发器上下文
* @en Tick trigger context
*/
export interface ITickTriggerContext extends ITriggerContext {
readonly type: 'tick';
/**
* @zh 增量时间(秒)
* @en Delta time (seconds)
*/
readonly deltaTime: number;
/**
* @zh 帧计数
* @en Frame count
*/
readonly frameCount: number;
}
/**
* @zh 输入触发器上下文
* @en Input trigger context
*/
export interface IInputTriggerContext extends ITriggerContext {
readonly type: 'input';
/**
* @zh 输入动作名称
* @en Input action name
*/
readonly action: string;
/**
* @zh 输入值
* @en Input value
*/
readonly value: number | boolean;
/**
* @zh 是否刚按下
* @en Is just pressed
*/
readonly pressed?: boolean;
/**
* @zh 是否刚释放
* @en Is just released
*/
readonly released?: boolean;
}
/**
* @zh 碰撞触发器上下文
* @en Collision trigger context
*/
export interface ICollisionTriggerContext extends ITriggerContext {
readonly type: 'collision';
/**
* @zh 碰撞的另一个实体 ID
* @en Other entity ID in collision
*/
readonly otherEntityId: string;
/**
* @zh 碰撞点
* @en Collision point
*/
readonly point?: { x: number; y: number };
/**
* @zh 碰撞法线
* @en Collision normal
*/
readonly normal?: { x: number; y: number };
/**
* @zh 是否开始碰撞
* @en Is collision start
*/
readonly isEnter: boolean;
/**
* @zh 是否结束碰撞
* @en Is collision end
*/
readonly isExit: boolean;
}
/**
* @zh 消息触发器上下文
* @en Message trigger context
*/
export interface IMessageTriggerContext extends ITriggerContext {
readonly type: 'message';
/**
* @zh 消息名称
* @en Message name
*/
readonly messageName: string;
/**
* @zh 发送者 ID
* @en Sender ID
*/
readonly senderId?: string;
/**
* @zh 消息负载
* @en Message payload
*/
readonly payload?: unknown;
}
/**
* @zh 定时器触发器上下文
* @en Timer trigger context
*/
export interface ITimerTriggerContext extends ITriggerContext {
readonly type: 'timer';
/**
* @zh 定时器 ID
* @en Timer ID
*/
readonly timerId: string;
/**
* @zh 是否循环触发
* @en Is repeating
*/
readonly isRepeating: boolean;
/**
* @zh 已触发次数
* @en Times fired
*/
readonly timesFired: number;
}
/**
* @zh 状态触发器上下文
* @en State trigger context
*/
export interface IStateTriggerContext extends ITriggerContext {
readonly type: 'stateEnter' | 'stateExit';
/**
* @zh 状态机 ID
* @en State machine ID
*/
readonly stateMachineId: string;
/**
* @zh 当前状态
* @en Current state
*/
readonly currentState: string;
/**
* @zh 之前状态
* @en Previous state
*/
readonly previousState?: string;
}
/**
* @zh 自定义触发器上下文
* @en Custom trigger context
*/
export interface ICustomTriggerContext extends ITriggerContext {
readonly type: 'custom';
/**
* @zh 事件名称
* @en Event name
*/
readonly eventName: string;
}
/**
* @zh 所有触发器上下文的联合类型
* @en Union type of all trigger contexts
*/
export type TriggerContext =
| ITickTriggerContext
| IInputTriggerContext
| ICollisionTriggerContext
| IMessageTriggerContext
| ITimerTriggerContext
| IStateTriggerContext
| ICustomTriggerContext;
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh 创建 Tick 触发器上下文
* @en Create tick trigger context
*/
export function createTickContext(
deltaTime: number,
frameCount: number,
sourceEntityId?: string
): ITickTriggerContext {
return {
type: 'tick',
timestamp: Date.now(),
deltaTime,
frameCount,
sourceEntityId
};
}
/**
* @zh 创建输入触发器上下文
* @en Create input trigger context
*/
export function createInputContext(
action: string,
value: number | boolean,
options?: {
pressed?: boolean;
released?: boolean;
sourceEntityId?: string;
}
): IInputTriggerContext {
return {
type: 'input',
timestamp: Date.now(),
action,
value,
pressed: options?.pressed,
released: options?.released,
sourceEntityId: options?.sourceEntityId
};
}
/**
* @zh 创建碰撞触发器上下文
* @en Create collision trigger context
*/
export function createCollisionContext(
otherEntityId: string,
isEnter: boolean,
options?: {
point?: { x: number; y: number };
normal?: { x: number; y: number };
sourceEntityId?: string;
}
): ICollisionTriggerContext {
return {
type: 'collision',
timestamp: Date.now(),
otherEntityId,
isEnter,
isExit: !isEnter,
point: options?.point,
normal: options?.normal,
sourceEntityId: options?.sourceEntityId
};
}
/**
* @zh 创建消息触发器上下文
* @en Create message trigger context
*/
export function createMessageContext(
messageName: string,
payload?: unknown,
options?: {
senderId?: string;
sourceEntityId?: string;
}
): IMessageTriggerContext {
return {
type: 'message',
timestamp: Date.now(),
messageName,
payload,
senderId: options?.senderId,
sourceEntityId: options?.sourceEntityId
};
}
/**
* @zh 创建定时器触发器上下文
* @en Create timer trigger context
*/
export function createTimerContext(
timerId: string,
isRepeating: boolean,
timesFired: number,
sourceEntityId?: string
): ITimerTriggerContext {
return {
type: 'timer',
timestamp: Date.now(),
timerId,
isRepeating,
timesFired,
sourceEntityId
};
}
/**
* @zh 创建状态触发器上下文
* @en Create state trigger context
*/
export function createStateContext(
type: 'stateEnter' | 'stateExit',
stateMachineId: string,
currentState: string,
previousState?: string,
sourceEntityId?: string
): IStateTriggerContext {
return {
type,
timestamp: Date.now(),
stateMachineId,
currentState,
previousState,
sourceEntityId
};
}
/**
* @zh 创建自定义触发器上下文
* @en Create custom trigger context
*/
export function createCustomContext(
eventName: string,
data?: Record<string, unknown>,
sourceEntityId?: string
): ICustomTriggerContext {
return {
type: 'custom',
timestamp: Date.now(),
eventName,
data,
sourceEntityId
};
}

View File

@@ -0,0 +1,105 @@
/**
* @zh 蓝图触发器模块
* @en Blueprint Triggers Module
*
* @zh 提供蓝图触发器系统的所有导出
* @en Provides all exports for the blueprint trigger system
*/
// =============================================================================
// 触发器类型 | Trigger Types
// =============================================================================
export type {
TriggerType,
ITriggerContext,
ITickTriggerContext,
IInputTriggerContext,
ICollisionTriggerContext,
IMessageTriggerContext,
ITimerTriggerContext,
IStateTriggerContext,
ICustomTriggerContext,
TriggerContext
} from './TriggerTypes';
export {
TriggerTypes,
createTickContext,
createInputContext,
createCollisionContext,
createMessageContext,
createTimerContext,
createStateContext,
createCustomContext
} from './TriggerTypes';
// =============================================================================
// 触发器条件 | Trigger Conditions
// =============================================================================
export type {
ITriggerCondition,
ConditionLogic
} from './TriggerCondition';
export {
CompositeCondition,
NotCondition,
AlwaysTrueCondition,
AlwaysFalseCondition,
TriggerTypeCondition,
EntityIdCondition,
FunctionCondition,
InputActionCondition,
MessageNameCondition,
StateNameCondition,
TimerIdCondition,
CollisionEntityCondition,
CustomEventCondition,
ConditionBuilder,
condition
} from './TriggerCondition';
// =============================================================================
// 蓝图触发器 | Blueprint Trigger
// =============================================================================
export type {
TriggerCallback,
IBlueprintTrigger,
TriggerConfig,
ITriggerRegistry
} from './BlueprintTrigger';
export {
BlueprintTrigger,
TriggerRegistry,
createTrigger,
createTickTrigger,
createInputTrigger,
createCollisionTrigger,
createMessageTrigger,
createTimerTrigger,
createStateEnterTrigger,
createStateExitTrigger,
createCustomTrigger
} from './BlueprintTrigger';
// =============================================================================
// 触发器调度器 | Trigger Dispatcher
// =============================================================================
export type {
TriggerResult,
DispatchResult,
ITriggerDispatcher,
IEntityTriggerManager
} from './TriggerDispatcher';
export {
TriggerDispatcher,
EntityTriggerManager,
createTriggerDispatcher,
createEntityTriggerManager
} from './TriggerDispatcher';

View File

@@ -0,0 +1,125 @@
/**
* Blueprint Asset Types
* 蓝图资产类型
*/
import { BlueprintNode, BlueprintConnection } from './nodes';
/**
* Variable scope determines lifetime and accessibility
* 变量作用域决定生命周期和可访问性
*/
export type VariableScope =
| 'local' // Per-execution (每次执行)
| 'instance' // Per-entity (每个实体)
| 'global'; // Shared across all (全局共享)
/**
* Blueprint variable definition
* 蓝图变量定义
*/
export interface BlueprintVariable {
/** Variable name (变量名) */
name: string;
/** Variable type (变量类型) */
type: string;
/** Default value (默认值) */
defaultValue: unknown;
/** Variable scope (变量作用域) */
scope: VariableScope;
/** Category for organization (分类) */
category?: string;
/** Description tooltip (描述提示) */
tooltip?: string;
}
/**
* Blueprint asset metadata
* 蓝图资产元数据
*/
export interface BlueprintMetadata {
/** Blueprint name (蓝图名称) */
name: string;
/** Description (描述) */
description?: string;
/** Category for organization (分类) */
category?: string;
/** Author (作者) */
author?: string;
/** Creation timestamp (创建时间戳) */
createdAt?: number;
/** Last modified timestamp (最后修改时间戳) */
modifiedAt?: number;
}
/**
* Blueprint asset format - saved to .bp files
* 蓝图资产格式 - 保存为 .bp 文件
*/
export interface BlueprintAsset {
/** Format version (格式版本) */
version: number;
/** Asset type identifier (资产类型标识符) */
type: 'blueprint';
/** Metadata (元数据) */
metadata: BlueprintMetadata;
/** Variable definitions (变量定义) */
variables: BlueprintVariable[];
/** Node instances (节点实例) */
nodes: BlueprintNode[];
/** Connections between nodes (节点之间的连接) */
connections: BlueprintConnection[];
}
/**
* Creates an empty blueprint asset
* 创建空的蓝图资产
*/
export function createEmptyBlueprint(name: string): BlueprintAsset {
return {
version: 1,
type: 'blueprint',
metadata: {
name,
createdAt: Date.now(),
modifiedAt: Date.now()
},
variables: [],
nodes: [],
connections: []
};
}
/**
* Validates a blueprint asset structure
* 验证蓝图资产结构
*/
export function validateBlueprintAsset(asset: unknown): asset is BlueprintAsset {
if (!asset || typeof asset !== 'object') return false;
const bp = asset as BlueprintAsset;
return (
typeof bp.version === 'number' &&
bp.type === 'blueprint' &&
typeof bp.metadata === 'object' &&
Array.isArray(bp.variables) &&
Array.isArray(bp.nodes) &&
Array.isArray(bp.connections)
);
}

View File

@@ -0,0 +1,3 @@
export * from './pins';
export * from './nodes';
export * from './blueprint';

View File

@@ -0,0 +1,138 @@
/**
* Blueprint Node Types
* 蓝图节点类型
*/
import { BlueprintPinDefinition } from './pins';
/**
* Node category for visual styling and organization
* 节点类别,用于视觉样式和组织
*/
export type BlueprintNodeCategory =
| 'event' // Event nodes - red (事件节点 - 红色)
| 'flow' // Flow control - gray (流程控制 - 灰色)
| 'entity' // Entity operations - blue (实体操作 - 蓝色)
| 'component' // Component access - cyan (组件访问 - 青色)
| 'math' // Math operations - green (数学运算 - 绿色)
| 'logic' // Logic operations - red (逻辑运算 - 红色)
| 'variable' // Variable access - purple (变量访问 - 紫色)
| 'input' // Input handling - orange (输入处理 - 橙色)
| 'physics' // Physics - yellow (物理 - 黄色)
| 'audio' // Audio - pink (音频 - 粉色)
| 'time' // Time utilities - cyan (时间工具 - 青色)
| 'debug' // Debug utilities - gray (调试工具 - 灰色)
| 'custom'; // Custom nodes (自定义节点)
/**
* Node template definition - describes a type of node
* 节点模板定义 - 描述一种节点类型
*/
export interface BlueprintNodeTemplate {
/** Unique type identifier (唯一类型标识符) */
type: string;
/** Display title (显示标题) */
title: string;
/** Node category (节点类别) */
category: BlueprintNodeCategory;
/** Optional subtitle (可选副标题) */
subtitle?: string;
/** Icon name (图标名称) */
icon?: string;
/** Description for documentation (文档描述) */
description?: string;
/** Search keywords (搜索关键词) */
keywords?: string[];
/** Menu path for node palette (节点面板的菜单路径) */
menuPath?: string[];
/** Input pin definitions (输入引脚定义) */
inputs: BlueprintPinDefinition[];
/** Output pin definitions (输出引脚定义) */
outputs: BlueprintPinDefinition[];
/** Whether this node is pure (no exec pins) (是否是纯节点,无执行引脚) */
isPure?: boolean;
/** Whether this node can be collapsed (是否可折叠) */
collapsible?: boolean;
/** Custom header color override (自定义头部颜色) */
headerColor?: string;
/** Node color for visual distinction (节点颜色用于视觉区分) */
color?: string;
}
/**
* Node instance in a blueprint graph
* 蓝图图中的节点实例
*/
export interface BlueprintNode {
/** Unique instance ID (唯一实例ID) */
id: string;
/** Template type reference (模板类型引用) */
type: string;
/** Position in graph (图中位置) */
position: { x: number; y: number };
/** Custom data for this instance (此实例的自定义数据) */
data: Record<string, unknown>;
/** Comment/note for this node (此节点的注释) */
comment?: string;
}
/**
* Connection between two pins
* 两个引脚之间的连接
*/
export interface BlueprintConnection {
/** Unique connection ID (唯一连接ID) */
id: string;
/** Source node ID (源节点ID) */
fromNodeId: string;
/** Source pin name (源引脚名称) */
fromPin: string;
/** Target node ID (目标节点ID) */
toNodeId: string;
/** Target pin name (目标引脚名称) */
toPin: string;
}
/**
* Gets the header color for a node category
* 获取节点类别的头部颜色
*/
export function getNodeCategoryColor(category: BlueprintNodeCategory): string {
const colors: Record<BlueprintNodeCategory, string> = {
event: '#8b1e1e',
flow: '#4a4a4a',
entity: '#1e5a8b',
component: '#1e8b8b',
math: '#1e8b5a',
logic: '#8b1e5a',
variable: '#5a1e8b',
input: '#8b5a1e',
physics: '#8b8b1e',
audio: '#8b1e6b',
time: '#1e6b8b',
debug: '#5a5a5a',
custom: '#4a4a4a'
};
return colors[category] ?? colors.custom;
}

View File

@@ -0,0 +1,135 @@
/**
* Blueprint Pin Types
* 蓝图引脚类型
*/
/**
* Pin data type for blueprint nodes
* 蓝图节点的引脚数据类型
*/
export type BlueprintPinType =
| 'exec' // Execution flow (执行流)
| 'bool' // Boolean (布尔)
| 'int' // Integer (整数)
| 'float' // Float (浮点数)
| 'string' // String (字符串)
| 'vector2' // 2D Vector (二维向量)
| 'vector3' // 3D Vector (三维向量)
| 'color' // RGBA Color (颜色)
| 'entity' // Entity reference (实体引用)
| 'component' // Component reference (组件引用)
| 'object' // Generic object (通用对象)
| 'array' // Array (数组)
| 'any'; // Wildcard (通配符)
/**
* Pin direction
* 引脚方向
*/
export type BlueprintPinDirection = 'input' | 'output';
/**
* Pin definition for node templates
* 节点模板的引脚定义
*
* Note: direction is determined by whether the pin is in inputs[] or outputs[] array
* 注意:方向由引脚在 inputs[] 还是 outputs[] 数组中决定
*/
export interface BlueprintPinDefinition {
/** Unique name within node (节点内唯一名称) */
name: string;
/** Pin data type (引脚数据类型) */
type: BlueprintPinType;
/** Display name shown in the editor (编辑器中显示的名称) */
displayName?: string;
/** Default value when not connected (未连接时的默认值) */
defaultValue?: unknown;
/** Allow multiple connections (允许多个连接) */
allowMultiple?: boolean;
/** Array element type if type is 'array' (数组元素类型) */
arrayType?: BlueprintPinType;
/** Whether this pin is optional (是否可选) */
optional?: boolean;
/** Tooltip description (提示描述) */
tooltip?: string;
}
/**
* Runtime pin with direction - used when processing pins
* 带方向的运行时引脚 - 处理引脚时使用
*/
export interface BlueprintRuntimePin extends BlueprintPinDefinition {
/** Pin direction (引脚方向) */
direction: BlueprintPinDirection;
}
/**
* Pin instance in a node
* 节点中的引脚实例
*/
export interface BlueprintPin {
id: string;
nodeId: string;
definition: BlueprintPinDefinition;
value?: unknown;
}
/**
* Gets the color for a pin type
* 获取引脚类型的颜色
*/
export function getPinTypeColor(type: BlueprintPinType): string {
const colors: Record<BlueprintPinType, string> = {
exec: '#ffffff',
bool: '#cc0000',
int: '#00d4aa',
float: '#88cc00',
string: '#ff88cc',
vector2: '#d4aa00',
vector3: '#ffcc00',
color: '#ff8844',
entity: '#0088ff',
component: '#44aaff',
object: '#4444aa',
array: '#8844ff',
any: '#888888'
};
return colors[type] ?? colors.any;
}
/**
* Checks if two pin types are compatible for connection
* 检查两个引脚类型是否兼容连接
*/
export function arePinTypesCompatible(from: BlueprintPinType, to: BlueprintPinType): boolean {
// Same type always compatible
// 相同类型始终兼容
if (from === to) return true;
// Any type is compatible with everything
// any 类型与所有类型兼容
if (from === 'any' || to === 'any') return true;
// Exec can only connect to exec
// exec 只能连接 exec
if (from === 'exec' || to === 'exec') return false;
// Numeric coercion
// 数值类型转换
const numericTypes: BlueprintPinType[] = ['int', 'float'];
if (numericTypes.includes(from) && numericTypes.includes(to)) return true;
// Vector coercion
// 向量类型转换
const vectorTypes: BlueprintPinType[] = ['vector2', 'vector3', 'color'];
if (vectorTypes.includes(from) && vectorTypes.includes(to)) return true;
return false;
}

View File

@@ -0,0 +1,23 @@
{
"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,
"jsx": "react-jsx",
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"composite": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "src/esengine"],
"references": [
{ "path": "../core" }
]
}

View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'tsup';
import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup';
export default defineConfig({
...runtimeOnlyPreset({
tsupConfig: {
entry: {
index: 'src/index.ts'
}
}
}),
tsconfig: 'tsconfig.build.json'
});