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:
43
packages/framework/blueprint/module.json
Normal file
43
packages/framework/blueprint/module.json
Normal 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"
|
||||
}
|
||||
58
packages/framework/blueprint/package.json
Normal file
58
packages/framework/blueprint/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
30
packages/framework/blueprint/plugin.json
Normal file
30
packages/framework/blueprint/plugin.json
Normal 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"
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
208
packages/framework/blueprint/src/composition/FragmentRegistry.ts
Normal file
208
packages/framework/blueprint/src/composition/FragmentRegistry.ts
Normal 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();
|
||||
}
|
||||
57
packages/framework/blueprint/src/composition/index.ts
Normal file
57
packages/framework/blueprint/src/composition/index.ts
Normal 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';
|
||||
67
packages/framework/blueprint/src/esengine/BlueprintPlugin.ts
Normal file
67
packages/framework/blueprint/src/esengine/BlueprintPlugin.ts
Normal 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 };
|
||||
37
packages/framework/blueprint/src/esengine/index.ts
Normal file
37
packages/framework/blueprint/src/esengine/index.ts
Normal 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';
|
||||
67
packages/framework/blueprint/src/index.ts
Normal file
67
packages/framework/blueprint/src/index.ts
Normal 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';
|
||||
91
packages/framework/blueprint/src/nodes/debug/Print.ts
Normal file
91
packages/framework/blueprint/src/nodes/debug/Print.ts
Normal 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'
|
||||
};
|
||||
}
|
||||
}
|
||||
6
packages/framework/blueprint/src/nodes/debug/index.ts
Normal file
6
packages/framework/blueprint/src/nodes/debug/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Debug Nodes - Tools for debugging blueprints
|
||||
* 调试节点 - 蓝图调试工具
|
||||
*/
|
||||
|
||||
export * from './Print';
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
118
packages/framework/blueprint/src/nodes/events/EventCollision.ts
Normal file
118
packages/framework/blueprint/src/nodes/events/EventCollision.ts
Normal 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: ''
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
79
packages/framework/blueprint/src/nodes/events/EventInput.ts
Normal file
79
packages/framework/blueprint/src/nodes/events/EventInput.ts
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
132
packages/framework/blueprint/src/nodes/events/EventState.ts
Normal file
132
packages/framework/blueprint/src/nodes/events/EventState.ts
Normal 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 ?? ''
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
50
packages/framework/blueprint/src/nodes/events/EventTick.ts
Normal file
50
packages/framework/blueprint/src/nodes/events/EventTick.ts
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
70
packages/framework/blueprint/src/nodes/events/EventTimer.ts
Normal file
70
packages/framework/blueprint/src/nodes/events/EventTimer.ts
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
16
packages/framework/blueprint/src/nodes/events/index.ts
Normal file
16
packages/framework/blueprint/src/nodes/events/index.ts
Normal 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';
|
||||
11
packages/framework/blueprint/src/nodes/index.ts
Normal file
11
packages/framework/blueprint/src/nodes/index.ts
Normal 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';
|
||||
122
packages/framework/blueprint/src/nodes/math/MathOperations.ts
Normal file
122
packages/framework/blueprint/src/nodes/math/MathOperations.ts
Normal 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 } };
|
||||
}
|
||||
}
|
||||
6
packages/framework/blueprint/src/nodes/math/index.ts
Normal file
6
packages/framework/blueprint/src/nodes/math/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Math Nodes - Mathematical operation nodes
|
||||
* 数学节点 - 数学运算节点
|
||||
*/
|
||||
|
||||
export * from './MathOperations';
|
||||
57
packages/framework/blueprint/src/nodes/time/Delay.ts
Normal file
57
packages/framework/blueprint/src/nodes/time/Delay.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
45
packages/framework/blueprint/src/nodes/time/GetDeltaTime.ts
Normal file
45
packages/framework/blueprint/src/nodes/time/GetDeltaTime.ts
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
45
packages/framework/blueprint/src/nodes/time/GetTime.ts
Normal file
45
packages/framework/blueprint/src/nodes/time/GetTime.ts
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
8
packages/framework/blueprint/src/nodes/time/index.ts
Normal file
8
packages/framework/blueprint/src/nodes/time/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Time Nodes - Time-related utility nodes
|
||||
* 时间节点 - 时间相关的工具节点
|
||||
*/
|
||||
|
||||
export * from './GetDeltaTime';
|
||||
export * from './GetTime';
|
||||
export * from './Delay';
|
||||
116
packages/framework/blueprint/src/runtime/BlueprintComponent.ts
Normal file
116
packages/framework/blueprint/src/runtime/BlueprintComponent.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
121
packages/framework/blueprint/src/runtime/BlueprintSystem.ts
Normal file
121
packages/framework/blueprint/src/runtime/BlueprintSystem.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
336
packages/framework/blueprint/src/runtime/BlueprintVM.ts
Normal file
336
packages/framework/blueprint/src/runtime/BlueprintVM.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
270
packages/framework/blueprint/src/runtime/ExecutionContext.ts
Normal file
270
packages/framework/blueprint/src/runtime/ExecutionContext.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
151
packages/framework/blueprint/src/runtime/NodeRegistry.ts
Normal file
151
packages/framework/blueprint/src/runtime/NodeRegistry.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
10
packages/framework/blueprint/src/runtime/index.ts
Normal file
10
packages/framework/blueprint/src/runtime/index.ts
Normal 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';
|
||||
22
packages/framework/blueprint/src/tokens.ts
Normal file
22
packages/framework/blueprint/src/tokens.ts
Normal 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 {};
|
||||
497
packages/framework/blueprint/src/triggers/BlueprintTrigger.ts
Normal file
497
packages/framework/blueprint/src/triggers/BlueprintTrigger.ts
Normal 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
|
||||
});
|
||||
}
|
||||
479
packages/framework/blueprint/src/triggers/TriggerCondition.ts
Normal file
479
packages/framework/blueprint/src/triggers/TriggerCondition.ts
Normal 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();
|
||||
}
|
||||
461
packages/framework/blueprint/src/triggers/TriggerDispatcher.ts
Normal file
461
packages/framework/blueprint/src/triggers/TriggerDispatcher.ts
Normal 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);
|
||||
}
|
||||
400
packages/framework/blueprint/src/triggers/TriggerTypes.ts
Normal file
400
packages/framework/blueprint/src/triggers/TriggerTypes.ts
Normal 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
|
||||
};
|
||||
}
|
||||
105
packages/framework/blueprint/src/triggers/index.ts
Normal file
105
packages/framework/blueprint/src/triggers/index.ts
Normal 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';
|
||||
125
packages/framework/blueprint/src/types/blueprint.ts
Normal file
125
packages/framework/blueprint/src/types/blueprint.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
3
packages/framework/blueprint/src/types/index.ts
Normal file
3
packages/framework/blueprint/src/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './pins';
|
||||
export * from './nodes';
|
||||
export * from './blueprint';
|
||||
138
packages/framework/blueprint/src/types/nodes.ts
Normal file
138
packages/framework/blueprint/src/types/nodes.ts
Normal 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;
|
||||
}
|
||||
135
packages/framework/blueprint/src/types/pins.ts
Normal file
135
packages/framework/blueprint/src/types/pins.ts
Normal 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;
|
||||
}
|
||||
23
packages/framework/blueprint/tsconfig.build.json
Normal file
23
packages/framework/blueprint/tsconfig.build.json
Normal 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"]
|
||||
}
|
||||
31
packages/framework/blueprint/tsconfig.json
Normal file
31
packages/framework/blueprint/tsconfig.json
Normal 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" }
|
||||
]
|
||||
}
|
||||
13
packages/framework/blueprint/tsup.config.ts
Normal file
13
packages/framework/blueprint/tsup.config.ts
Normal 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'
|
||||
});
|
||||
Reference in New Issue
Block a user