From 5a06f5420bbce05d26ade0a5b807d974b25af8d3 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Wed, 18 Jun 2025 18:21:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E8=A3=85=E9=A5=B0=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=92=8C=E6=9D=A1=E4=BB=B6=E8=A3=85=E9=A5=B0=E5=99=A8?= =?UTF-8?q?=E7=BB=93=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../behavior-tree/composables/useAppState.ts | 11 + .../composables/useBehaviorTreeEditor.ts | 96 +- .../composables/useConditionAttachment.ts | 344 ++++++ .../composables/useNodeOperations.ts | 16 +- .../behavior-tree/data/nodeTemplates.ts | 313 +++-- .../source/panels/behavior-tree/index.ts | 28 +- .../panels/behavior-tree/types/index.ts | 8 +- .../panels/behavior-tree/utils/nodeUtils.ts | 2 +- .../static/style/behavior-tree/base.css | 75 ++ .../static/style/behavior-tree/canvas.css | 149 +++ .../static/style/behavior-tree/conditions.css | 156 +++ .../static/style/behavior-tree/index.css | 1018 ----------------- .../static/style/behavior-tree/modals.css | 133 +++ .../static/style/behavior-tree/nodes.css | 322 ++++++ .../static/style/behavior-tree/panels.css | 251 ++++ .../static/style/behavior-tree/toolbar.css | 89 ++ .../behavior-tree/BehaviorTreeEditor.html | 39 +- 17 files changed, 1916 insertions(+), 1134 deletions(-) create mode 100644 extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConditionAttachment.ts create mode 100644 extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/base.css create mode 100644 extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/canvas.css create mode 100644 extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/conditions.css delete mode 100644 extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/index.css create mode 100644 extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css create mode 100644 extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css create mode 100644 extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css create mode 100644 extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts index cf856e8d..bd3d9dcc 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useAppState.ts @@ -18,6 +18,17 @@ export function useAppState() { const selectedNodeId = ref(null); const nodeSearchText = ref(''); + // 调试:检查条件节点模板 + console.log('🔍 条件节点模板检查:'); + nodeTemplates.filter(t => t.category === 'condition').forEach(template => { + console.log(` ${template.name}: isDraggableCondition=${template.isDraggableCondition}`); + }); + + console.log('🎭 装饰器节点模板检查:'); + nodeTemplates.filter(t => t.category === 'decorator').forEach(template => { + console.log(` ${template.name}: type=${template.type}`); + }); + // 画布状态 const canvasWidth = ref(800); const canvasHeight = ref(600); diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts index 187e4d5e..97b612e5 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useBehaviorTreeEditor.ts @@ -8,6 +8,7 @@ import { useFileOperations } from './useFileOperations'; import { useConnectionManager } from './useConnectionManager'; import { useCanvasManager } from './useCanvasManager'; import { useNodeDisplay } from './useNodeDisplay'; +import { useConditionAttachment } from './useConditionAttachment'; import { validateTree as validateTreeStructure } from '../utils/nodeUtils'; /** @@ -118,6 +119,11 @@ export function useBehaviorTreeEditor() { const nodeDisplay = useNodeDisplay(); + const conditionAttachment = useConditionAttachment( + appState.treeNodes, + appState.getNodeByIdLocal + ); + const dragState = reactive({ isDragging: false, dragNode: null as any, @@ -340,6 +346,8 @@ export function useBehaviorTreeEditor() { } }; + + onMounted(() => { const appContainer = document.querySelector('#behavior-tree-app'); if (appContainer) { @@ -370,16 +378,51 @@ export function useBehaviorTreeEditor() { event.preventDefault(); connectionManager.cancelConnection(); } + // Escape键取消条件拖拽 + if (event.key === 'Escape' && conditionAttachment.dragState.isDraggingCondition) { + event.preventDefault(); + conditionAttachment.resetDragState(); + } + }; + + // 全局拖拽结束处理 + const handleGlobalDragEnd = (event: DragEvent) => { + console.log('🔚 全局拖拽结束,是否正在拖拽条件:', conditionAttachment.dragState.isDraggingCondition); + if (conditionAttachment.dragState.isDraggingCondition) { + setTimeout(() => { + console.log('⏰ 延迟重置拖拽状态'); + conditionAttachment.resetDragState(); + }, 100); // 延迟重置,确保drop事件先执行 + } + }; + + // 全局拖拽监听器用于调试 + const handleGlobalDragOver = (event: DragEvent) => { + if (conditionAttachment.dragState.isDraggingCondition) { + console.log('🌐 全局dragover,鼠标位置:', event.clientX, event.clientY, '目标:', event.target); + } + }; + + const handleGlobalDrop = (event: DragEvent) => { + if (conditionAttachment.dragState.isDraggingCondition) { + console.log('🌐 全局drop事件,目标:', event.target, '位置:', event.clientX, event.clientY); + } }; document.addEventListener('load-behavior-tree-file', handleLoadBehaviorTreeFile as EventListener); document.addEventListener('file-load-error', handleFileLoadError as EventListener); document.addEventListener('keydown', handleKeydown); + document.addEventListener('dragend', handleGlobalDragEnd); + document.addEventListener('dragover', handleGlobalDragOver); + document.addEventListener('drop', handleGlobalDrop); onUnmounted(() => { document.removeEventListener('load-behavior-tree-file', handleLoadBehaviorTreeFile as EventListener); document.removeEventListener('file-load-error', handleFileLoadError as EventListener); document.removeEventListener('keydown', handleKeydown); + document.removeEventListener('dragend', handleGlobalDragEnd); + document.removeEventListener('dragover', handleGlobalDragOver); + document.removeEventListener('drop', handleGlobalDrop); // 清理暴露的方法 if (appContainer) { @@ -412,6 +455,57 @@ export function useBehaviorTreeEditor() { dragState, autoLayout, validateTree, - clearAllConnections + clearAllConnections, + // 条件吸附功能 + conditionDragState: conditionAttachment.dragState, + startConditionDrag: conditionAttachment.startConditionDrag, + handleDecoratorDragOver: conditionAttachment.handleDecoratorDragOver, + handleDecoratorDragLeave: conditionAttachment.handleDecoratorDragLeave, + attachConditionToDecorator: conditionAttachment.attachConditionToDecorator, + getConditionDisplayText: conditionAttachment.getConditionDisplayText, + removeConditionFromDecorator: conditionAttachment.removeConditionFromDecorator, + canAcceptCondition: conditionAttachment.canAcceptCondition, + resetDragState: conditionAttachment.resetDragState, + // 合并的画布拖拽处理 + handleCanvasDrop: (event: DragEvent) => { + // 先尝试条件拖拽处理 + if (conditionAttachment.handleCanvasDrop(event)) { + return; // 如果是条件拖拽,直接返回 + } + // 否则使用正常的节点拖拽处理 + nodeOps.onCanvasDrop(event); + }, + // 条件节点拖拽处理 + handleConditionNodeDragStart: (event: DragEvent, template: any) => { + console.log('🎯 条件节点拖拽事件:', template.name, template.isDraggableCondition); + if (template.isDraggableCondition) { + conditionAttachment.startConditionDrag(event, template); + } else { + nodeOps.onNodeDragStart(event, template); + } + }, + // 节点拖拽事件处理 + handleNodeDrop: (event: DragEvent, node: any) => { + console.log('📦 节点拖拽放置:', node.name, node.type, 'isDraggingCondition:', conditionAttachment.dragState.isDraggingCondition); + if (node.type === 'conditional-decorator') { + event.preventDefault(); + event.stopPropagation(); + return conditionAttachment.attachConditionToDecorator(event, node); + } + }, + handleNodeDragOver: (event: DragEvent, node: any) => { + console.log('🔄 节点拖拽悬停:', node.name, node.type, 'isDraggingCondition:', conditionAttachment.dragState.isDraggingCondition); + if (node.type === 'conditional-decorator') { + event.preventDefault(); + event.stopPropagation(); + return conditionAttachment.handleDecoratorDragOver(event, node); + } + }, + handleNodeDragLeave: (event: DragEvent, node: any) => { + console.log('🔙 节点拖拽离开:', node.name, node.type); + if (node.type === 'conditional-decorator') { + conditionAttachment.handleDecoratorDragLeave(node); + } + } }; } \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConditionAttachment.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConditionAttachment.ts new file mode 100644 index 00000000..3eb835f6 --- /dev/null +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useConditionAttachment.ts @@ -0,0 +1,344 @@ +import { ref, reactive, Ref } from 'vue'; +import { TreeNode } from '../types'; +import { NodeTemplate } from '../data/nodeTemplates'; + +/** + * 拖拽状态 + */ +interface DragState { + isDraggingCondition: boolean; + conditionTemplate: NodeTemplate | null; + mousePosition: { x: number, y: number } | null; + hoveredDecoratorId: string | null; +} + +/** + * 条件节点吸附功能 + */ +export function useConditionAttachment( + treeNodes: Ref, + getNodeByIdLocal: (id: string) => TreeNode | undefined +) { + + const dragState = reactive({ + isDraggingCondition: false, + conditionTemplate: null, + mousePosition: null, + hoveredDecoratorId: null + }); + + /** + * 检查节点是否为条件装饰器 + */ + const isConditionalDecorator = (node: TreeNode): boolean => { + return node.type === 'conditional-decorator'; + }; + + /** + * 开始拖拽条件节点 + */ + const startConditionDrag = (event: DragEvent, template: NodeTemplate) => { + console.log('🎯 开始条件拖拽:', template.name, template.isDraggableCondition); + + if (!template.isDraggableCondition) { + console.warn('节点不是可拖拽条件:', template.name); + return; + } + + dragState.isDraggingCondition = true; + dragState.conditionTemplate = template; + + if (event.dataTransfer) { + event.dataTransfer.setData('application/json', JSON.stringify({ + ...template, + isConditionDrag: true + })); + event.dataTransfer.effectAllowed = 'copy'; + } + + console.log('✅ 条件拖拽状态已设置:', dragState); + }; + + /** + * 处理拖拽悬停在装饰器上 + */ + const handleDecoratorDragOver = (event: DragEvent, decoratorNode: TreeNode) => { + console.log('🔀 装饰器拖拽悬停:', decoratorNode.name, decoratorNode.type, 'isDragging:', dragState.isDraggingCondition); + + // 检查传输数据 + const transferData = event.dataTransfer?.getData('application/json'); + if (transferData) { + try { + const data = JSON.parse(transferData); + console.log('📦 传输数据:', data.isConditionDrag, data.isDraggableCondition, data.name); + } catch (e) { + console.log('❌ 传输数据解析失败:', transferData); + } + } + + if (!dragState.isDraggingCondition || !isConditionalDecorator(decoratorNode)) { + console.log('❌ 不符合条件:', { + isDragging: dragState.isDraggingCondition, + isDecorator: isConditionalDecorator(decoratorNode), + nodeType: decoratorNode.type + }); + return false; + } + + event.preventDefault(); + event.stopPropagation(); + + dragState.hoveredDecoratorId = decoratorNode.id; + + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'copy'; + } + + console.log('✅ 装饰器可接受拖拽:', decoratorNode.name); + return true; + }; + + /** + * 处理拖拽离开装饰器 + */ + const handleDecoratorDragLeave = (decoratorNode: TreeNode) => { + if (dragState.hoveredDecoratorId === decoratorNode.id) { + dragState.hoveredDecoratorId = null; + } + }; + + /** + * 条件到装饰器属性的映射 + */ + const mapConditionToDecoratorProperties = (conditionTemplate: NodeTemplate): Record => { + const baseConfig = { + conditionType: getConditionTypeFromTemplate(conditionTemplate), + shouldReevaluate: true + }; + + switch (conditionTemplate.type) { + case 'condition-random': + return { + ...baseConfig, + successProbability: conditionTemplate.properties?.successProbability?.value || 0.5 + }; + + case 'condition-component': + return { + ...baseConfig, + componentType: conditionTemplate.properties?.componentType?.value || 'Component' + }; + + case 'condition-tag': + return { + ...baseConfig, + tagValue: conditionTemplate.properties?.tagValue?.value || 0 + }; + + case 'condition-active': + return { + ...baseConfig, + checkHierarchy: conditionTemplate.properties?.checkHierarchy?.value || true + }; + + case 'condition-numeric': + return { + ...baseConfig, + propertyPath: conditionTemplate.properties?.propertyPath?.value || 'context.someValue', + compareOperator: conditionTemplate.properties?.compareOperator?.value || 'greater', + compareValue: conditionTemplate.properties?.compareValue?.value || 0 + }; + + case 'condition-property': + return { + ...baseConfig, + propertyPath: conditionTemplate.properties?.propertyPath?.value || 'context.someProperty' + }; + + case 'condition-custom': + return { + ...baseConfig, + conditionCode: conditionTemplate.properties?.conditionCode?.value || '(context) => true' + }; + + default: + return baseConfig; + } + }; + + /** + * 获取条件类型字符串 + */ + const getConditionTypeFromTemplate = (template: NodeTemplate): string => { + const typeMap: Record = { + 'condition-random': 'random', + 'condition-component': 'hasComponent', + 'condition-tag': 'hasTag', + 'condition-active': 'isActive', + 'condition-numeric': 'numericCompare', + 'condition-property': 'propertyExists', + 'condition-custom': 'custom' + }; + + return typeMap[template.type] || 'custom'; + }; + + /** + * 执行条件吸附到装饰器 + */ + const attachConditionToDecorator = ( + event: DragEvent, + decoratorNode: TreeNode + ): boolean => { + console.log('🎯 执行条件吸附:', decoratorNode.name, dragState.conditionTemplate?.name); + + event.preventDefault(); + event.stopPropagation(); + + if (!dragState.isDraggingCondition || !dragState.conditionTemplate) { + console.log('❌ 拖拽状态无效:', { + isDragging: dragState.isDraggingCondition, + hasTemplate: !!dragState.conditionTemplate + }); + return false; + } + + if (!isConditionalDecorator(decoratorNode)) { + console.log('❌ 不是条件装饰器:', decoratorNode.type); + return false; + } + + // 获取条件配置 + const conditionConfig = mapConditionToDecoratorProperties(dragState.conditionTemplate); + console.log('📝 条件配置:', conditionConfig); + + // 更新装饰器属性 + if (!decoratorNode.properties) { + decoratorNode.properties = {}; + } + + Object.assign(decoratorNode.properties, conditionConfig); + + // 标记装饰器已附加条件 + decoratorNode.attachedCondition = { + type: dragState.conditionTemplate.type, + name: dragState.conditionTemplate.name, + icon: dragState.conditionTemplate.icon + }; + + console.log('✅ 条件吸附成功!', decoratorNode.attachedCondition); + + // 重置拖拽状态 + resetDragState(); + + return true; + }; + + /** + * 处理画布拖拽事件(阻止条件节点创建为独立节点) + */ + const handleCanvasDrop = (event: DragEvent): boolean => { + const templateData = event.dataTransfer?.getData('application/json'); + if (!templateData) return false; + + try { + const data = JSON.parse(templateData); + // 如果是条件拖拽,阻止创建独立节点 + if (data.isConditionDrag || data.isDraggableCondition) { + event.preventDefault(); + resetDragState(); + return true; + } + } catch (error) { + // 忽略解析错误 + } + + return false; + }; + + /** + * 重置拖拽状态 + */ + const resetDragState = () => { + console.log('🔄 重置拖拽状态'); + dragState.isDraggingCondition = false; + dragState.conditionTemplate = null; + dragState.mousePosition = null; + dragState.hoveredDecoratorId = null; + }; + + /** + * 获取条件显示文本 + */ + const getConditionDisplayText = (decoratorNode: TreeNode): string => { + if (!decoratorNode.attachedCondition || !decoratorNode.properties) { + return ''; + } + + const conditionType = decoratorNode.properties.conditionType; + + switch (conditionType) { + case 'random': + const probability = decoratorNode.properties.successProbability || 0.5; + return `${(probability * 100).toFixed(0)}%概率`; + + case 'hasComponent': + return `有${decoratorNode.properties.componentType || 'Component'}`; + + case 'hasTag': + return `标签=${decoratorNode.properties.tagValue || 0}`; + + case 'isActive': + const checkHierarchy = decoratorNode.properties.checkHierarchy; + return checkHierarchy ? '激活(含层级)' : '激活'; + + case 'numericCompare': + const path = decoratorNode.properties.propertyPath || 'value'; + const operator = decoratorNode.properties.compareOperator || '>'; + const value = decoratorNode.properties.compareValue || 0; + return `${path} ${operator} ${value}`; + + case 'propertyExists': + return `存在${decoratorNode.properties.propertyPath || 'property'}`; + + case 'custom': + return '自定义条件'; + + default: + return decoratorNode.attachedCondition.name; + } + }; + + /** + * 移除装饰器的条件 + */ + const removeConditionFromDecorator = (decoratorNode: TreeNode) => { + if (decoratorNode.attachedCondition) { + delete decoratorNode.attachedCondition; + + // 完全清空所有属性,回到初始空白状态 + decoratorNode.properties = {}; + } + }; + + /** + * 检查装饰器是否可以接受条件吸附 + */ + const canAcceptCondition = (decoratorNode: TreeNode): boolean => { + return isConditionalDecorator(decoratorNode); + }; + + return { + dragState, + startConditionDrag, + handleDecoratorDragOver, + handleDecoratorDragLeave, + attachConditionToDecorator, + handleCanvasDrop, + resetDragState, + getConditionDisplayText, + removeConditionFromDecorator, + canAcceptCondition, + isConditionalDecorator + }; +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts index a83d9586..3d190e24 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/composables/useNodeOperations.ts @@ -26,7 +26,12 @@ export function useNodeOperations( // 拖拽事件处理 const onNodeDragStart = (event: DragEvent, template: NodeTemplate) => { if (event.dataTransfer) { - event.dataTransfer.setData('application/json', JSON.stringify(template)); + // 检查是否为条件节点,如果是则标记为条件拖拽 + const dragData = { + ...template, + isConditionDrag: template.isDraggableCondition || false + }; + event.dataTransfer.setData('application/json', JSON.stringify(dragData)); event.dataTransfer.effectAllowed = 'copy'; } }; @@ -45,7 +50,14 @@ export function useNodeOperations( if (!templateData) return; try { - const template: NodeTemplate = JSON.parse(templateData); + const dragData = JSON.parse(templateData); + + // 如果是条件节点拖拽,阻止创建独立节点 + if (dragData.isConditionDrag || dragData.isDraggableCondition) { + return; // 条件节点不能作为独立节点创建 + } + + const template: NodeTemplate = dragData; const canvasElement = event.currentTarget as HTMLElement; const { x, y } = getCanvasCoords(event, canvasElement); diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts index 06833f00..9ba49825 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/data/nodeTemplates.ts @@ -26,6 +26,9 @@ export interface NodeTemplate { properties?: Record; className?: string; // 对应的实际类名 namespace?: string; // 命名空间 + // 条件节点相关 + isDraggableCondition?: boolean; // 是否为可拖拽的条件节点 + attachableToDecorator?: boolean; // 是否可以吸附到条件装饰器 } /** @@ -126,7 +129,24 @@ export const nodeTemplates: NodeTemplate[] = [ canHaveParent: true, minChildren: 2, className: 'RandomSelector', - namespace: 'behaviourTree/composites' + namespace: 'behaviourTree/composites', + properties: { + reshuffleOnRestart: { + name: '重启时重新洗牌', + type: 'boolean', + value: true, + description: '是否在每次重新开始时都重新洗牌子节点顺序', + required: false + }, + abortType: { + name: '中止类型', + type: 'select', + value: 'None', + options: ['None', 'LowerPriority', 'Self', 'Both'], + description: '决定节点在何种情况下会被中止', + required: false + } + } }, { type: 'random-sequence', @@ -138,7 +158,24 @@ export const nodeTemplates: NodeTemplate[] = [ canHaveParent: true, minChildren: 2, className: 'RandomSequence', - namespace: 'behaviourTree/composites' + namespace: 'behaviourTree/composites', + properties: { + reshuffleOnRestart: { + name: '重启时重新洗牌', + type: 'boolean', + value: true, + description: '是否在每次重新开始时都重新洗牌子节点顺序', + required: false + }, + abortType: { + name: '中止类型', + type: 'select', + value: 'None', + options: ['None', 'LowerPriority', 'Self', 'Both'], + description: '决定节点在何种情况下会被中止', + required: false + } + } }, // 装饰器节点 (Decorators) - 只能有一个子节点 @@ -155,18 +192,25 @@ export const nodeTemplates: NodeTemplate[] = [ className: 'Repeater', namespace: 'behaviourTree/decorators', properties: { - repeatCount: { + count: { name: '重复次数', type: 'number', value: -1, - description: '重复执行次数,-1表示无限重复', + description: '重复执行次数,-1表示无限重复,必须是正整数', required: true }, - repeatForever: { - name: '无限重复', + endOnFailure: { + name: '失败时停止', type: 'boolean', - value: true, - description: '是否无限重复执行', + value: false, + description: '子节点失败时是否停止重复', + required: false + }, + endOnSuccess: { + name: '成功时停止', + type: 'boolean', + value: false, + description: '子节点成功时是否停止重复', required: false } } @@ -241,22 +285,14 @@ export const nodeTemplates: NodeTemplate[] = [ name: '条件装饰器', icon: '🔀', category: 'decorator', - description: '基于条件执行子节点', + description: '基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)', canHaveChildren: true, canHaveParent: true, maxChildren: 1, minChildren: 1, className: 'ConditionalDecorator', namespace: 'behaviourTree/decorators', - properties: { - conditionCode: { - name: '条件代码', - type: 'code', - value: '(context) => true', - description: '条件判断函数代码', - required: true - } - } + properties: {} }, // 动作节点 (Actions) - 叶子节点,不能有子节点 @@ -304,14 +340,14 @@ export const nodeTemplates: NodeTemplate[] = [ name: '等待时间', type: 'number', value: 1.0, - description: '等待时间(秒)', + description: '等待时间(秒),必须大于0', required: true }, - randomVariance: { - name: '随机变化', - type: 'number', - value: 0.0, - description: '时间的随机变化量', + useExternalTime: { + name: '使用外部时间', + type: 'boolean', + value: false, + description: '是否使用上下文提供的deltaTime,否则使用内部时间计算', required: false } } @@ -348,7 +384,7 @@ export const nodeTemplates: NodeTemplate[] = [ { type: 'behavior-tree-reference', name: '行为树引用', - icon: '🌳', + icon: '🔗', category: 'action', description: '运行另一个行为树', canHaveChildren: false, @@ -367,39 +403,44 @@ export const nodeTemplates: NodeTemplate[] = [ } }, - // 条件节点 (基础条件) - 叶子节点,不能有子节点 + + + + // 条件节点 (可拖拽到条件装饰器上吸附) { - type: 'execute-conditional', - name: '执行条件', - icon: '❓', + type: 'condition-random', + name: '随机概率', + icon: '🎲', category: 'condition', - description: '执行自定义条件判断', + description: '基于概率的随机条件 (拖拽到条件装饰器上使用)', canHaveChildren: false, - canHaveParent: true, + canHaveParent: false, // 不能作为常规子节点 maxChildren: 0, - className: 'ExecuteActionConditional', + isDraggableCondition: true, // 标记为可拖拽的条件 + attachableToDecorator: true, // 可以吸附到装饰器 + className: 'RandomProbability', namespace: 'behaviourTree/conditionals', properties: { - conditionCode: { - name: '条件代码', - type: 'code', - value: '(context) => {\n // 在这里编写条件判断逻辑\n return TaskStatus.Success; // 或 TaskStatus.Failure\n}', - description: '条件判断函数代码', + successProbability: { + name: '成功概率', + type: 'number', + value: 0.5, + description: '返回成功的概率 (0.0 - 1.0)', required: true } } }, - - // ECS专用节点 - 都是叶子节点 { - type: 'has-component', - name: '检查组件', - icon: '🔍', - category: 'ecs', - description: '检查实体是否包含指定组件', + type: 'condition-component', + name: '组件检查', + icon: '🔍📦', + category: 'condition', + description: '检查实体是否有指定组件 (拖拽到条件装饰器上使用)', canHaveChildren: false, - canHaveParent: true, + canHaveParent: false, maxChildren: 0, + isDraggableCondition: true, + attachableToDecorator: true, className: 'HasComponentCondition', namespace: 'ecs-integration/behaviors', properties: { @@ -412,6 +453,145 @@ export const nodeTemplates: NodeTemplate[] = [ } } }, + { + type: 'condition-tag', + name: '标签检查', + icon: '🏷️', + category: 'condition', + description: '检查实体标签 (拖拽到条件装饰器上使用)', + canHaveChildren: false, + canHaveParent: false, + maxChildren: 0, + isDraggableCondition: true, + attachableToDecorator: true, + className: 'HasTagCondition', + namespace: 'ecs-integration/behaviors', + properties: { + tagValue: { + name: '标签值', + type: 'number', + value: 0, + description: '要检查的标签值', + required: true + } + } + }, + { + type: 'condition-active', + name: '激活状态', + icon: '👁️', + category: 'condition', + description: '检查实体激活状态 (拖拽到条件装饰器上使用)', + canHaveChildren: false, + canHaveParent: false, + maxChildren: 0, + isDraggableCondition: true, + attachableToDecorator: true, + className: 'IsActiveCondition', + namespace: 'ecs-integration/behaviors', + properties: { + checkHierarchy: { + name: '检查层级', + type: 'boolean', + value: true, + description: '是否检查层级激活状态', + required: false + } + } + }, + { + type: 'condition-numeric', + name: '数值比较', + icon: '🔢', + category: 'condition', + description: '数值比较条件 (拖拽到条件装饰器上使用)', + canHaveChildren: false, + canHaveParent: false, + maxChildren: 0, + isDraggableCondition: true, + attachableToDecorator: true, + className: 'NumericComparison', + namespace: 'behaviourTree/conditionals', + properties: { + propertyPath: { + name: '属性路径', + type: 'string', + value: 'context.someValue', + description: '要比较的属性路径', + required: true + }, + compareOperator: { + name: '比较操作符', + type: 'select', + value: 'greater', + options: ['greater', 'less', 'equal', 'greaterEqual', 'lessEqual', 'notEqual'], + description: '数值比较操作符', + required: true + }, + compareValue: { + name: '比较值', + type: 'number', + value: 0, + description: '用于比较的数值', + required: true + } + } + }, + { + type: 'condition-property', + name: '属性存在', + icon: '📋', + category: 'condition', + description: '检查属性是否存在 (拖拽到条件装饰器上使用)', + canHaveChildren: false, + canHaveParent: false, + maxChildren: 0, + isDraggableCondition: true, + attachableToDecorator: true, + className: 'PropertyExists', + namespace: 'behaviourTree/conditionals', + properties: { + propertyPath: { + name: '属性路径', + type: 'string', + value: 'context.someProperty', + description: '要检查的属性路径', + required: true + } + } + }, + { + type: 'condition-custom', + name: '自定义条件', + icon: '⚙️', + category: 'condition', + description: '自定义代码条件 (拖拽到条件装饰器上使用)', + canHaveChildren: false, + canHaveParent: false, + maxChildren: 0, + isDraggableCondition: true, + attachableToDecorator: true, + className: 'ExecuteActionConditional', + namespace: 'behaviourTree/conditionals', + properties: { + conditionCode: { + name: '条件代码', + type: 'code', + value: '(context) => {\n // 条件判断逻辑\n return true; // 返回 true/false\n}', + description: '条件判断函数代码', + required: true + }, + conditionName: { + name: '条件名称', + type: 'string', + value: '', + description: '用于调试的条件名称', + required: false + } + } + }, + + // ECS专用节点 - 动作节点 { type: 'add-component', name: '添加组件', @@ -489,48 +669,7 @@ export const nodeTemplates: NodeTemplate[] = [ } } }, - { - type: 'has-tag', - name: '检查标签', - icon: '🏷️', - category: 'ecs', - description: '检查实体是否具有指定标签', - canHaveChildren: false, - canHaveParent: true, - maxChildren: 0, - className: 'HasTagCondition', - namespace: 'ecs-integration/behaviors', - properties: { - tag: { - name: '标签值', - type: 'number', - value: 0, - description: '要检查的标签值', - required: true - } - } - }, - { - type: 'is-active', - name: '检查激活状态', - icon: '🔋', - category: 'ecs', - description: '检查实体是否处于激活状态', - canHaveChildren: false, - canHaveParent: true, - maxChildren: 0, - className: 'IsActiveCondition', - namespace: 'ecs-integration/behaviors', - properties: { - checkHierarchy: { - name: '检查层级', - type: 'boolean', - value: true, - description: '是否检查层级激活状态', - required: false - } - } - }, + { type: 'wait-time', name: 'ECS等待', diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/index.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/index.ts index 99e68aeb..ffc09d42 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/index.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/index.ts @@ -18,30 +18,23 @@ let currentPanelInstance: any = null; * 面板定义 */ const panelDefinition = { - /** - * 面板模板 - */ template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/index.html'), 'utf-8'), - /** - * 面板样式 - */ - style: readFileSync(join(__dirname, '../../../static/style/behavior-tree/index.css'), 'utf-8'), + style: [ + readFileSync(join(__dirname, '../../../static/style/behavior-tree/base.css'), 'utf-8'), + readFileSync(join(__dirname, '../../../static/style/behavior-tree/toolbar.css'), 'utf-8'), + readFileSync(join(__dirname, '../../../static/style/behavior-tree/panels.css'), 'utf-8'), + readFileSync(join(__dirname, '../../../static/style/behavior-tree/canvas.css'), 'utf-8'), + readFileSync(join(__dirname, '../../../static/style/behavior-tree/nodes.css'), 'utf-8'), + readFileSync(join(__dirname, '../../../static/style/behavior-tree/conditions.css'), 'utf-8'), + readFileSync(join(__dirname, '../../../static/style/behavior-tree/modals.css'), 'utf-8') + ].join('\n'), - /** - * 选择器 - */ $: { app: '#behavior-tree-app', }, - /** - * 面板方法 - 用于处理来自扩展主进程的消息 - */ methods: { - /** - * 加载行为树文件 - */ async loadBehaviorTreeFile(assetInfo: any) { try { const filePath = assetInfo?.file || assetInfo?.path; @@ -113,9 +106,6 @@ const panelDefinition = { }, }, - /** - * 面板准备完成时调用 - */ ready() { currentPanelInstance = this; diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts index 4227f65c..209ca301 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/types/index.ts @@ -10,12 +10,18 @@ export interface TreeNode { y: number; children: string[]; parent?: string; - properties?: Record; + properties?: Record; // 改为any以支持动态属性值 canHaveChildren: boolean; canHaveParent: boolean; maxChildren?: number; // 最大子节点数量限制 minChildren?: number; // 最小子节点数量要求 hasError?: boolean; + // 条件装饰器相关 + attachedCondition?: { + type: string; + name: string; + icon: string; + }; } export interface Connection { diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/nodeUtils.ts b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/nodeUtils.ts index 1a4780ad..6b80d219 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/nodeUtils.ts +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/source/panels/behavior-tree/utils/nodeUtils.ts @@ -31,7 +31,7 @@ export function createNodeFromTemplate(template: NodeTemplate, x: number = 100, const node: TreeNode = { id: nodeId, - type: template.className || template.type, + type: template.type, name: template.name, icon: template.icon, description: template.description, diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/base.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/base.css new file mode 100644 index 00000000..bca194be --- /dev/null +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/base.css @@ -0,0 +1,75 @@ +/* 基础样式和布局 */ + +/* 全局重置和通用样式 */ +* { + box-sizing: border-box; +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: #1a202c; +} + +::-webkit-scrollbar-thumb { + background: #4a5568; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #667eea; +} + +/* 选择文本样式 */ +::selection { + background: rgba(102, 126, 234, 0.3); + color: #ffffff; +} + +#behavior-tree-app { + display: flex; + flex-direction: column; + height: 100vh; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: #1e1e1e; + color: #ffffff; + overflow: hidden; +} + +/* 编辑器容器 */ +.editor-container { + display: flex; + flex: 1; + overflow: hidden; +} + +/* 响应式设计 */ +@media (max-width: 1200px) { + .nodes-panel { + width: 240px; + } + + .properties-panel { + width: 280px; + } +} + +@media (max-width: 900px) { + .editor-container { + flex-direction: column; + } + + .nodes-panel, .properties-panel { + width: 100%; + height: 200px; + } + + .canvas-container { + flex: 1; + min-height: 400px; + } +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/canvas.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/canvas.css new file mode 100644 index 00000000..970006d2 --- /dev/null +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/canvas.css @@ -0,0 +1,149 @@ +/* 画布区域样式 */ + +.canvas-container { + flex: 1; + display: flex; + flex-direction: column; + background: #1a202c; + overflow: hidden; +} + +.canvas-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; + background: #2d3748; + border-bottom: 1px solid #4a5568; +} + +.zoom-controls, .canvas-actions { + display: flex; + gap: 8px; + align-items: center; +} + +.zoom-controls button, .canvas-actions button { + padding: 4px 8px; + background: #4a5568; + border: 1px solid #718096; + border-radius: 4px; + color: white; + cursor: pointer; + font-size: 11px; + transition: background 0.3s ease; +} + +.zoom-controls button:hover, .canvas-actions button:hover { + background: #667eea; +} + +.canvas-area { + flex: 1; + position: relative; + overflow: hidden; + background: #1a202c; + background-image: + radial-gradient(circle, #2d3748 1px, transparent 1px); + background-size: 20px 20px; + background-position: 0 0; + cursor: grab; +} + +.behavior-tree-canvas { + position: absolute; + top: 0; + left: 0; + pointer-events: none; +} + +/* 连接线样式 */ +.connection-layer { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + overflow: visible; +} + +.connection-line { + fill: none; + stroke: #667eea; + stroke-width: 2; + pointer-events: all; + cursor: pointer; + transition: all 0.3s ease; +} + +.connection-line:hover { + stroke: #f6ad55; + stroke-width: 3; + filter: drop-shadow(0 0 6px rgba(246, 173, 85, 0.6)); +} + +.connection-active { + stroke: #48bb78; + stroke-dasharray: 8, 4; + animation: flow 2s linear infinite; +} + +@keyframes flow { + 0% { stroke-dashoffset: 0; } + 100% { stroke-dashoffset: 12; } +} + +.connection-temp { + fill: none; + stroke: #f6ad55; + stroke-width: 2; + stroke-dasharray: 5, 5; + animation: dash 1s linear infinite; + opacity: 0.8; + pointer-events: none; +} + +@keyframes dash { + 0% { stroke-dashoffset: 0; } + 100% { stroke-dashoffset: 10; } +} + +/* 节点层 */ +.nodes-layer { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + transform-origin: 0 0; + width: 100%; + height: 100%; +} + +/* 网格背景 */ +.grid-background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + opacity: 0.3; +} + +/* 画布状态 */ +.canvas-area.connecting { + cursor: crosshair; +} + +.canvas-area.connecting .tree-node { + pointer-events: none; +} + +.canvas-area.connecting .port { + pointer-events: all; + cursor: pointer; +} + +.drag-over { + background: rgba(102, 126, 234, 0.1); + border: 2px dashed #667eea; +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/conditions.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/conditions.css new file mode 100644 index 00000000..48e4b440 --- /dev/null +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/conditions.css @@ -0,0 +1,156 @@ +/* 条件拖拽功能样式 */ + +/* 可拖拽条件节点 */ +.node-item.draggable-condition { + border-left: 3px solid #ffd700; + position: relative; +} + +.node-item.draggable-condition .drag-hint { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + font-size: 10px; + color: #ffd700; + animation: bounce-hint 2s infinite; +} + +@keyframes bounce-hint { + 0%, 20%, 50%, 80%, 100% { + transform: translateY(-50%); + } + 40% { + transform: translateY(-60%); + } + 60% { + transform: translateY(-40%); + } +} + +/* 条件装饰器接受状态 */ +.tree-node.can-accept-condition { + border: 2px dashed #ffd700; + animation: pulse-accept 1.5s infinite; +} + +@keyframes pulse-accept { + 0%, 100% { + border-color: #ffd700; + box-shadow: 0 0 5px rgba(255, 215, 0, 0.3); + } + 50% { + border-color: #ffed4e; + box-shadow: 0 0 15px rgba(255, 215, 0, 0.6); + } +} + +.tree-node.condition-hover { + border-color: #ffd700; + background: rgba(255, 215, 0, 0.1); + transform: scale(1.02); +} + +/* 条件附加区域 */ +.condition-attachment-area { + margin-top: 8px; + padding: 8px; + border-radius: 4px; + min-height: 32px; +} + +.condition-placeholder { + text-align: center; + padding: 12px; + border: 2px dashed #4a5568; + border-radius: 4px; + color: #a0aec0; + font-size: 11px; + transition: all 0.3s ease; +} + +.tree-node.can-accept-condition .condition-placeholder { + border-color: #ffd700; + color: #ffd700; + background: rgba(255, 215, 0, 0.05); +} + +.attached-condition { + background: rgba(255, 215, 0, 0.1); + border: 1px solid #ffd700; + border-radius: 4px; + padding: 6px; +} + +.condition-info { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; +} + +.condition-icon { + font-size: 12px; +} + +.condition-text { + flex: 1; + color: #ffd700; + font-weight: 500; +} + +.remove-condition-btn { + background: none; + border: none; + color: #e53e3e; + cursor: pointer; + font-size: 14px; + padding: 2px 4px; + border-radius: 2px; + transition: all 0.2s ease; +} + +.remove-condition-btn:hover { + background: rgba(229, 62, 62, 0.2); + transform: scale(1.1); +} + +/* 画布状态 */ +.canvas-area.condition-dragging { + background: rgba(255, 215, 0, 0.02); +} + +.canvas-area.condition-dragging .tree-node:not(.can-accept-condition) { + opacity: 0.6; +} + +/* 条件装饰器节点的特殊样式 */ +.tree-node.node-conditional-decorator .condition-attachment-area { + border: 1px solid #9f7aea; + background: rgba(159, 122, 234, 0.05); +} + +.tree-node.node-conditional-decorator.node-selected .condition-attachment-area { + border-color: #ffd700; + background: rgba(255, 215, 0, 0.1); +} + +/* 条件附加成功的动画效果 */ +@keyframes condition-attached { + 0% { + transform: scale(1); + background: rgba(255, 215, 0, 0.3); + } + 50% { + transform: scale(1.05); + background: rgba(255, 215, 0, 0.5); + } + 100% { + transform: scale(1); + background: rgba(255, 215, 0, 0.1); + } +} + +.condition-just-attached { + animation: condition-attached 0.6s ease-out; +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/index.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/index.css deleted file mode 100644 index 7a054d25..00000000 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/index.css +++ /dev/null @@ -1,1018 +0,0 @@ -/* 基础样式 */ -#behavior-tree-app { - display: flex; - flex-direction: column; - height: 100vh; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background: #1e1e1e; - color: #ffffff; - overflow: hidden; -} - -/* 头部工具栏 */ -.header-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 20px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-bottom: 2px solid #4a5568; - box-shadow: 0 2px 8px rgba(0,0,0,0.3); -} - -.toolbar-left { - display: flex; - align-items: center; - gap: 20px; -} - -.toolbar-left h2 { - margin: 0; - font-size: 20px; - font-weight: 600; - text-shadow: 0 2px 4px rgba(0,0,0,0.3); -} - -.current-file { - color: #a0aec0; - font-weight: 400; - font-size: 16px; - margin-left: 8px; -} - -.unsaved-indicator { - color: #ff6b6b; - animation: pulse-unsaved 2s infinite; - margin-left: 8px; -} - -@keyframes pulse-unsaved { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -.toolbar-buttons { - display: flex; - gap: 8px; -} - -.tool-btn { - display: flex; - align-items: center; - gap: 6px; - padding: 8px 12px; - background: rgba(255,255,255,0.1); - border: 1px solid rgba(255,255,255,0.2); - border-radius: 6px; - color: white; - cursor: pointer; - transition: all 0.3s ease; - font-size: 12px; -} - -.tool-btn:hover { - background: rgba(255,255,255,0.2); - transform: translateY(-1px); -} - -.tool-btn.has-changes { - background: rgba(255, 107, 107, 0.2); - border-color: #ff6b6b; - animation: glow-save 2s infinite; -} - -@keyframes glow-save { - 0%, 100% { - box-shadow: 0 0 5px rgba(255, 107, 107, 0.5); - } - 50% { - box-shadow: 0 0 15px rgba(255, 107, 107, 0.8); - } -} - -.toolbar-right .install-status { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 16px; - background: rgba(255,255,255,0.1); - border-radius: 8px; -} - -/* 编辑器容器 */ -.editor-container { - display: flex; - flex: 1; - overflow: hidden; -} - -/* 左侧节点面板 */ -.nodes-panel { - width: 280px; - background: #2d3748; - border-right: 1px solid #4a5568; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.panel-header { - padding: 16px; - background: #4a5568; - border-bottom: 1px solid #718096; -} - -.panel-header h3 { - margin: 0 0 12px 0; - font-size: 16px; - color: #e2e8f0; -} - -.search-input { - width: 100%; - padding: 8px 12px; - background: #1a202c; - border: 1px solid #4a5568; - border-radius: 4px; - color: white; - font-size: 12px; -} - -.search-input::placeholder { - color: #a0aec0; -} - -.node-categories { - flex: 1; - overflow-y: auto; - padding: 8px; -} - -.category { - margin-bottom: 16px; -} - -.category-title { - margin: 0 0 8px 0; - padding: 8px 12px; - background: #4a5568; - border-radius: 4px; - font-size: 14px; - color: #e2e8f0; -} - -.node-list { - display: flex; - flex-direction: column; - gap: 4px; -} - -.node-item { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: #1a202c; - border: 1px solid #4a5568; - border-radius: 4px; - cursor: grab; - transition: all 0.3s ease; - font-size: 12px; -} - -.node-item:hover { - background: #2d3748; - border-color: #667eea; - transform: translateX(4px); -} - -.node-item:active { - cursor: grabbing; -} - -.node-item.root { border-left: 3px solid #f6ad55; } -.node-item.composite { border-left: 3px solid #667eea; } -.node-item.decorator { border-left: 3px solid #9f7aea; } -.node-item.action { border-left: 3px solid #48bb78; } -.node-item.condition { border-left: 3px solid #ed8936; } -.node-item.ecs { border-left: 3px solid #38b2ac; } - -.node-icon { - font-size: 16px; - min-width: 20px; -} - -.node-name { - flex: 1; - color: #e2e8f0; -} - -/* 中间画布区域 */ -.canvas-container { - flex: 1; - display: flex; - flex-direction: column; - background: #1a202c; - position: relative; -} - -.canvas-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 16px; - background: #2d3748; - border-bottom: 1px solid #4a5568; -} - -.zoom-controls, .canvas-actions { - display: flex; - gap: 8px; - align-items: center; -} - -.zoom-controls button, .canvas-actions button { - padding: 4px 8px; - background: #4a5568; - border: 1px solid #718096; - border-radius: 4px; - color: white; - cursor: pointer; - font-size: 12px; - transition: background 0.3s ease; -} - -.zoom-controls button:hover, .canvas-actions button:hover { - background: #667eea; -} - -.canvas-area { - flex: 1; - position: relative; - overflow: auto; - background: - radial-gradient(circle at 20px 20px, #4a5568 1px, transparent 1px), - linear-gradient(90deg, transparent 19px, #2d3748 20px, transparent 21px), - linear-gradient(transparent 19px, #2d3748 20px, transparent 21px); - background-size: 20px 20px; -} - -.behavior-tree-canvas { - position: absolute; - top: 0; - left: 0; - pointer-events: none; - z-index: 0; -} - -.connection-layer { - position: absolute; - top: 0; - left: 0; - pointer-events: none; - z-index: 1; - will-change: transform; - overflow: visible; -} - -.connection-line { - stroke: #67b7dc; - stroke-width: 3; - fill: none; - transition: none; - opacity: 0.9; - will-change: d; - pointer-events: stroke; - cursor: pointer; -} - -.connection-line:hover { - stroke: #f56565; - stroke-width: 5; - opacity: 1; - filter: drop-shadow(0 0 4px rgba(245, 101, 101, 0.6)); -} - -.connection-active { - stroke: #48bb78; - stroke-width: 4; - animation: flow 1.5s linear infinite; -} - -@keyframes flow { - 0% { stroke-dasharray: 8,8; stroke-dashoffset: 0; } - 100% { stroke-dasharray: 8,8; stroke-dashoffset: 16; } -} - -.connection-temp { - stroke: #f6ad55; - stroke-width: 3; - stroke-dasharray: 6,6; - opacity: 0.8; - animation: dash 1s linear infinite; - will-change: d; - transition: none; -} - -@keyframes dash { - 0% { stroke-dashoffset: 0; } - 100% { stroke-dashoffset: 12; } -} - -.nodes-layer { - position: absolute; - top: 0; - left: 0; - z-index: 2; - transform-origin: 0 0; - will-change: transform; - overflow: visible; - /* 硬件加速优化 */ - transform: translateZ(0); - backface-visibility: hidden; - perspective: 1000px; -} - -.tree-node { - position: absolute; - min-width: 150px; - min-height: 80px; - background: linear-gradient(145deg, #2d3748 0%, #1a202c 100%); - border: 2px solid #4a5568; - border-radius: 12px; - cursor: pointer; - user-select: none; - transition: all 0.2s ease; - box-shadow: 0 4px 12px rgba(0,0,0,0.3); - will-change: transform; - backface-visibility: hidden; - transform-style: preserve-3d; -} - -.tree-node:hover { - border-color: #67b7dc; - box-shadow: 0 6px 20px rgba(103, 183, 220, 0.3); - transform: translateY(-1px); -} - -.tree-node.node-selected { - border-color: #48bb78; - box-shadow: 0 0 0 3px rgba(72, 187, 120, 0.3); - background: linear-gradient(145deg, #2f4f4f 0%, #1e3a3a 100%); -} - -.tree-node.node-error { - border-color: #f56565; - box-shadow: 0 0 0 3px rgba(245, 101, 101, 0.3); - background: linear-gradient(145deg, #4a2626 0%, #2d1a1a 100%); -} - -.node-header { - display: flex; - align-items: center; - padding: 10px 14px; - background: rgba(0,0,0,0.2); - border-bottom: 1px solid #4a5568; - border-radius: 10px 10px 0 0; - backdrop-filter: blur(2px); -} - -.node-header .node-icon { - font-size: 16px; - margin-right: 10px; - opacity: 0.9; -} - -.node-title { - flex: 1; - font-size: 13px; - font-weight: 600; - color: #e2e8f0; - text-shadow: 0 1px 2px rgba(0,0,0,0.3); -} - -.node-delete { - width: 20px; - height: 20px; - background: #f56565; - border: none; - border-radius: 50%; - color: white; - cursor: pointer; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - opacity: 0.7; - transition: opacity 0.3s ease; -} - -.node-delete:hover { - opacity: 1; -} - -.node-body { - padding: 10px 14px; -} - -.node-description { - font-size: 11px; - color: #a0aec0; - line-height: 1.4; - opacity: 0.9; -} - -/* 端口基础样式 */ -.port { - position: absolute; - width: 16px; - height: 16px; - border-radius: 2px; - border: 2px solid; - cursor: pointer; - transition: all 0.2s ease; - z-index: 100; - left: 50%; - pointer-events: auto; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - backdrop-filter: blur(1px); -} - -/* 增加端口的可点击区域 */ -.port::before { - content: ''; - position: absolute; - top: -10px; - left: -10px; - right: -10px; - bottom: -10px; - z-index: 101; - pointer-events: auto; -} - -.port:hover { - transform: scale(1.3); - box-shadow: 0 0 8px rgba(0, 0, 0, 0.3); -} - -/* 输入端口 - 执行流入口,蓝色方形 */ -.port-input { - top: -8px; - transform: translateX(-50%); - background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%); - border-color: #2c5aa0; -} - -.port-input:hover { - background: linear-gradient(135deg, #5ba3f5 0%, #4a90e2 100%); - border-color: #357abd; - box-shadow: 0 0 12px rgba(74, 144, 226, 0.6); - transform: translateX(-50%) scale(1.3); -} - -/* 输出端口 - 执行流出口,橙色方形 */ -.port-output { - bottom: -8px; - transform: translateX(-50%); - background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%); - border-color: #d68910; -} - -.port-output:hover { - background: linear-gradient(135deg, #f5b041 0%, #f39c12 100%); - border-color: #e67e22; - box-shadow: 0 0 12px rgba(243, 156, 18, 0.6); - transform: translateX(-50%) scale(1.3); -} - -/* 连接中的端口高亮 - 更清晰的反馈 */ -.port.connecting { - background: linear-gradient(135deg, #ffd700 0%, #ffb347 100%) !important; - border-color: #ff8c00 !important; - box-shadow: 0 0 15px rgba(255, 215, 0, 0.8) !important; - animation: pulse-port 1s infinite; -} - -@keyframes pulse-port { - 0% { - transform: translateX(-50%) scale(1.2); - box-shadow: 0 0 15px rgba(255, 215, 0, 0.8); - } - 50% { - transform: translateX(-50%) scale(1.4); - box-shadow: 0 0 20px rgba(255, 215, 0, 1); - } - 100% { - transform: translateX(-50%) scale(1.2); - box-shadow: 0 0 15px rgba(255, 215, 0, 0.8); - } -} - -/* 拖拽目标样式 - 绿色高亮表示可连接 */ -.port.drag-target { - background: linear-gradient(135deg, #48bb78 0%, #38a169 100%) !important; - border-color: #2f855a !important; - transform: scale(1.4) !important; - box-shadow: 0 0 15px rgba(72, 187, 120, 0.8) !important; - animation: pulse-target 0.8s infinite; -} - -@keyframes pulse-target { - 0% { - transform: scale(1.4); - box-shadow: 0 0 15px rgba(72, 187, 120, 0.8); - } - 50% { - transform: scale(1.6); - box-shadow: 0 0 20px rgba(72, 187, 120, 1); - } - 100% { - transform: scale(1.4); - box-shadow: 0 0 15px rgba(72, 187, 120, 0.8); - } -} - -/* 端口内部亮点效果 */ -.port-inner { - position: absolute; - top: 3px; - left: 3px; - right: 3px; - bottom: 3px; - border-radius: 1px; - background: rgba(255, 255, 255, 0.2); - opacity: 0; - transition: opacity 0.2s ease; - pointer-events: none; -} - -.port:hover .port-inner { - opacity: 1; - background: rgba(255, 255, 255, 0.4); -} - -.port.connecting .port-inner { - opacity: 1; - background: rgba(255, 255, 255, 0.6); -} - -.port.drag-target .port-inner { - opacity: 1; - background: rgba(255, 255, 255, 0.8); -} - -.children-indicator { - position: absolute; - bottom: -12px; - right: 8px; - width: 20px; - height: 20px; - background: #667eea; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 10px; - font-weight: bold; - color: white; -} - -/* 右侧属性面板 */ -.properties-panel { - width: 320px; - background: #2d3748; - border-left: 1px solid #4a5568; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.node-properties { - flex: 1; - overflow-y: auto; - padding: 16px; -} - -.property-section { - margin-bottom: 20px; - padding-bottom: 16px; - border-bottom: 1px solid #4a5568; -} - -.property-section h4 { - margin: 0 0 12px 0; - font-size: 14px; - color: #e2e8f0; -} - -.property-item { - margin-bottom: 12px; -} - -.property-item label { - display: block; - margin-bottom: 4px; - font-size: 12px; - color: #a0aec0; -} - -.property-item input, -.property-item textarea, -.property-item select { - flex: 1; - padding: 6px 8px; - background: #1a202c; - border: 1px solid #4a5568; - border-radius: 4px; - color: white; - font-size: 12px; -} - -.property-item input:focus, -.property-item textarea:focus, -.property-item select:focus { - border-color: #667eea; - outline: none; -} - -.property-item .code-input { - font-family: 'Consolas', 'Monaco', 'Courier New', monospace; - font-size: 11px; - line-height: 1.4; - resize: vertical; - min-height: 120px; -} - -.property-help { - margin: 4px 0 0 0; - font-size: 10px; - color: #a0aec0; - font-style: italic; -} - -.code-preview { - background: #1a202c; - border: 1px solid #4a5568; - border-radius: 4px; - padding: 12px; - font-family: 'Consolas', 'Monaco', monospace; - font-size: 11px; - color: #e2e8f0; - white-space: pre-wrap; - overflow-x: auto; -} - -.no-selection { - padding: 40px 20px; - text-align: center; - color: #a0aec0; -} - -.tree-structure { - padding: 16px; - border-top: 1px solid #4a5568; - background: #1a202c; -} - -.structure-tree { - font-family: monospace; - font-size: 12px; -} - -.empty-tree { - color: #a0aec0; - font-style: italic; - text-align: center; - padding: 20px; -} - -.tree-node-item { - margin-bottom: 2px; -} - -.tree-node-line { - display: flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - border-radius: 3px; - cursor: pointer; - transition: background 0.2s ease; - font-size: 11px; -} - -.tree-node-line:hover { - background: #4a5568; -} - -.tree-node-icon { - font-size: 12px; - min-width: 16px; -} - -.tree-node-name { - flex: 1; - color: #e2e8f0; - font-weight: 500; -} - -.tree-node-type { - color: #a0aec0; - font-size: 10px; -} - -/* 底部状态栏 */ -.status-bar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 20px; - background: #2d3748; - border-top: 1px solid #4a5568; - font-size: 12px; - color: #a0aec0; -} - -.status-left, .status-right { - display: flex; - gap: 16px; -} - -.status-valid { - color: #48bb78; -} - -.status-invalid { - color: #f56565; -} - -/* 模态框 */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0,0,0,0.8); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.modal-content { - background: #2d3748; - border-radius: 8px; - width: 90%; - max-width: 800px; - max-height: 90%; - display: flex; - flex-direction: column; - box-shadow: 0 20px 40px rgba(0,0,0,0.5); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 20px; - border-bottom: 1px solid #4a5568; -} - -.modal-header h3 { - margin: 0; - font-size: 18px; - color: #e2e8f0; -} - -.modal-header button { - background: none; - border: none; - font-size: 24px; - color: #a0aec0; - cursor: pointer; -} - -.modal-body { - flex: 1; - padding: 20px; - overflow-y: auto; -} - -.export-options { - display: flex; - gap: 20px; - margin-bottom: 20px; -} - -.export-options label { - display: flex; - align-items: center; - gap: 8px; - color: #e2e8f0; - cursor: pointer; -} - -.code-output { - background: #1a202c; - border: 1px solid #4a5568; - border-radius: 4px; - padding: 16px; - height: 400px; - overflow: auto; -} - -.code-output pre { - margin: 0; - font-family: 'Consolas', 'Monaco', monospace; - font-size: 12px; - color: #e2e8f0; - white-space: pre-wrap; -} - -.modal-footer { - display: flex; - gap: 12px; - padding: 16px 20px; - border-top: 1px solid #4a5568; - justify-content: flex-end; -} - -.modal-footer button { - padding: 8px 16px; - background: #667eea; - border: none; - border-radius: 4px; - color: white; - cursor: pointer; - font-size: 12px; - transition: background 0.3s ease; -} - -.modal-footer button:hover { - background: #5a67d8; -} - -/* 响应式设计 */ -@media (max-width: 1200px) { - .nodes-panel { - width: 240px; - } - - .properties-panel { - width: 280px; - } -} - -@media (max-width: 900px) { - .editor-container { - flex-direction: column; - } - - .nodes-panel, .properties-panel { - width: 100%; - height: 200px; - } - - .canvas-container { - flex: 1; - min-height: 400px; - } -} - -/* 动画效果 */ -@keyframes nodeAppear { - from { - opacity: 0; - transform: scale(0.8); - } - to { - opacity: 1; - transform: scale(1); - } -} - -.tree-node { - animation: nodeAppear 0.3s ease-out; -} - -/* 拖拽相关样式 */ -.drag-over { - background: rgba(102, 126, 234, 0.1); - border: 2px dashed #667eea; -} - -/* 拖动状态样式 - 优化硬件加速 */ -.tree-node.dragging { - opacity: 0.9; - transform: translateZ(0) scale(1.02) rotate(1deg); - box-shadow: 0 12px 30px rgba(0,0,0,0.4); - z-index: 1000; - cursor: grabbing; - will-change: transform, opacity; - backface-visibility: hidden; - transform-style: preserve-3d; - transition: none !important; - border-color: #67b7dc; - /* 强制硬件加速 */ - transform-origin: center center; - perspective: 1000px; -} - -/* 节点悬停时的端口显示优化 */ -.tree-node:hover .port { - opacity: 1; - transform: translateX(-50%) scale(1.1); -} - -.tree-node .port { - opacity: 0.8; - transition: all 0.2s ease; -} - -/* 连接状态下的cursor样式 */ -.canvas-area.connecting { - cursor: crosshair; -} - -.canvas-area.connecting .tree-node { - pointer-events: none; -} - -.canvas-area.connecting .port { - pointer-events: all; - cursor: pointer; -} - -.dragging { - opacity: 0.7; - transform: rotate(5deg); -} - -/* 节点属性预览样式 */ -.node-properties-preview { - margin-top: 6px; - padding-top: 6px; - border-top: 1px solid rgba(255, 255, 255, 0.1); - font-size: 10px; -} - -.property-preview-item { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 2px; - padding: 1px 0; -} - -.property-label { - color: #a0aec0; - font-weight: 500; - flex-shrink: 0; - margin-right: 4px; - font-size: 9px; -} - -.property-value { - color: #e2e8f0; - font-weight: 600; - text-align: right; - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 9px; -} - -/* 不同类型属性的颜色 */ -.property-value.property-boolean { - color: #68d391; -} - -.property-value.property-number { - color: #63b3ed; -} - -.property-value.property-select { - color: #f6ad55; -} - -.property-value.property-string { - color: #cbd5e0; -} - -.property-value.property-code { - color: #d69e2e; - font-family: 'Consolas', 'Monaco', monospace; -} - -/* 适应节点宽度 */ -.tree-node .node-body { - max-width: 146px; /* 节点宽度 - padding */ - overflow: hidden; -} - -/* 当节点有属性时稍微增加高度空间 */ -.tree-node:has(.node-properties-preview) { - min-height: 100px; -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css new file mode 100644 index 00000000..03e4c2ec --- /dev/null +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/modals.css @@ -0,0 +1,133 @@ +/* 模态框和状态栏样式 */ + +/* 状态栏 */ +.status-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; + background: #2d3748; + border-top: 1px solid #4a5568; + font-size: 12px; + color: #a0aec0; +} + +.status-left, .status-right { + display: flex; + gap: 16px; +} + +.status-valid { + color: #48bb78; +} + +.status-invalid { + color: #e53e3e; +} + +/* 模态框 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; +} + +.modal-content { + background: #2d3748; + border-radius: 8px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + max-width: 90vw; + max-height: 90vh; + display: flex; + flex-direction: column; + border: 1px solid #4a5568; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid #4a5568; + background: #4a5568; + border-radius: 8px 8px 0 0; +} + +.modal-header h3 { + margin: 0; + color: #e2e8f0; + font-size: 16px; +} + +.modal-header button { + background: none; + border: none; + color: #a0aec0; + cursor: pointer; + font-size: 18px; +} + +.modal-body { + flex: 1; + overflow-y: auto; + padding: 20px; +} + +.export-options { + margin-bottom: 20px; +} + +.export-options label { + display: block; + margin-bottom: 8px; + color: #e2e8f0; + font-size: 14px; + font-weight: 500; +} + +.code-output { + background: #1a202c; + border: 1px solid #4a5568; + border-radius: 4px; + padding: 16px; + height: 400px; + overflow: auto; +} + +.code-output pre { + margin: 0; + font-family: 'Consolas', 'Monaco', monospace; + font-size: 12px; + color: #e2e8f0; + white-space: pre-wrap; +} + +.modal-footer { + display: flex; + gap: 12px; + padding: 16px 20px; + border-top: 1px solid #4a5568; + justify-content: flex-end; +} + +.modal-footer button { + padding: 8px 16px; + background: #667eea; + border: none; + border-radius: 4px; + color: white; + cursor: pointer; + font-size: 12px; + transition: background 0.3s ease; +} + +.modal-footer button:hover { + background: #5a67d8; +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css new file mode 100644 index 00000000..74a6fc08 --- /dev/null +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/nodes.css @@ -0,0 +1,322 @@ +/* 节点样式 */ + +.tree-node { + position: absolute; + width: 160px; + min-height: 80px; + background: #2d3748; + border: 2px solid #4a5568; + border-radius: 8px; + pointer-events: all; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + z-index: 1; + animation: nodeAppear 0.3s ease-out; +} + +.tree-node:hover { + border-color: #667eea; + box-shadow: 0 6px 20px rgba(0,0,0,0.4); + transform: translateY(-2px); +} + +.tree-node.node-selected { + border-color: #f6ad55; + box-shadow: 0 0 20px rgba(246, 173, 85, 0.4); +} + +.tree-node.node-error { + border-color: #e53e3e; + background: rgba(229, 62, 62, 0.1); +} + +.node-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #4a5568; + border-radius: 6px 6px 0 0; + border-bottom: 1px solid #718096; +} + +.node-header .node-icon { + font-size: 16px; + flex-shrink: 0; +} + +.node-title { + flex: 1; + font-weight: 600; + font-size: 12px; + color: #e2e8f0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.node-delete { + background: none; + border: none; + color: #e53e3e; + cursor: pointer; + font-size: 16px; + font-weight: bold; + padding: 2px 4px; + border-radius: 3px; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.node-delete:hover { + background: rgba(229, 62, 62, 0.2); + transform: scale(1.1); +} + +.node-body { + padding: 8px 12px; + color: #a0aec0; + font-size: 11px; + line-height: 1.4; + max-width: 136px; + overflow: hidden; +} + +.node-description { + margin-bottom: 6px; + color: #cbd5e0; + font-style: italic; +} + +/* 端口样式 */ +.port { + position: absolute; + width: 12px; + height: 12px; + border-radius: 50%; + cursor: pointer; + transform: translateX(-50%); + transition: all 0.3s ease; + z-index: 10; + opacity: 0.8; +} + +.port::before { + content: ''; + position: absolute; + top: -4px; + left: -4px; + right: -4px; + bottom: -4px; + border-radius: 50%; + background: transparent; + transition: all 0.3s ease; +} + +.port:hover { + transform: translateX(-50%) scale(1.3); + opacity: 1; +} + +.port-input { + top: -6px; + left: 50%; + background: #4299e1; + border: 2px solid #2b6cb0; +} + +.port-input:hover { + background: #63b3ed; + box-shadow: 0 0 12px rgba(66, 153, 225, 0.6); +} + +.port-output { + bottom: -6px; + left: 50%; + background: #48bb78; + border: 2px solid #2f855a; +} + +.port-output:hover { + background: #68d391; + box-shadow: 0 0 12px rgba(72, 187, 120, 0.6); +} + +.port.connecting { + animation: pulse-port 1s ease-in-out infinite; + transform: translateX(-50%) scale(1.4); +} + +@keyframes pulse-port { + 0%, 100% { + box-shadow: 0 0 8px rgba(246, 173, 85, 0.4); + } + 50% { + box-shadow: 0 0 20px rgba(246, 173, 85, 0.8); + transform: translateX(-50%) scale(1.6); + } +} + +.port.drag-target { + animation: pulse-target 0.8s ease-in-out infinite; + background: #f6ad55 !important; + border-color: #dd6b20 !important; + transform: translateX(-50%) scale(1.5); +} + +@keyframes pulse-target { + 0%, 100% { + box-shadow: 0 0 12px rgba(246, 173, 85, 0.6); + } + 50% { + box-shadow: 0 0 24px rgba(246, 173, 85, 1); + transform: translateX(-50%) scale(1.7); + } +} + +.port-inner { + position: absolute; + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + transition: all 0.3s ease; +} + +.port:hover .port-inner { + background: rgba(255, 255, 255, 0.6); +} + +.port.connecting .port-inner { + background: rgba(255, 255, 255, 0.8); +} + +.port.drag-target .port-inner { + background: rgba(255, 255, 255, 0.9); +} + +.children-indicator { + position: absolute; + bottom: 2px; + right: 6px; + background: #667eea; + color: white; + border-radius: 50%; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + font-weight: bold; + z-index: 5; +} + +/* 节点属性预览 */ +.node-properties-preview { + margin-top: 6px; + padding-top: 6px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + font-size: 10px; +} + +.property-preview-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2px; + padding: 1px 0; +} + +.property-label { + color: #a0aec0; + font-weight: 500; + flex-shrink: 0; + margin-right: 4px; + font-size: 9px; +} + +.property-value { + color: #e2e8f0; + font-weight: 600; + text-align: right; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 9px; +} + +/* 不同类型属性的颜色 */ +.property-value.property-boolean { + color: #68d391; +} + +.property-value.property-number { + color: #63b3ed; +} + +.property-value.property-select { + color: #f6ad55; +} + +.property-value.property-string { + color: #cbd5e0; +} + +.property-value.property-code { + color: #d69e2e; + font-family: 'Consolas', 'Monaco', monospace; +} + +/* 当节点有属性时稍微增加高度空间 */ +.tree-node:has(.node-properties-preview) { + min-height: 100px; +} + +/* 节点悬停时的端口显示优化 */ +.tree-node:hover .port { + opacity: 1; + transform: translateX(-50%) scale(1.1); +} + +.tree-node .port { + opacity: 0.8; + transition: all 0.2s ease; +} + +/* 拖动状态样式 - 优化硬件加速 */ +.tree-node.dragging { + opacity: 0.9; + transform: translateZ(0) scale(1.02) rotate(1deg); + box-shadow: 0 12px 30px rgba(0,0,0,0.4); + z-index: 1000; + cursor: grabbing; + will-change: transform, opacity; + backface-visibility: hidden; + transform-style: preserve-3d; + transition: none !important; + border-color: #67b7dc; + transform-origin: center center; + perspective: 1000px; +} + +.dragging { + opacity: 0.7; + transform: rotate(5deg); +} + +/* 动画效果 */ +@keyframes nodeAppear { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css new file mode 100644 index 00000000..ffbb3bdd --- /dev/null +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/panels.css @@ -0,0 +1,251 @@ +/* 侧边面板样式 */ + +/* 左侧节点面板 */ +.nodes-panel { + width: 280px; + background: #2d3748; + border-right: 1px solid #4a5568; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.panel-header { + padding: 16px; + background: #4a5568; + border-bottom: 1px solid #718096; +} + +.panel-header h3 { + margin: 0 0 12px 0; + font-size: 16px; + color: #e2e8f0; +} + +.search-input { + width: 100%; + padding: 8px 12px; + background: #1a202c; + border: 1px solid #4a5568; + border-radius: 4px; + color: white; + font-size: 12px; +} + +.search-input::placeholder { + color: #a0aec0; +} + +.node-categories { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +.category { + margin-bottom: 16px; +} + +.category-title { + margin: 0 0 8px 0; + padding: 8px 12px; + background: #4a5568; + border-radius: 4px; + font-size: 14px; + color: #e2e8f0; +} + +.node-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.node-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #1a202c; + border: 1px solid #4a5568; + border-radius: 4px; + cursor: grab; + transition: all 0.3s ease; + font-size: 12px; + position: relative; +} + +.node-item:hover { + background: #2d3748; + border-color: #667eea; + transform: translateX(4px); +} + +.node-item:active { + cursor: grabbing; +} + +.node-item.root { border-left: 3px solid #f6ad55; } +.node-item.composite { border-left: 3px solid #667eea; } +.node-item.decorator { border-left: 3px solid #9f7aea; } +.node-item.action { border-left: 3px solid #48bb78; } +.node-item.condition { border-left: 3px solid #ed8936; } +.node-item.ecs { border-left: 3px solid #38b2ac; } + +.node-icon { + font-size: 14px; + flex-shrink: 0; +} + +.node-name { + font-weight: 500; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 右侧属性面板 */ +.properties-panel { + width: 320px; + background: #2d3748; + border-left: 1px solid #4a5568; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.node-properties { + flex: 1; + overflow-y: auto; + padding: 16px; +} + +.property-section { + margin-bottom: 20px; +} + +.property-section h4 { + margin: 0 0 12px 0; + color: #e2e8f0; + font-size: 14px; +} + +.property-item { + margin-bottom: 12px; +} + +.property-item label { + display: block; + margin-bottom: 4px; + color: #a0aec0; + font-size: 12px; + font-weight: 500; +} + +.property-item input, +.property-item textarea, +.property-item select { + width: 100%; + padding: 8px 12px; + background: #1a202c; + border: 1px solid #4a5568; + border-radius: 4px; + color: white; + font-size: 12px; + font-family: inherit; +} + +.property-item input:focus, +.property-item textarea:focus, +.property-item select:focus { + border-color: #667eea; + outline: none; + box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); +} + +.property-item .code-input { + font-family: 'Consolas', 'Monaco', monospace; + font-size: 11px; + resize: vertical; + min-height: 80px; +} + +.property-help { + margin-top: 4px; + font-size: 10px; + color: #718096; + line-height: 1.4; +} + +.code-preview { + background: #1a202c; + border: 1px solid #4a5568; + border-radius: 4px; + padding: 12px; + margin-top: 8px; + font-family: 'Consolas', 'Monaco', monospace; + font-size: 11px; + color: #e2e8f0; + white-space: pre-wrap; + max-height: 200px; + overflow-y: auto; +} + +.no-selection { + padding: 40px 20px; + text-align: center; + color: #718096; + font-style: italic; +} + +.tree-structure { + margin-top: 20px; +} + +.structure-tree { + font-family: 'Consolas', 'Monaco', monospace; + font-size: 11px; +} + +.empty-tree { + padding: 20px; + text-align: center; + color: #718096; + font-style: italic; + border: 2px dashed #4a5568; + border-radius: 4px; +} + +.tree-node-item { + cursor: pointer; +} + +.tree-node-line { + padding: 2px 4px; + border-radius: 2px; + margin: 1px 0; + display: flex; + align-items: center; + gap: 4px; + transition: background 0.2s ease; +} + +.tree-node-line:hover { + background: rgba(102, 126, 234, 0.1); +} + +.tree-node-icon { + font-size: 10px; + width: 12px; +} + +.tree-node-name { + flex: 1; + color: #e2e8f0; +} + +.tree-node-type { + font-size: 9px; + color: #718096; +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css new file mode 100644 index 00000000..9c0fae24 --- /dev/null +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/style/behavior-tree/toolbar.css @@ -0,0 +1,89 @@ +/* 头部工具栏样式 */ +.header-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-bottom: 2px solid #4a5568; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); +} + +.toolbar-left { + display: flex; + align-items: center; + gap: 20px; +} + +.toolbar-left h2 { + margin: 0; + font-size: 20px; + font-weight: 600; + text-shadow: 0 2px 4px rgba(0,0,0,0.3); +} + +.current-file { + color: #a0aec0; + font-weight: 400; + font-size: 16px; + margin-left: 8px; +} + +.unsaved-indicator { + color: #ff6b6b; + animation: pulse-unsaved 2s infinite; + margin-left: 8px; +} + +@keyframes pulse-unsaved { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.toolbar-buttons { + display: flex; + gap: 8px; +} + +.tool-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 6px; + color: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 12px; +} + +.tool-btn:hover { + background: rgba(255,255,255,0.2); + transform: translateY(-1px); +} + +.tool-btn.has-changes { + background: rgba(255, 107, 107, 0.2); + border-color: #ff6b6b; + animation: glow-save 2s infinite; +} + +@keyframes glow-save { + 0%, 100% { + box-shadow: 0 0 5px rgba(255, 107, 107, 0.5); + } + 50% { + box-shadow: 0 0 15px rgba(255, 107, 107, 0.8); + } +} + +.toolbar-right .install-status { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 16px; + background: rgba(255,255,255,0.1); + border-radius: 8px; +} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html index dd39f84c..7fc7799f 100644 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html +++ b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/static/template/behavior-tree/BehaviorTreeEditor.html @@ -127,12 +127,14 @@ v-for="node in filteredConditionNodes()" :key="node.type" class="node-item condition" + :class="{ 'draggable-condition': node.isDraggableCondition }" :draggable="true" - @dragstart="onNodeDragStart($event, node)" - :title="node.description" + @dragstart="handleConditionNodeDragStart($event, node)" + :title="node.description + (node.isDraggableCondition ? ' (可拖拽到条件装饰器)' : '')" > {{ node.icon }} {{ node.name }} + 🎯 @@ -177,8 +179,11 @@
{{ node.icon }} @@ -249,6 +259,25 @@
{{ node.description }}
+ + +
+
+
+ {{ node.attachedCondition.icon }} + {{ getConditionDisplayText(node) }} + +
+
+
+ 🎯 拖拽条件到此处 +
+
+