import { useState } from 'react'; import { Clipboard, Edit2, Trash2, ChevronDown, ChevronRight, Globe, Save, Folder, FileCode } from 'lucide-react'; import { save } from '@tauri-apps/plugin-dialog'; import { invoke } from '@tauri-apps/api/core'; import { Core } from '@esengine/ecs-framework'; import type { BlackboardValueType } from '@esengine/behavior-tree'; import { GlobalBlackboardService } from '@esengine/behavior-tree'; import { GlobalBlackboardTypeGenerator } from '../generators/GlobalBlackboardTypeGenerator'; import { createLogger } from '@esengine/ecs-framework'; const logger = createLogger('BehaviorTreeBlackboard'); type SimpleBlackboardType = 'number' | 'string' | 'boolean' | 'object'; interface BlackboardVariable { key: string; value: any; type: SimpleBlackboardType; } interface BehaviorTreeBlackboardProps { variables: Record; initialVariables?: Record; globalVariables?: Record; onVariableChange: (key: string, value: any) => void; onVariableAdd: (key: string, value: any, type: SimpleBlackboardType) => void; onVariableDelete: (key: string) => void; onVariableRename?: (oldKey: string, newKey: string) => void; onGlobalVariableChange?: (key: string, value: any) => void; onGlobalVariableAdd?: (key: string, value: any, type: BlackboardValueType) => void; onGlobalVariableDelete?: (key: string) => void; projectPath?: string; hasUnsavedGlobalChanges?: boolean; onSaveGlobal?: () => void; } /** * 行为树黑板变量面板 * * 用于管理和调试行为树运行时的黑板变量 */ export const BehaviorTreeBlackboard: React.FC = ({ variables, initialVariables, globalVariables, onVariableChange, onVariableAdd, onVariableDelete, onVariableRename, onGlobalVariableChange, onGlobalVariableAdd, onGlobalVariableDelete, projectPath, hasUnsavedGlobalChanges, onSaveGlobal }) => { const [viewMode, setViewMode] = useState<'local' | 'global'>('local'); const isModified = (key: string): boolean => { if (!initialVariables) return false; return JSON.stringify(variables[key]) !== JSON.stringify(initialVariables[key]); }; const handleExportTypeScript = async () => { try { const globalBlackboard = Core.services.resolve(GlobalBlackboardService); const config = globalBlackboard.exportConfig(); const tsCode = GlobalBlackboardTypeGenerator.generate(config); const outputPath = await save({ filters: [{ name: 'TypeScript', extensions: ['ts'] }], defaultPath: 'GlobalBlackboard.ts' }); if (outputPath) { await invoke('write_file_content', { path: outputPath, content: tsCode }); logger.info('TypeScript 类型定义已导出', outputPath); } } catch (error) { logger.error('导出 TypeScript 失败', error); } }; const [isAdding, setIsAdding] = useState(false); const [newKey, setNewKey] = useState(''); const [newValue, setNewValue] = useState(''); const [newType, setNewType] = useState('string'); const [editingKey, setEditingKey] = useState(null); const [editingNewKey, setEditingNewKey] = useState(''); const [editValue, setEditValue] = useState(''); const [editType, setEditType] = useState('string'); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); const handleAddVariable = () => { if (!newKey.trim()) return; let parsedValue: any = newValue; if (newType === 'number') { parsedValue = parseFloat(newValue) || 0; } else if (newType === 'boolean') { parsedValue = newValue === 'true'; } else if (newType === 'object') { try { parsedValue = JSON.parse(newValue); } catch { parsedValue = {}; } } if (viewMode === 'global' && onGlobalVariableAdd) { const globalType = newType as BlackboardValueType; onGlobalVariableAdd(newKey, parsedValue, globalType); } else { onVariableAdd(newKey, parsedValue, newType); } setNewKey(''); setNewValue(''); setIsAdding(false); }; const handleStartEdit = (key: string, value: any) => { setEditingKey(key); setEditingNewKey(key); const currentType = getVariableType(value); setEditType(currentType); setEditValue(typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)); }; const handleSaveEdit = (key: string) => { const newKey = editingNewKey.trim(); if (!newKey) return; let parsedValue: any = editValue; if (editType === 'number') { parsedValue = parseFloat(editValue) || 0; } else if (editType === 'boolean') { parsedValue = editValue === 'true' || editValue === '1'; } else if (editType === 'object') { try { parsedValue = JSON.parse(editValue); } catch { return; } } if (viewMode === 'global' && onGlobalVariableChange) { if (newKey !== key && onGlobalVariableDelete) { onGlobalVariableDelete(key); } onGlobalVariableChange(newKey, parsedValue); } else { if (newKey !== key && onVariableRename) { onVariableRename(key, newKey); } onVariableChange(newKey, parsedValue); } setEditingKey(null); }; const toggleGroup = (groupName: string) => { setCollapsedGroups(prev => { const newSet = new Set(prev); if (newSet.has(groupName)) { newSet.delete(groupName); } else { newSet.add(groupName); } return newSet; }); }; const getVariableType = (value: any): BlackboardVariable['type'] => { if (typeof value === 'number') return 'number'; if (typeof value === 'boolean') return 'boolean'; if (typeof value === 'object') return 'object'; return 'string'; }; const currentVariables = viewMode === 'global' ? (globalVariables || {}) : variables; const variableEntries = Object.entries(currentVariables); const currentOnDelete = viewMode === 'global' ? onGlobalVariableDelete : onVariableDelete; const groupedVariables: Record> = variableEntries.reduce((groups, [key, value]) => { const parts = key.split('.'); const groupName = (parts.length > 1 && parts[0]) ? parts[0] : 'default'; const varName = parts.length > 1 ? parts.slice(1).join('.') : key; if (!groups[groupName]) { groups[groupName] = []; } const group = groups[groupName]; if (group) { group.push({ fullKey: key, varName, value }); } return groups; }, {} as Record>); const groupNames = Object.keys(groupedVariables).sort((a, b) => { if (a === 'default') return 1; if (b === 'default') return -1; return a.localeCompare(b); }); return (
{/* 标题栏 */}
Blackboard
{/* 工具栏 */}
{viewMode === 'global' && projectPath ? ( <> .ecs/global-blackboard.json ) : ( {viewMode === 'local' ? '当前行为树的本地变量' : '所有行为树共享的全局变量'} )}
{viewMode === 'global' && onSaveGlobal && ( <> )}
{/* 变量列表 */}
{variableEntries.length === 0 && !isAdding && (
No variables yet. Click "Add" to create one.
)} {groupNames.map(groupName => { const isCollapsed = collapsedGroups.has(groupName); const groupVars = groupedVariables[groupName]; if (!groupVars) return null; return (
{groupName !== 'default' && (
toggleGroup(groupName)} style={{ display: 'flex', alignItems: 'center', gap: '4px', padding: '4px 6px', backgroundColor: '#252525', borderRadius: '3px', cursor: 'pointer', marginBottom: '4px', userSelect: 'none' }} > {isCollapsed ? : } {groupName} ({groupVars.length})
)} {!isCollapsed && groupVars.map(({ fullKey: key, varName, value }) => { const type = getVariableType(value); const isEditing = editingKey === key; const handleDragStart = (e: React.DragEvent) => { const variableData = { variableName: key, variableValue: value, variableType: type }; e.dataTransfer.setData('application/blackboard-variable', JSON.stringify(variableData)); e.dataTransfer.effectAllowed = 'copy'; }; const typeColor = type === 'number' ? '#4ec9b0' : type === 'boolean' ? '#569cd6' : type === 'object' ? '#ce9178' : '#d4d4d4'; const displayValue = type === 'object' ? JSON.stringify(value) : String(value); const truncatedValue = displayValue.length > 30 ? displayValue.substring(0, 30) + '...' : displayValue; return (
{isEditing ? (
Name
setEditingNewKey(e.target.value)} style={{ width: '100%', padding: '4px', marginBottom: '4px', backgroundColor: '#1e1e1e', border: '1px solid #3c3c3c', borderRadius: '2px', color: '#9cdcfe', fontSize: '11px', fontFamily: 'monospace' }} placeholder="Variable name (e.g., player.health)" />
Type
Value