Feature/physics and tilemap enhancement (#247)

* feat(behavior-tree,tilemap): 修复编辑器连线缩放问题并增强插件系统

* feat(node-editor,blueprint): 新增通用节点编辑器和蓝图可视化脚本系统

* feat(editor,tilemap): 优化编辑器UI样式和Tilemap编辑器功能

* fix: 修复CodeQL安全警告和CI类型检查错误

* fix: 修复CodeQL安全警告和CI类型检查错误

* fix: 修复CodeQL安全警告和CI类型检查错误
This commit is contained in:
YHH
2025-11-29 23:00:48 +08:00
committed by GitHub
parent f03b73b58e
commit 359886c72f
198 changed files with 33879 additions and 13121 deletions

View File

@@ -0,0 +1,180 @@
/**
* Blueprint Editor Plugin - Integrates blueprint editor with the editor
* 蓝图编辑器插件 - 将蓝图编辑器与编辑器集成
*/
import {
type ServiceContainer,
type IPluginLoader,
type IEditorModuleLoader,
type PluginDescriptor,
type PanelDescriptor,
type MenuItemDescriptor,
type FileActionHandler,
type FileCreationTemplate,
PanelPosition,
FileSystem,
createLogger,
MessageHub,
IMessageHub
} from '@esengine/editor-runtime';
import { BlueprintEditorPanel } from './components/BlueprintEditorPanel';
import { useBlueprintEditorStore } from './stores/blueprintEditorStore';
import { createEmptyBlueprint, validateBlueprintAsset } from '../types/blueprint';
const logger = createLogger('BlueprintEditorModule');
/**
* Blueprint 编辑器模块
* Blueprint editor module
*/
class BlueprintEditorModule implements IEditorModuleLoader {
private services?: ServiceContainer;
async install(services: ServiceContainer): Promise<void> {
this.services = services;
logger.info('Blueprint editor module installed');
}
async uninstall(): Promise<void> {
logger.info('Blueprint editor module uninstalled');
}
getPanels(): PanelDescriptor[] {
return [
{
id: 'panel-blueprint-editor',
title: 'Blueprint Editor',
position: PanelPosition.Center,
defaultSize: 800,
resizable: true,
closable: true,
icon: 'Workflow',
order: 20,
isDynamic: true,
component: BlueprintEditorPanel
}
];
}
getMenuItems(): MenuItemDescriptor[] {
return [
{
id: 'blueprint-new',
label: 'New Blueprint',
parentId: 'file',
shortcut: 'Ctrl+Shift+B',
execute: () => {
useBlueprintEditorStore.getState().createNewBlueprint('New Blueprint');
}
},
{
id: 'view-blueprint-editor',
label: 'Blueprint Editor',
parentId: 'view',
shortcut: 'Ctrl+B'
}
];
}
getFileActionHandlers(): FileActionHandler[] {
const services = this.services;
return [
{
extensions: ['bp'],
onDoubleClick: async (filePath: string) => {
try {
// 使用 FileSystem API 读取文件
const content = await FileSystem.readTextFile(filePath);
const data = JSON.parse(content);
if (validateBlueprintAsset(data)) {
useBlueprintEditorStore.getState().loadBlueprint(data, filePath);
logger.info('Loaded blueprint:', filePath);
// 打开蓝图编辑器面板
if (services) {
const messageHub = services.resolve<MessageHub>(IMessageHub);
if (messageHub) {
const fileName = filePath.split(/[\\/]/).pop() || 'Blueprint';
messageHub.publish('dynamic-panel:open', {
panelId: 'panel-blueprint-editor',
title: `Blueprint - ${fileName}`
});
}
}
} else {
logger.error('Invalid blueprint file:', filePath);
}
} catch (error) {
logger.error('Failed to load blueprint:', error);
}
}
}
];
}
getFileCreationTemplates(): FileCreationTemplate[] {
return [
{
id: 'create-blueprint',
label: 'Blueprint',
extension: 'bp',
icon: 'Workflow',
category: 'scripting',
getContent: (fileName: string) => {
const name = fileName.replace('.bp', '');
const blueprint = createEmptyBlueprint(name);
return JSON.stringify(blueprint, null, 2);
}
}
];
}
async onEditorReady(): Promise<void> {
logger.info('Editor ready');
}
async onProjectOpen(_projectPath: string): Promise<void> {
logger.info('Project opened');
}
async onProjectClose(): Promise<void> {
useBlueprintEditorStore.getState().createNewBlueprint('New Blueprint');
logger.info('Project closed');
}
}
/**
* Plugin descriptor
* 插件描述符
*/
const descriptor: PluginDescriptor = {
id: '@esengine/blueprint',
name: 'Blueprint Visual Scripting',
version: '1.0.0',
description: 'Visual scripting system for creating game logic without code',
category: 'scripting',
icon: 'Workflow',
enabledByDefault: true,
canContainContent: true,
isEnginePlugin: true,
isCore: false,
modules: [
{
name: 'BlueprintEditor',
type: 'editor',
loadingPhase: 'default',
panels: ['panel-blueprint-editor']
}
]
};
/**
* Blueprint Plugin Export
* 蓝图插件导出
*/
export const BlueprintPlugin: IPluginLoader = {
descriptor,
editorModule: new BlueprintEditorModule()
};

View File

@@ -0,0 +1,383 @@
/**
* Blueprint Canvas - Main canvas for editing blueprints using NodeEditor
* 蓝图画布 - 使用 NodeEditor 编辑蓝图的主画布
*/
import React, { useCallback, useMemo, useState } from 'react';
import {
NodeEditor,
Graph,
GraphNode,
Position,
Connection,
NodeContextMenu,
ConfirmDialog,
type NodeTemplate,
type NodeCategory,
type PinCategory
} from '@esengine/node-editor';
import { useBlueprintEditorStore } from '../stores/blueprintEditorStore';
import { NodeRegistry } from '../../runtime/NodeRegistry';
import type { BlueprintNode, BlueprintConnection, BlueprintNodeTemplate } from '../../types/nodes';
import type { BlueprintPinDefinition } from '../../types/pins';
interface ContextMenuState {
isOpen: boolean;
screenPosition: { x: number; y: number };
canvasPosition: Position;
}
interface DeleteDialogState {
isOpen: boolean;
nodeId: string;
nodeTitle: string;
}
/**
* Map blueprint pin type to node-editor PinCategory
*/
function mapPinCategory(type: string): PinCategory {
switch (type) {
case 'exec':
return 'exec';
case 'boolean':
case 'bool':
return 'bool';
case 'integer':
case 'int':
return 'int';
case 'float':
case 'number':
return 'float';
case 'string':
return 'string';
case 'vector2':
return 'vector2';
case 'vector3':
return 'vector3';
case 'vector4':
return 'vector4';
case 'color':
return 'color';
case 'object':
case 'reference':
return 'object';
case 'array':
return 'array';
case 'struct':
return 'struct';
case 'enum':
return 'enum';
default:
return 'any';
}
}
/**
* Map blueprint category to node-editor NodeCategory
*/
function mapNodeCategory(category?: string): NodeCategory {
switch (category) {
case 'event':
return 'event';
case 'function':
return 'function';
case 'pure':
return 'pure';
case 'flow':
return 'flow';
case 'variable':
return 'variable';
case 'literal':
return 'literal';
case 'comment':
return 'comment';
default:
return 'function';
}
}
/**
* Convert blueprint node template to node-editor template
*/
function convertNodeTemplate(bpTemplate: BlueprintNodeTemplate): NodeTemplate {
return {
id: bpTemplate.type,
title: bpTemplate.title,
category: mapNodeCategory(bpTemplate.category),
icon: bpTemplate.icon,
inputPins: bpTemplate.inputs.map((p: BlueprintPinDefinition) => ({
name: p.name,
displayName: p.displayName || p.name,
category: mapPinCategory(p.type),
defaultValue: p.defaultValue
})),
outputPins: bpTemplate.outputs.map((p: BlueprintPinDefinition) => ({
name: p.name,
displayName: p.displayName || p.name,
category: mapPinCategory(p.type)
}))
};
}
/**
* Convert blueprint node to graph node
*/
function convertToGraphNode(node: BlueprintNode): GraphNode | null {
const bpTemplate = NodeRegistry.instance.getTemplate(node.type);
if (!bpTemplate) return null;
const template = convertNodeTemplate(bpTemplate);
return new GraphNode(
node.id,
template,
new Position(node.position.x, node.position.y),
node.data
);
}
/**
* Convert blueprint connection to graph connection
*/
function convertToGraphConnection(
conn: BlueprintConnection,
nodes: BlueprintNode[],
graphNodes: GraphNode[]
): Connection | null {
const fromNode = nodes.find(n => n.id === conn.fromNodeId);
const toNode = nodes.find(n => n.id === conn.toNodeId);
if (!fromNode || !toNode) return null;
const fromTemplate = NodeRegistry.instance.getTemplate(fromNode.type);
if (!fromTemplate) return null;
const fromPin = fromTemplate.outputs.find(p => p.name === conn.fromPin);
if (!fromPin) return null;
// Find graph nodes to get the actual pin IDs
const fromGraphNode = graphNodes.find(n => n.id === conn.fromNodeId);
const toGraphNode = graphNodes.find(n => n.id === conn.toNodeId);
if (!fromGraphNode || !toGraphNode) return null;
// Find pins by name
const fromGraphPin = fromGraphNode.outputPins.find(p => p.name === conn.fromPin);
const toGraphPin = toGraphNode.inputPins.find(p => p.name === conn.toPin);
if (!fromGraphPin || !toGraphPin) return null;
return new Connection(
conn.id,
conn.fromNodeId,
fromGraphPin.id,
conn.toNodeId,
toGraphPin.id,
mapPinCategory(fromPin.type)
);
}
/**
* Blueprint Canvas Component using NodeEditor
*/
export const BlueprintCanvas: React.FC = () => {
const {
blueprint,
selectedNodeIds,
selectNodes,
updateNodePosition,
addNode,
addConnection,
removeNode,
removeConnection
} = useBlueprintEditorStore();
const [selectedConnections, setSelectedConnections] = useState<Set<string>>(new Set());
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
isOpen: false,
screenPosition: { x: 0, y: 0 },
canvasPosition: new Position(0, 0)
});
const [deleteDialog, setDeleteDialog] = useState<DeleteDialogState>({
isOpen: false,
nodeId: '',
nodeTitle: ''
});
// Convert blueprint to Graph
const graph = useMemo(() => {
if (!blueprint) return Graph.empty('blueprint', 'Blueprint');
const graphNodes: GraphNode[] = [];
for (const node of blueprint.nodes) {
const graphNode = convertToGraphNode(node);
if (graphNode) {
graphNodes.push(graphNode);
}
}
const graphConnections: Connection[] = [];
for (const conn of blueprint.connections) {
const graphConn = convertToGraphConnection(conn, blueprint.nodes, graphNodes);
if (graphConn) {
graphConnections.push(graphConn);
}
}
return new Graph('blueprint', blueprint.metadata.name, graphNodes, graphConnections);
}, [blueprint]);
// Handle graph changes
const handleGraphChange = useCallback((newGraph: Graph) => {
if (!blueprint) return;
// Update node positions
for (const graphNode of newGraph.nodes) {
const oldNode = blueprint.nodes.find(n => n.id === graphNode.id);
if (oldNode) {
if (oldNode.position.x !== graphNode.position.x || oldNode.position.y !== graphNode.position.y) {
updateNodePosition(graphNode.id, graphNode.position.x, graphNode.position.y);
}
}
}
// Handle new connections
for (const graphConn of newGraph.connections) {
const exists = blueprint.connections.some(c => c.id === graphConn.id);
if (!exists) {
// Extract pin names from graph connection
const fromNode = newGraph.getNode(graphConn.fromNodeId);
const toNode = newGraph.getNode(graphConn.toNodeId);
if (fromNode && toNode) {
const fromPin = fromNode.outputPins.find(p => p.id === graphConn.fromPinId);
const toPin = toNode.inputPins.find(p => p.id === graphConn.toPinId);
if (fromPin && toPin) {
addConnection({
id: graphConn.id,
fromNodeId: graphConn.fromNodeId,
fromPin: fromPin.name,
toNodeId: graphConn.toNodeId,
toPin: toPin.name
});
}
}
}
}
// Handle removed connections
for (const oldConn of blueprint.connections) {
const exists = newGraph.connections.some(c => c.id === oldConn.id);
if (!exists) {
removeConnection(oldConn.id);
}
}
}, [blueprint, updateNodePosition, addConnection, removeConnection]);
// Handle selection changes
const handleSelectionChange = useCallback((nodeIds: Set<string>, connectionIds: Set<string>) => {
selectNodes(Array.from(nodeIds));
setSelectedConnections(connectionIds);
}, [selectNodes]);
// Handle canvas context menu - open node selection menu
const handleCanvasContextMenu = useCallback((position: Position, e: React.MouseEvent) => {
setContextMenu({
isOpen: true,
screenPosition: { x: e.clientX, y: e.clientY },
canvasPosition: position
});
}, []);
// Handle template selection from context menu
const handleSelectTemplate = useCallback((template: NodeTemplate, position: Position) => {
addNode({
id: '',
type: template.id,
position: { x: position.x, y: position.y },
data: {}
});
}, [addNode]);
// Close context menu
const handleCloseContextMenu = useCallback(() => {
setContextMenu(prev => ({ ...prev, isOpen: false }));
}, []);
// Handle node context menu
const handleNodeContextMenu = useCallback((node: GraphNode, e: React.MouseEvent) => {
e.preventDefault();
setDeleteDialog({
isOpen: true,
nodeId: node.id,
nodeTitle: node.title
});
}, []);
// Handle delete confirmation
const handleConfirmDelete = useCallback(() => {
if (deleteDialog.nodeId) {
removeNode(deleteDialog.nodeId);
}
setDeleteDialog({ isOpen: false, nodeId: '', nodeTitle: '' });
}, [deleteDialog.nodeId, removeNode]);
// Handle delete cancel
const handleCancelDelete = useCallback(() => {
setDeleteDialog({ isOpen: false, nodeId: '', nodeTitle: '' });
}, []);
// Get available templates
const templates = useMemo(() => {
const allTemplates = NodeRegistry.instance.getAllTemplates();
return allTemplates.map(t => convertNodeTemplate(t));
}, []);
if (!blueprint) {
return (
<div style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#1a1a2e',
color: '#666'
}}>
<div style={{ textAlign: 'center' }}>
<p style={{ fontSize: '16px', marginBottom: '8px' }}>No blueprint loaded</p>
<p style={{ fontSize: '12px', opacity: 0.7 }}>Create a new blueprint or open an existing one</p>
</div>
</div>
);
}
return (
<>
<NodeEditor
graph={graph}
templates={templates}
selectedNodeIds={new Set(selectedNodeIds)}
selectedConnectionIds={selectedConnections}
onGraphChange={handleGraphChange}
onSelectionChange={handleSelectionChange}
onCanvasContextMenu={handleCanvasContextMenu}
onNodeContextMenu={handleNodeContextMenu}
/>
<NodeContextMenu
isOpen={contextMenu.isOpen}
position={contextMenu.screenPosition}
canvasPosition={contextMenu.canvasPosition}
templates={templates}
onSelectTemplate={handleSelectTemplate}
onClose={handleCloseContextMenu}
/>
<ConfirmDialog
isOpen={deleteDialog.isOpen}
title="Delete Node"
message={`Are you sure you want to delete "${deleteDialog.nodeTitle}"?`}
confirmText="Delete"
cancelText="Cancel"
type="danger"
onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete}
/>
</>
);
};

View File

@@ -0,0 +1,48 @@
/**
* Blueprint Editor Panel - Main panel for blueprint editing
* 蓝图编辑器面板 - 蓝图编辑的主面板
*/
import React, { useEffect } from 'react';
import { BlueprintCanvas } from './BlueprintCanvas';
import { useBlueprintEditorStore } from '../stores/blueprintEditorStore';
// Import nodes to register them
// 导入节点以注册它们
import '../../nodes';
/**
* Panel container styles
* 面板容器样式
*/
const panelStyles: React.CSSProperties = {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
backgroundColor: '#1a1a2e',
color: '#fff',
overflow: 'hidden'
};
/**
* Blueprint Editor Panel Component
* 蓝图编辑器面板组件
*/
export const BlueprintEditorPanel: React.FC = () => {
const { blueprint, createNewBlueprint } = useBlueprintEditorStore();
// Create a default blueprint if none exists
// 如果不存在则创建默认蓝图
useEffect(() => {
if (!blueprint) {
createNewBlueprint('New Blueprint');
}
}, [blueprint, createNewBlueprint]);
return (
<div style={panelStyles}>
<BlueprintCanvas />
</div>
);
};

View File

@@ -0,0 +1,7 @@
/**
* Blueprint Editor Components
* 蓝图编辑器组件
*/
export * from './BlueprintCanvas';
export * from './BlueprintEditorPanel';

View File

@@ -0,0 +1,8 @@
/**
* Blueprint Editor Module
* 蓝图编辑器模块
*/
export * from './components';
export * from './stores';
export * from './BlueprintPlugin';

View File

@@ -0,0 +1,256 @@
/**
* Blueprint Editor Store - State management for blueprint editor
* 蓝图编辑器状态管理
*/
import { create } from 'zustand';
import { BlueprintAsset, createEmptyBlueprint } from '../../types/blueprint';
import { BlueprintNode, BlueprintConnection } from '../../types/nodes';
/**
* Blueprint editor state interface
* 蓝图编辑器状态接口
*/
interface BlueprintEditorState {
/** Current blueprint being edited (当前编辑的蓝图) */
blueprint: BlueprintAsset | null;
/** Selected node IDs (选中的节点ID) */
selectedNodeIds: string[];
/** Currently dragging node (当前拖拽的节点) */
draggingNodeId: string | null;
/** Canvas pan offset (画布平移偏移) */
panOffset: { x: number; y: number };
/** Canvas zoom level (画布缩放级别) */
zoom: number;
/** Whether the blueprint has unsaved changes (是否有未保存的更改) */
isDirty: boolean;
/** Current file path if saved (当前文件路径) */
filePath: string | null;
// Actions (操作)
/** Create new blueprint (创建新蓝图) */
createNewBlueprint: (name: string) => void;
/** Load blueprint from asset (从资产加载蓝图) */
loadBlueprint: (asset: BlueprintAsset, filePath?: string) => void;
/** Add a node (添加节点) */
addNode: (node: BlueprintNode) => void;
/** Remove a node (移除节点) */
removeNode: (nodeId: string) => void;
/** Update node position (更新节点位置) */
updateNodePosition: (nodeId: string, x: number, y: number) => void;
/** Update node data (更新节点数据) */
updateNodeData: (nodeId: string, data: Record<string, unknown>) => void;
/** Add connection (添加连接) */
addConnection: (connection: BlueprintConnection) => void;
/** Remove connection (移除连接) */
removeConnection: (connectionId: string) => void;
/** Select nodes (选择节点) */
selectNodes: (nodeIds: string[]) => void;
/** Clear selection (清除选择) */
clearSelection: () => void;
/** Set pan offset (设置平移偏移) */
setPanOffset: (x: number, y: number) => void;
/** Set zoom level (设置缩放级别) */
setZoom: (zoom: number) => void;
/** Mark as dirty (标记为已修改) */
markDirty: () => void;
/** Mark as clean (标记为未修改) */
markClean: () => void;
}
/**
* Generate unique ID for nodes and connections
* 为节点和连接生成唯一ID
*/
function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Blueprint editor store
* 蓝图编辑器状态存储
*/
export const useBlueprintEditorStore = create<BlueprintEditorState>((set, get) => ({
blueprint: null,
selectedNodeIds: [],
draggingNodeId: null,
panOffset: { x: 0, y: 0 },
zoom: 1,
isDirty: false,
filePath: null,
createNewBlueprint: (name: string) => {
const blueprint = createEmptyBlueprint(name);
set({
blueprint,
selectedNodeIds: [],
panOffset: { x: 0, y: 0 },
zoom: 1,
isDirty: false,
filePath: null
});
},
loadBlueprint: (asset: BlueprintAsset, filePath?: string) => {
set({
blueprint: asset,
selectedNodeIds: [],
panOffset: { x: 0, y: 0 },
zoom: 1,
isDirty: false,
filePath: filePath ?? null
});
},
addNode: (node: BlueprintNode) => {
const { blueprint } = get();
if (!blueprint) return;
const newNode = { ...node, id: node.id || generateId() };
set({
blueprint: {
...blueprint,
nodes: [...blueprint.nodes, newNode],
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
},
isDirty: true
});
},
removeNode: (nodeId: string) => {
const { blueprint } = get();
if (!blueprint) return;
set({
blueprint: {
...blueprint,
nodes: blueprint.nodes.filter(n => n.id !== nodeId),
connections: blueprint.connections.filter(
c => c.fromNodeId !== nodeId && c.toNodeId !== nodeId
),
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
},
selectedNodeIds: get().selectedNodeIds.filter(id => id !== nodeId),
isDirty: true
});
},
updateNodePosition: (nodeId: string, x: number, y: number) => {
const { blueprint } = get();
if (!blueprint) return;
set({
blueprint: {
...blueprint,
nodes: blueprint.nodes.map(n =>
n.id === nodeId ? { ...n, position: { x, y } } : n
),
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
},
isDirty: true
});
},
updateNodeData: (nodeId: string, data: Record<string, unknown>) => {
const { blueprint } = get();
if (!blueprint) return;
set({
blueprint: {
...blueprint,
nodes: blueprint.nodes.map(n =>
n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n
),
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
},
isDirty: true
});
},
addConnection: (connection: BlueprintConnection) => {
const { blueprint } = get();
if (!blueprint) return;
const newConnection = { ...connection, id: connection.id || generateId() };
// Check for existing connection to the same input pin
// 检查是否已存在到同一输入引脚的连接
const existingIndex = blueprint.connections.findIndex(
c => c.toNodeId === connection.toNodeId && c.toPin === connection.toPin
);
const newConnections = [...blueprint.connections];
if (existingIndex >= 0) {
// Replace existing connection (替换现有连接)
newConnections[existingIndex] = newConnection;
} else {
newConnections.push(newConnection);
}
set({
blueprint: {
...blueprint,
connections: newConnections,
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
},
isDirty: true
});
},
removeConnection: (connectionId: string) => {
const { blueprint } = get();
if (!blueprint) return;
set({
blueprint: {
...blueprint,
connections: blueprint.connections.filter(c => c.id !== connectionId),
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
},
isDirty: true
});
},
selectNodes: (nodeIds: string[]) => {
set({ selectedNodeIds: nodeIds });
},
clearSelection: () => {
set({ selectedNodeIds: [] });
},
setPanOffset: (x: number, y: number) => {
set({ panOffset: { x, y } });
},
setZoom: (zoom: number) => {
set({ zoom: Math.max(0.1, Math.min(2, zoom)) });
},
markDirty: () => {
set({ isDirty: true });
},
markClean: () => {
set({ isDirty: false });
}
}));

View File

@@ -0,0 +1,6 @@
/**
* Blueprint Editor Stores
* 蓝图编辑器状态存储
*/
export * from './blueprintEditorStore';

View File

@@ -0,0 +1,31 @@
/**
* @esengine/blueprint - Visual scripting system for ECS Framework
* 蓝图可视化脚本系统
*/
// Types
export * from './types';
// Runtime
export * from './runtime';
// Nodes (import to register)
import './nodes';
// Re-export commonly used items
export { NodeRegistry, RegisterNode } from './runtime/NodeRegistry';
export { BlueprintVM } from './runtime/BlueprintVM';
export {
createBlueprintComponentData,
initializeBlueprintVM,
startBlueprint,
stopBlueprint,
tickBlueprint,
cleanupBlueprint
} from './runtime/BlueprintComponent';
export {
createBlueprintSystem,
triggerBlueprintEvent,
triggerCustomBlueprintEvent
} from './runtime/BlueprintSystem';
export { createEmptyBlueprint, validateBlueprintAsset } from './types/blueprint';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
/**
* Event Nodes - Entry points for blueprint execution
* 事件节点 - 蓝图执行的入口点
*/
export * from './EventBeginPlay';
export * from './EventTick';
export * from './EventEndPlay';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,116 @@
/**
* Blueprint Component - Attaches a blueprint to an entity
* 蓝图组件 - 将蓝图附加到实体
*/
import { BlueprintAsset } from '../types/blueprint';
import { BlueprintVM } from './BlueprintVM';
import { IEntity, IScene } from './ExecutionContext';
/**
* 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: IEntity,
scene: IScene
): void {
if (!component.blueprintAsset) {
return;
}
// Create VM instance
// 创建 VM 实例
component.vm = new BlueprintVM(component.blueprintAsset, entity, scene);
component.vm.debug = component.debug;
}
/**
* Start blueprint execution
* 开始蓝图执行
*/
export function startBlueprint(component: IBlueprintComponent): void {
if (component.vm && !component.isStarted) {
component.vm.start();
component.isStarted = true;
}
}
/**
* Stop blueprint execution
* 停止蓝图执行
*/
export function stopBlueprint(component: IBlueprintComponent): void {
if (component.vm && component.isStarted) {
component.vm.stop();
component.isStarted = false;
}
}
/**
* Update blueprint execution
* 更新蓝图执行
*/
export function tickBlueprint(component: IBlueprintComponent, deltaTime: number): void {
if (component.vm && component.isStarted) {
component.vm.tick(deltaTime);
}
}
/**
* Clean up blueprint resources
* 清理蓝图资源
*/
export function cleanupBlueprint(component: IBlueprintComponent): void {
if (component.vm) {
if (component.isStarted) {
component.vm.stop();
}
component.vm = null;
component.isStarted = false;
}
}

View File

@@ -0,0 +1,121 @@
/**
* Blueprint Execution System - Manages blueprint lifecycle and execution
* 蓝图执行系统 - 管理蓝图生命周期和执行
*/
import {
IBlueprintComponent,
initializeBlueprintVM,
startBlueprint,
tickBlueprint,
cleanupBlueprint
} from './BlueprintComponent';
import { IEntity, IScene } from './ExecutionContext';
/**
* 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 IEntity {
/** Blueprint component data (蓝图组件数据) */
blueprintComponent: IBlueprintComponent;
}
/**
* Creates a blueprint execution system
* 创建蓝图执行系统
*/
export function createBlueprintSystem(scene: IScene): IBlueprintSystem {
return {
process(entities: IBlueprintEntity[], deltaTime: number): void {
for (const entity of entities) {
const component = entity.blueprintComponent;
// Skip if no blueprint asset loaded
// 如果没有加载蓝图资产则跳过
if (!component.blueprintAsset) {
continue;
}
// Initialize VM if needed
// 如果需要则初始化 VM
if (!component.vm) {
initializeBlueprintVM(component, entity, scene);
}
// Auto-start if enabled
// 如果启用则自动启动
if (component.autoStart && !component.isStarted) {
startBlueprint(component);
}
// Tick the blueprint
// 更新蓝图
tickBlueprint(component, deltaTime);
}
},
onEntityAdded(entity: IBlueprintEntity): void {
const component = entity.blueprintComponent;
if (component.blueprintAsset) {
initializeBlueprintVM(component, entity, scene);
if (component.autoStart) {
startBlueprint(component);
}
}
},
onEntityRemoved(entity: IBlueprintEntity): void {
cleanupBlueprint(entity.blueprintComponent);
}
};
}
/**
* Utility to manually trigger blueprint events
* 手动触发蓝图事件的工具
*/
export function triggerBlueprintEvent(
entity: IBlueprintEntity,
eventType: string,
data?: Record<string, unknown>
): void {
const vm = entity.blueprintComponent.vm;
if (vm && entity.blueprintComponent.isStarted) {
vm.triggerEvent(eventType, data);
}
}
/**
* Utility to trigger custom events by name
* 按名称触发自定义事件的工具
*/
export function triggerCustomBlueprintEvent(
entity: IBlueprintEntity,
eventName: string,
data?: Record<string, unknown>
): void {
const vm = entity.blueprintComponent.vm;
if (vm && entity.blueprintComponent.isStarted) {
vm.triggerCustomEvent(eventName, data);
}
}

View File

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

View File

@@ -0,0 +1,294 @@
/**
* Execution Context - Runtime context for blueprint execution
* 执行上下文 - 蓝图执行的运行时上下文
*/
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;
}
/**
* Entity interface (minimal for decoupling)
* 实体接口(最小化以解耦)
*/
export interface IEntity {
id: number;
name: string;
active: boolean;
getComponent<T>(type: new (...args: unknown[]) => T): T | null;
addComponent<T>(component: T): T;
removeComponent<T>(type: new (...args: unknown[]) => T): void;
hasComponent<T>(type: new (...args: unknown[]) => T): boolean;
}
/**
* Scene interface (minimal for decoupling)
* 场景接口(最小化以解耦)
*/
export interface IScene {
createEntity(name?: string): IEntity;
destroyEntity(entity: IEntity): void;
findEntityByName(name: string): IEntity | null;
findEntitiesByTag(tag: number): IEntity[];
}
/**
* Execution context provides access to runtime services
* 执行上下文提供对运行时服务的访问
*/
export class ExecutionContext {
/** Current blueprint asset (当前蓝图资产) */
readonly blueprint: BlueprintAsset;
/** Owner entity (所有者实体) */
readonly entity: IEntity;
/** 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: IEntity, scene: IScene) {
this.blueprint = blueprint;
this.entity = entity;
this.scene = scene;
// Initialize instance variables with defaults
// 使用默认值初始化实例变量
for (const variable of blueprint.variables) {
if (variable.scope === 'instance') {
this._instanceVariables.set(variable.name, variable.defaultValue);
}
}
// Build connection lookup maps
// 构建连接查找映射
this._buildConnectionMaps();
}
private _buildConnectionMaps(): void {
for (const conn of this.blueprint.connections) {
// By target
const targetKey = `${conn.toNodeId}.${conn.toPin}`;
if (!this._connectionsByTarget.has(targetKey)) {
this._connectionsByTarget.set(targetKey, []);
}
this._connectionsByTarget.get(targetKey)!.push(conn);
// By source
const sourceKey = `${conn.fromNodeId}.${conn.fromPin}`;
if (!this._connectionsBySource.has(sourceKey)) {
this._connectionsBySource.set(sourceKey, []);
}
this._connectionsBySource.get(sourceKey)!.push(conn);
}
}
/**
* Get a node by ID
* 通过ID获取节点
*/
getNode(nodeId: string): BlueprintNode | undefined {
return this.blueprint.nodes.find(n => n.id === nodeId);
}
/**
* Get connections to a target pin
* 获取到目标引脚的连接
*/
getConnectionsToPin(nodeId: string, pinName: string): BlueprintConnection[] {
return this._connectionsByTarget.get(`${nodeId}.${pinName}`) ?? [];
}
/**
* Get connections from a source pin
* 获取从源引脚的连接
*/
getConnectionsFromPin(nodeId: string, pinName: string): BlueprintConnection[] {
return this._connectionsBySource.get(`${nodeId}.${pinName}`) ?? [];
}
/**
* Evaluate an input pin value (follows connections or uses default)
* 计算输入引脚值(跟随连接或使用默认值)
*/
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown {
const connections = this.getConnectionsToPin(nodeId, pinName);
if (connections.length === 0) {
// Use default from node data or provided default
// 使用节点数据的默认值或提供的默认值
const node = this.getNode(nodeId);
return node?.data[pinName] ?? defaultValue;
}
// Get value from connected output
// 从连接的输出获取值
const conn = connections[0];
const cachedOutputs = this._outputCache.get(conn.fromNodeId);
if (cachedOutputs && conn.fromPin in cachedOutputs) {
return cachedOutputs[conn.fromPin];
}
// Need to execute the source node first (lazy evaluation)
// 需要先执行源节点(延迟求值)
return defaultValue;
}
/**
* Set output values for a node (cached for current execution)
* 设置节点的输出值(为当前执行缓存)
*/
setOutputs(nodeId: string, outputs: Record<string, unknown>): void {
this._outputCache.set(nodeId, outputs);
}
/**
* Get cached outputs for a node
* 获取节点的缓存输出
*/
getOutputs(nodeId: string): Record<string, unknown> | undefined {
return this._outputCache.get(nodeId);
}
/**
* Clear output cache (call at start of new execution)
* 清除输出缓存(在新执行开始时调用)
*/
clearOutputCache(): void {
this._outputCache.clear();
this._localVariables.clear();
}
/**
* Get a variable value
* 获取变量值
*/
getVariable(name: string): unknown {
// Check local first, then instance, then global
// 先检查局部,然后实例,然后全局
if (this._localVariables.has(name)) {
return this._localVariables.get(name);
}
if (this._instanceVariables.has(name)) {
return this._instanceVariables.get(name);
}
if (ExecutionContext._globalVariables.has(name)) {
return ExecutionContext._globalVariables.get(name);
}
// Return default from variable definition
// 返回变量定义的默认值
const varDef = this.blueprint.variables.find(v => v.name === name);
return varDef?.defaultValue;
}
/**
* Set a variable value
* 设置变量值
*/
setVariable(name: string, value: unknown): void {
const varDef = this.blueprint.variables.find(v => v.name === name);
if (!varDef) {
// Treat unknown variables as local
// 将未知变量视为局部变量
this._localVariables.set(name, value);
return;
}
switch (varDef.scope) {
case 'local':
this._localVariables.set(name, value);
break;
case 'instance':
this._instanceVariables.set(name, value);
break;
case 'global':
ExecutionContext._globalVariables.set(name, value);
break;
}
}
/**
* Get all instance variables (for serialization)
* 获取所有实例变量(用于序列化)
*/
getInstanceVariables(): Map<string, unknown> {
return new Map(this._instanceVariables);
}
/**
* Set instance variables (for deserialization)
* 设置实例变量(用于反序列化)
*/
setInstanceVariables(variables: Map<string, unknown>): void {
this._instanceVariables = new Map(variables);
}
/**
* Clear global variables (for scene reset)
* 清除全局变量(用于场景重置)
*/
static clearGlobalVariables(): void {
ExecutionContext._globalVariables.clear();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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