支持先加入实体后加入系统以让matcher进行实体匹配/优化行为树节点效果及逻辑
This commit is contained in:
@@ -0,0 +1,818 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "root_1",
|
||||
"type": "root",
|
||||
"name": "行为树指南根",
|
||||
"icon": "🌳",
|
||||
"x": 1270,
|
||||
"y": 50,
|
||||
"children": [
|
||||
"selector_main"
|
||||
],
|
||||
"properties": {},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": false,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "selector_main",
|
||||
"type": "selector",
|
||||
"name": "主选择器",
|
||||
"icon": "?",
|
||||
"x": 1280,
|
||||
"y": 180,
|
||||
"children": [
|
||||
"repeater_patrol",
|
||||
"selector_combat",
|
||||
"sequence_idle"
|
||||
],
|
||||
"properties": {
|
||||
"abortType": {
|
||||
"name": "中止类型",
|
||||
"type": "select",
|
||||
"value": "LowerPriority",
|
||||
"description": "决定节点在何种情况下会被中止",
|
||||
"options": [
|
||||
"None",
|
||||
"LowerPriority",
|
||||
"Self",
|
||||
"Both"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "repeater_patrol",
|
||||
"type": "repeater",
|
||||
"name": "巡逻重复器",
|
||||
"icon": "🔄",
|
||||
"x": 510,
|
||||
"y": 360,
|
||||
"children": [
|
||||
"sequence_patrol"
|
||||
],
|
||||
"properties": {
|
||||
"count": {
|
||||
"name": "重复次数",
|
||||
"type": "number",
|
||||
"value": -1,
|
||||
"description": "重复执行次数,-1表示无限重复,必须是正整数",
|
||||
"required": true
|
||||
},
|
||||
"continueOnFailure": {
|
||||
"name": "失败时继续",
|
||||
"type": "boolean",
|
||||
"value": true,
|
||||
"description": "子节点失败时是否继续重复",
|
||||
"required": false
|
||||
},
|
||||
"delayBetween": {
|
||||
"name": "重复间隔",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "重复之间是否有延迟",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_patrol",
|
||||
"type": "sequence",
|
||||
"name": "巡逻序列",
|
||||
"icon": "→",
|
||||
"x": 510,
|
||||
"y": 580,
|
||||
"children": [
|
||||
"decorator_patrol_check",
|
||||
"action_patrol"
|
||||
],
|
||||
"properties": {
|
||||
"abortType": {
|
||||
"name": "中止类型",
|
||||
"type": "select",
|
||||
"value": "None",
|
||||
"description": "决定节点在何种情况下会被中止",
|
||||
"options": [
|
||||
"None",
|
||||
"LowerPriority",
|
||||
"Self",
|
||||
"Both"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "decorator_patrol_check",
|
||||
"type": "conditional-decorator",
|
||||
"name": "巡逻条件检查",
|
||||
"icon": "🔀",
|
||||
"x": 400,
|
||||
"y": 760,
|
||||
"children": [
|
||||
"log_patrolling"
|
||||
],
|
||||
"properties": {
|
||||
"conditionType": {
|
||||
"name": "条件类型",
|
||||
"type": "select",
|
||||
"value": "custom",
|
||||
"description": "装饰器使用的条件类型",
|
||||
"options": [
|
||||
"custom",
|
||||
"random",
|
||||
"hasComponent",
|
||||
"hasTag",
|
||||
"isActive",
|
||||
"numericCompare",
|
||||
"propertyExists"
|
||||
],
|
||||
"required": false
|
||||
},
|
||||
"executeWhenTrue": {
|
||||
"name": "条件为真时执行",
|
||||
"type": "boolean",
|
||||
"value": true,
|
||||
"description": "条件为真时是否执行子节点",
|
||||
"required": false
|
||||
},
|
||||
"executeWhenFalse": {
|
||||
"name": "条件为假时执行",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "条件为假时是否执行子节点",
|
||||
"required": false
|
||||
},
|
||||
"checkInterval": {
|
||||
"name": "检查间隔",
|
||||
"type": "number",
|
||||
"value": 1,
|
||||
"description": "条件检查间隔时间(秒),0表示每帧检查",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"attachedCondition": {
|
||||
"type": "condition-custom",
|
||||
"name": "巡逻状态检查",
|
||||
"icon": "⚙️",
|
||||
"properties": {
|
||||
"conditionCode": {
|
||||
"name": "条件代码",
|
||||
"type": "code",
|
||||
"value": "(context) => {\n // 检查是否处于巡逻状态\n return context.blackboard && context.blackboard.getValue('state') === 'patrol';\n}",
|
||||
"description": "条件判断函数代码",
|
||||
"required": true
|
||||
},
|
||||
"conditionName": {
|
||||
"name": "条件名称",
|
||||
"type": "string",
|
||||
"value": "巡逻状态检查",
|
||||
"description": "用于调试的条件名称",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "log_patrolling",
|
||||
"type": "log-action",
|
||||
"name": "记录巡逻",
|
||||
"icon": "📝",
|
||||
"x": 400,
|
||||
"y": 1000,
|
||||
"children": [],
|
||||
"properties": {
|
||||
"message": {
|
||||
"name": "日志消息",
|
||||
"type": "string",
|
||||
"value": "正在执行巡逻任务,当前状态: {{state}}",
|
||||
"description": "使用{{}}引用黑板变量显示当前状态",
|
||||
"required": true
|
||||
},
|
||||
"logLevel": {
|
||||
"name": "日志级别",
|
||||
"type": "select",
|
||||
"value": "info",
|
||||
"description": "日志输出级别",
|
||||
"options": [
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": false,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "action_patrol",
|
||||
"type": "set-blackboard-value",
|
||||
"name": "执行巡逻",
|
||||
"icon": "📝",
|
||||
"x": 620,
|
||||
"y": 760,
|
||||
"children": [],
|
||||
"properties": {
|
||||
"variableName": {
|
||||
"name": "变量名",
|
||||
"type": "string",
|
||||
"value": "lastAction",
|
||||
"description": "黑板变量名",
|
||||
"required": true
|
||||
},
|
||||
"value": {
|
||||
"name": "设置值",
|
||||
"type": "string",
|
||||
"value": "{{state}}_执行中",
|
||||
"description": "使用{{}}引用当前状态并添加后缀",
|
||||
"required": false
|
||||
},
|
||||
"sourceVariable": {
|
||||
"name": "源变量名",
|
||||
"type": "string",
|
||||
"value": "",
|
||||
"description": "从另一个黑板变量复制值",
|
||||
"required": false
|
||||
},
|
||||
"force": {
|
||||
"name": "强制设置",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否忽略只读限制",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": false,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "selector_combat",
|
||||
"type": "selector",
|
||||
"name": "战斗选择器",
|
||||
"icon": "?",
|
||||
"x": 1170,
|
||||
"y": 360,
|
||||
"children": [
|
||||
"sequence_attack",
|
||||
"sequence_defend"
|
||||
],
|
||||
"properties": {
|
||||
"abortType": {
|
||||
"name": "中止类型",
|
||||
"type": "select",
|
||||
"value": "None",
|
||||
"description": "决定节点在何种情况下会被中止",
|
||||
"options": [
|
||||
"None",
|
||||
"LowerPriority",
|
||||
"Self",
|
||||
"Both"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_attack",
|
||||
"type": "sequence",
|
||||
"name": "攻击序列",
|
||||
"icon": "→",
|
||||
"x": 950,
|
||||
"y": 540,
|
||||
"children": [
|
||||
"inverter_enemy",
|
||||
"action_attack"
|
||||
],
|
||||
"properties": {
|
||||
"abortType": {
|
||||
"name": "中止类型",
|
||||
"type": "select",
|
||||
"value": "Self",
|
||||
"description": "决定节点在何种情况下会被中止",
|
||||
"options": [
|
||||
"None",
|
||||
"LowerPriority",
|
||||
"Self",
|
||||
"Both"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "inverter_enemy",
|
||||
"type": "inverter",
|
||||
"name": "敌人检查反转",
|
||||
"icon": "!",
|
||||
"x": 840,
|
||||
"y": 720,
|
||||
"children": [
|
||||
"condition_enemy"
|
||||
],
|
||||
"properties": {},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "condition_enemy",
|
||||
"type": "condition-random",
|
||||
"name": "随机敌人出现",
|
||||
"icon": "🎲",
|
||||
"x": 840,
|
||||
"y": 880,
|
||||
"children": [],
|
||||
"properties": {
|
||||
"successProbability": {
|
||||
"name": "成功概率",
|
||||
"type": "number",
|
||||
"value": 0.3,
|
||||
"description": "条件成功的概率 (0.0 - 1.0)",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"canHaveChildren": false,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "action_attack",
|
||||
"type": "log-action",
|
||||
"name": "攻击动作",
|
||||
"icon": "📝",
|
||||
"x": 1060,
|
||||
"y": 720,
|
||||
"children": [],
|
||||
"properties": {
|
||||
"message": {
|
||||
"name": "日志消息",
|
||||
"type": "string",
|
||||
"value": "发动攻击!生命值: {{health}}, 能量: {{energy}}",
|
||||
"description": "使用{{}}引用显示战斗时的状态信息",
|
||||
"required": true
|
||||
},
|
||||
"logLevel": {
|
||||
"name": "日志级别",
|
||||
"type": "select",
|
||||
"value": "warn",
|
||||
"description": "日志输出级别",
|
||||
"options": [
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": false,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_defend",
|
||||
"type": "sequence",
|
||||
"name": "防御序列",
|
||||
"icon": "→",
|
||||
"x": 1390,
|
||||
"y": 540,
|
||||
"children": [
|
||||
"wait_defend",
|
||||
"action_defend"
|
||||
],
|
||||
"properties": {
|
||||
"abortType": {
|
||||
"name": "中止类型",
|
||||
"type": "select",
|
||||
"value": "None",
|
||||
"description": "决定节点在何种情况下会被中止",
|
||||
"options": [
|
||||
"None",
|
||||
"LowerPriority",
|
||||
"Self",
|
||||
"Both"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "wait_defend",
|
||||
"type": "wait-action",
|
||||
"name": "防御准备",
|
||||
"icon": "⏰",
|
||||
"x": 1280,
|
||||
"y": 720,
|
||||
"children": [],
|
||||
"properties": {
|
||||
"waitTime": {
|
||||
"name": "等待时间",
|
||||
"type": "number",
|
||||
"value": 0.5,
|
||||
"description": "等待时间(秒),必须大于0",
|
||||
"required": true
|
||||
},
|
||||
"useExternalTime": {
|
||||
"name": "使用外部时间",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否使用上下文提供的deltaTime,否则使用内部时间计算",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": false,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "action_defend",
|
||||
"type": "execute-action",
|
||||
"name": "执行防御",
|
||||
"icon": "⚙️",
|
||||
"x": 1500,
|
||||
"y": 720,
|
||||
"children": [],
|
||||
"properties": {
|
||||
"actionCode": {
|
||||
"name": "动作代码",
|
||||
"type": "code",
|
||||
"value": "(context) => {\n // 防御逻辑\n console.log('开始防御姿态');\n if(context.blackboard) {\n context.blackboard.setValue('defendActive', true);\n context.blackboard.setValue('lastAction', '防御中');\n }\n return 'success';\n}",
|
||||
"description": "要执行的动作函数代码",
|
||||
"required": true
|
||||
},
|
||||
"actionName": {
|
||||
"name": "动作名称",
|
||||
"type": "string",
|
||||
"value": "防御动作_生命值{{health}}",
|
||||
"description": "使用{{}}引用在动作名称中显示生命值",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": false,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_idle",
|
||||
"type": "sequence",
|
||||
"name": "闲置序列",
|
||||
"icon": "→",
|
||||
"x": 1940,
|
||||
"y": 360,
|
||||
"children": [
|
||||
"action_idle",
|
||||
"log_status",
|
||||
"wait_idle"
|
||||
],
|
||||
"properties": {
|
||||
"abortType": {
|
||||
"name": "中止类型",
|
||||
"type": "select",
|
||||
"value": "None",
|
||||
"description": "决定节点在何种情况下会被中止",
|
||||
"options": [
|
||||
"None",
|
||||
"LowerPriority",
|
||||
"Self",
|
||||
"Both"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": true,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "action_idle",
|
||||
"type": "set-blackboard-value",
|
||||
"name": "设置闲置",
|
||||
"icon": "📝",
|
||||
"x": 1720,
|
||||
"y": 540,
|
||||
"children": [],
|
||||
"properties": {
|
||||
"variableName": {
|
||||
"name": "变量名",
|
||||
"type": "string",
|
||||
"value": "state",
|
||||
"description": "黑板变量名",
|
||||
"required": true
|
||||
},
|
||||
"value": {
|
||||
"name": "设置值",
|
||||
"type": "string",
|
||||
"value": "idle",
|
||||
"description": "要设置的值(留空则使用源变量)",
|
||||
"required": false
|
||||
},
|
||||
"sourceVariable": {
|
||||
"name": "源变量名",
|
||||
"type": "string",
|
||||
"value": "",
|
||||
"description": "从另一个黑板变量复制值",
|
||||
"required": false
|
||||
},
|
||||
"force": {
|
||||
"name": "强制设置",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否忽略只读限制",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": false,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "log_status",
|
||||
"type": "log-action",
|
||||
"name": "状态报告",
|
||||
"icon": "📝",
|
||||
"x": 1940,
|
||||
"y": 540,
|
||||
"children": [],
|
||||
"properties": {
|
||||
"message": {
|
||||
"name": "日志消息",
|
||||
"type": "string",
|
||||
"value": "状态报告 - 当前: {{state}}, 上次动作: {{lastAction}}, 防御中: {{defendActive}}",
|
||||
"description": "完整的黑板变量引用示例,显示多个变量值",
|
||||
"required": true
|
||||
},
|
||||
"logLevel": {
|
||||
"name": "日志级别",
|
||||
"type": "select",
|
||||
"value": "debug",
|
||||
"description": "日志输出级别",
|
||||
"options": [
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error"
|
||||
],
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": false,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
},
|
||||
{
|
||||
"id": "wait_idle",
|
||||
"type": "wait-action",
|
||||
"name": "闲置等待",
|
||||
"icon": "⏰",
|
||||
"x": 2160,
|
||||
"y": 540,
|
||||
"children": [],
|
||||
"properties": {
|
||||
"waitTime": {
|
||||
"name": "等待时间",
|
||||
"type": "number",
|
||||
"value": 3,
|
||||
"description": "等待时间(秒),必须大于0",
|
||||
"required": true
|
||||
},
|
||||
"useExternalTime": {
|
||||
"name": "使用外部时间",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"description": "是否使用上下文提供的deltaTime,否则使用内部时间计算",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"canHaveChildren": false,
|
||||
"canHaveParent": true,
|
||||
"hasError": false
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "root_1-selector_main",
|
||||
"sourceId": "root_1",
|
||||
"targetId": "selector_main",
|
||||
"path": "M 1349.21875 128 C 1349.21875 158 1359.21875 152 1359.21875 182",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "selector_main-repeater_patrol",
|
||||
"sourceId": "selector_main",
|
||||
"targetId": "repeater_patrol",
|
||||
"path": "M 1359.21875 278 C 1359.21875 320 590 320 590 362",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "selector_main-selector_combat",
|
||||
"sourceId": "selector_main",
|
||||
"targetId": "selector_combat",
|
||||
"path": "M 1359.21875 278 C 1359.21875 320 1250 320 1250 362",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "selector_main-sequence_idle",
|
||||
"sourceId": "selector_main",
|
||||
"targetId": "sequence_idle",
|
||||
"path": "M 1359.21875 278 C 1359.21875 320 2019.21875 320 2019.21875 362",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "repeater_patrol-sequence_patrol",
|
||||
"sourceId": "repeater_patrol",
|
||||
"targetId": "sequence_patrol",
|
||||
"path": "M 590 458 C 590 520 590 520 590 582",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_patrol-decorator_patrol_check",
|
||||
"sourceId": "sequence_patrol",
|
||||
"targetId": "decorator_patrol_check",
|
||||
"path": "M 590 678 C 590 720 510 720 510 762",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_patrol-action_patrol",
|
||||
"sourceId": "sequence_patrol",
|
||||
"targetId": "action_patrol",
|
||||
"path": "M 590 678 C 590 720 700 720 700 762",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "decorator_patrol_check-log_patrolling",
|
||||
"sourceId": "decorator_patrol_check",
|
||||
"targetId": "log_patrolling",
|
||||
"path": "M 510 942.078125 C 510 972.078125 480 972 480 1002",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "selector_combat-sequence_attack",
|
||||
"sourceId": "selector_combat",
|
||||
"targetId": "sequence_attack",
|
||||
"path": "M 1250 458 C 1250 500 1030 500 1030 542",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "selector_combat-sequence_defend",
|
||||
"sourceId": "selector_combat",
|
||||
"targetId": "sequence_defend",
|
||||
"path": "M 1250 458 C 1250 500 1470 500 1470 542",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_attack-inverter_enemy",
|
||||
"sourceId": "sequence_attack",
|
||||
"targetId": "inverter_enemy",
|
||||
"path": "M 1030 638 C 1030 680 920 680 920 722",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_attack-action_attack",
|
||||
"sourceId": "sequence_attack",
|
||||
"targetId": "action_attack",
|
||||
"path": "M 1030 638 C 1030 680 1140 680 1140 722",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "inverter_enemy-condition_enemy",
|
||||
"sourceId": "inverter_enemy",
|
||||
"targetId": "condition_enemy",
|
||||
"path": "M 920 798 C 920 840 920 840 920 882",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_defend-wait_defend",
|
||||
"sourceId": "sequence_defend",
|
||||
"targetId": "wait_defend",
|
||||
"path": "M 1470 638 C 1470 680 1360 680 1360 722",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_defend-action_defend",
|
||||
"sourceId": "sequence_defend",
|
||||
"targetId": "action_defend",
|
||||
"path": "M 1470 638 C 1470 680 1580 680 1580 722",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_idle-action_idle",
|
||||
"sourceId": "sequence_idle",
|
||||
"targetId": "action_idle",
|
||||
"path": "M 2019.21875 458 C 2019.21875 500 1800 500 1800 542",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_idle-log_status",
|
||||
"sourceId": "sequence_idle",
|
||||
"targetId": "log_status",
|
||||
"path": "M 2019.21875 458 C 2019.21875 500 2019.21875 500 2019.21875 542",
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"id": "sequence_idle-wait_idle",
|
||||
"sourceId": "sequence_idle",
|
||||
"targetId": "wait_idle",
|
||||
"path": "M 2019.21875 458 C 2019.21875 500 2238.4375 500 2238.4375 542",
|
||||
"active": false
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"name": "behavior-tree-examples-guide.bt",
|
||||
"created": "2025-06-19T04:28:44.589Z",
|
||||
"version": "1.0"
|
||||
},
|
||||
"blackboard": [
|
||||
{
|
||||
"name": "state",
|
||||
"type": "string",
|
||||
"value": "patrol",
|
||||
"defaultValue": "idle",
|
||||
"description": "当前状态",
|
||||
"group": "核心状态",
|
||||
"readOnly": false,
|
||||
"constraints": {
|
||||
"allowedValues": [
|
||||
"idle",
|
||||
"patrol",
|
||||
"combat",
|
||||
"defend"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lastAction",
|
||||
"type": "string",
|
||||
"value": "",
|
||||
"defaultValue": "",
|
||||
"description": "最后执行的动作",
|
||||
"group": "核心状态",
|
||||
"readOnly": false,
|
||||
"constraints": {}
|
||||
},
|
||||
{
|
||||
"name": "defendActive",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"defaultValue": false,
|
||||
"description": "是否正在防御",
|
||||
"group": "战斗状态",
|
||||
"readOnly": false,
|
||||
"constraints": {}
|
||||
},
|
||||
{
|
||||
"name": "health",
|
||||
"type": "number",
|
||||
"value": 100,
|
||||
"defaultValue": 100,
|
||||
"description": "生命值",
|
||||
"group": "属性",
|
||||
"readOnly": false,
|
||||
"constraints": {
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "energy",
|
||||
"type": "number",
|
||||
"value": 50,
|
||||
"defaultValue": 100,
|
||||
"description": "能量值",
|
||||
"group": "属性",
|
||||
"readOnly": false,
|
||||
"constraints": {
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "2.0.1",
|
||||
"importer": "json",
|
||||
"imported": true,
|
||||
"uuid": "ba6c564a-c5c5-4dc7-ba95-9f0279e0bd66",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ref } from 'vue';
|
||||
import { TreeNode, DragState, Connection } from '../types';
|
||||
import { nodeTemplates } from '../data/nodeTemplates';
|
||||
import { allNodeTemplates as nodeTemplates } from '../data/nodeTemplates';
|
||||
|
||||
/**
|
||||
* 应用状态管理
|
||||
|
||||
@@ -36,7 +36,11 @@ export function useBehaviorTreeEditor() {
|
||||
appState.treeNodes,
|
||||
appState.nodeTemplates,
|
||||
appState.getNodeByIdLocal,
|
||||
getRootNode
|
||||
getRootNode,
|
||||
computed(() => blackboard.blackboardVariables.value.reduce((map, variable) => {
|
||||
map.set(variable.name, variable);
|
||||
return map;
|
||||
}, new Map()))
|
||||
);
|
||||
|
||||
const computedProps = useComputedProperties(
|
||||
@@ -115,7 +119,14 @@ export function useBehaviorTreeEditor() {
|
||||
tempConnection: appState.tempConnection,
|
||||
showExportModal: appState.showExportModal,
|
||||
codeGeneration: codeGen,
|
||||
updateConnections: connectionManager.updateConnections
|
||||
updateConnections: connectionManager.updateConnections,
|
||||
blackboardOperations: {
|
||||
getBlackboardVariables: () => blackboard.blackboardVariables.value,
|
||||
loadBlackboardVariables: (variables: any[]) => {
|
||||
blackboard.loadBlackboardFromArray(variables);
|
||||
},
|
||||
clearBlackboard: blackboard.clearBlackboard
|
||||
}
|
||||
});
|
||||
|
||||
const canvasManager = useCanvasManager(
|
||||
@@ -250,6 +261,51 @@ export function useBehaviorTreeEditor() {
|
||||
}
|
||||
};
|
||||
|
||||
// 节点类型识别相关方法
|
||||
const getOriginalNodeName = (nodeType: string): string => {
|
||||
const template = appState.nodeTemplates.value.find(t => t.type === nodeType);
|
||||
return template?.name || nodeType;
|
||||
};
|
||||
|
||||
const getNodeTemplate = (nodeType: string) => {
|
||||
return appState.nodeTemplates.value.find(t => t.type === nodeType);
|
||||
};
|
||||
|
||||
const getNodeCategory = (nodeType: string): string => {
|
||||
const template = getNodeTemplate(nodeType);
|
||||
if (!template) return 'unknown';
|
||||
|
||||
const category = template.category || 'unknown';
|
||||
const categoryMap: Record<string, string> = {
|
||||
'root': '根节点',
|
||||
'composite': '组合',
|
||||
'decorator': '装饰器',
|
||||
'action': '动作',
|
||||
'condition': '条件',
|
||||
'ecs': 'ECS'
|
||||
};
|
||||
|
||||
return categoryMap[category] || category;
|
||||
};
|
||||
|
||||
const isNodeNameCustomized = (node: any): boolean => {
|
||||
if (!node) return false;
|
||||
const originalName = getOriginalNodeName(node.type);
|
||||
return node.name !== originalName;
|
||||
};
|
||||
|
||||
const resetNodeToOriginalName = () => {
|
||||
if (!appState.selectedNodeId.value) return;
|
||||
|
||||
const selectedNode = appState.getNodeByIdLocal(appState.selectedNodeId.value);
|
||||
if (!selectedNode) return;
|
||||
|
||||
const originalName = getOriginalNodeName(selectedNode.type);
|
||||
nodeOps.updateNodeProperty('name', originalName);
|
||||
|
||||
console.log(`节点名称已重置为原始名称: ${originalName}`);
|
||||
};
|
||||
|
||||
const startNodeDrag = (event: MouseEvent, node: any) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -306,12 +362,13 @@ export function useBehaviorTreeEditor() {
|
||||
installation.handleInstall();
|
||||
};
|
||||
|
||||
// 自动布局功能
|
||||
// 紧凑子树布局算法 - 体现行为树的层次结构
|
||||
const autoLayout = () => {
|
||||
if (appState.treeNodes.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 找到根节点
|
||||
const rootNode = appState.treeNodes.value.find(node =>
|
||||
!appState.treeNodes.value.some(otherNode =>
|
||||
otherNode.children?.includes(node.id)
|
||||
@@ -319,57 +376,225 @@ export function useBehaviorTreeEditor() {
|
||||
);
|
||||
|
||||
if (!rootNode) {
|
||||
console.warn('未找到根节点,无法进行自动布局');
|
||||
return;
|
||||
}
|
||||
|
||||
const levelNodes: { [level: number]: any[] } = {};
|
||||
const visited = new Set<string>();
|
||||
|
||||
const queue = [{ node: rootNode, level: 0 }];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { node, level } = queue.shift()!;
|
||||
// 计算节点尺寸
|
||||
const getNodeSize = (node: any) => {
|
||||
let width = 180;
|
||||
let height = 100;
|
||||
|
||||
if (visited.has(node.id)) continue;
|
||||
visited.add(node.id);
|
||||
|
||||
if (!levelNodes[level]) {
|
||||
levelNodes[level] = [];
|
||||
// 根据节点类型调整基础尺寸
|
||||
switch (node.category || node.type) {
|
||||
case 'root':
|
||||
width = 200; height = 70;
|
||||
break;
|
||||
case 'composite':
|
||||
width = 160; height = 90;
|
||||
break;
|
||||
case 'decorator':
|
||||
width = 140; height = 80;
|
||||
break;
|
||||
case 'action':
|
||||
width = 180; height = 100;
|
||||
break;
|
||||
case 'condition':
|
||||
width = 150; height = 85;
|
||||
break;
|
||||
}
|
||||
levelNodes[level].push(node);
|
||||
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
// 根据属性数量动态调整
|
||||
if (node.properties) {
|
||||
const propertyCount = Object.keys(node.properties).length;
|
||||
height += propertyCount * 20;
|
||||
}
|
||||
|
||||
// 根据名称长度调整宽度
|
||||
if (node.name) {
|
||||
const nameWidth = node.name.length * 8 + 40;
|
||||
width = Math.max(width, nameWidth);
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
// 紧凑子树布局核心算法
|
||||
const layoutSubtree = (node: any, parentX = 0, parentY = 0, depth = 0): { width: number, height: number } => {
|
||||
const nodeSize = getNodeSize(node);
|
||||
|
||||
// 如果是叶子节点,直接返回自身尺寸
|
||||
if (!node.children || node.children.length === 0) {
|
||||
node.x = parentX;
|
||||
node.y = parentY;
|
||||
return { width: nodeSize.width, height: nodeSize.height };
|
||||
}
|
||||
|
||||
// 递归布局所有子节点,收集子树信息
|
||||
const childSubtrees: Array<{ node: any, width: number, height: number }> = [];
|
||||
let totalChildrenWidth = 0;
|
||||
let maxChildHeight = 0;
|
||||
|
||||
const childY = parentY + nodeSize.height + 60; // 子节点距离父节点的垂直间距
|
||||
const siblingSpacing = 40; // 同级子节点间的水平间距
|
||||
|
||||
// 先计算每个子树的尺寸
|
||||
node.children.forEach((childId: string) => {
|
||||
const childNode = appState.treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
const subtreeInfo = layoutSubtree(childNode, 0, childY, depth + 1);
|
||||
childSubtrees.push({ node: childNode, ...subtreeInfo });
|
||||
totalChildrenWidth += subtreeInfo.width;
|
||||
maxChildHeight = Math.max(maxChildHeight, subtreeInfo.height);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加子节点间的间距
|
||||
if (childSubtrees.length > 1) {
|
||||
totalChildrenWidth += (childSubtrees.length - 1) * siblingSpacing;
|
||||
}
|
||||
|
||||
// 计算父节点的最终位置(在子节点的中心上方)
|
||||
const subtreeWidth = Math.max(nodeSize.width, totalChildrenWidth);
|
||||
node.x = parentX + subtreeWidth / 2 - nodeSize.width / 2;
|
||||
node.y = parentY;
|
||||
|
||||
// 布局子节点(以父节点为中心分布)
|
||||
let currentX = parentX + subtreeWidth / 2 - totalChildrenWidth / 2;
|
||||
|
||||
childSubtrees.forEach(({ node: childNode, width: childWidth }) => {
|
||||
// 将子节点定位到其子树的中心
|
||||
const childCenterOffset = childWidth / 2;
|
||||
childNode.x = currentX + childCenterOffset - getNodeSize(childNode).width / 2;
|
||||
|
||||
// 递归调整子树中所有节点的位置
|
||||
adjustSubtreePosition(childNode, currentX, childY);
|
||||
|
||||
currentX += childWidth + siblingSpacing;
|
||||
});
|
||||
|
||||
// 返回整个子树的尺寸
|
||||
const subtreeHeight = nodeSize.height + 60 + maxChildHeight;
|
||||
return { width: subtreeWidth, height: subtreeHeight };
|
||||
};
|
||||
|
||||
// 递归调整子树位置
|
||||
const adjustSubtreePosition = (node: any, baseX: number, baseY: number) => {
|
||||
const nodeSize = getNodeSize(node);
|
||||
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算子节点的总宽度
|
||||
let totalChildrenWidth = 0;
|
||||
const siblingSpacing = 40;
|
||||
|
||||
node.children.forEach((childId: string) => {
|
||||
const childNode = appState.treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
const childSubtreeWidth = calculateSubtreeWidth(childNode);
|
||||
totalChildrenWidth += childSubtreeWidth;
|
||||
}
|
||||
});
|
||||
|
||||
if (node.children.length > 1) {
|
||||
totalChildrenWidth += (node.children.length - 1) * siblingSpacing;
|
||||
}
|
||||
|
||||
// 重新定位子节点
|
||||
let currentX = baseX + Math.max(nodeSize.width, totalChildrenWidth) / 2 - totalChildrenWidth / 2;
|
||||
const childY = baseY + nodeSize.height + 60;
|
||||
|
||||
node.children.forEach((childId: string) => {
|
||||
const childNode = appState.treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
const childSubtreeWidth = calculateSubtreeWidth(childNode);
|
||||
const childCenterOffset = childSubtreeWidth / 2;
|
||||
childNode.x = currentX + childCenterOffset - getNodeSize(childNode).width / 2;
|
||||
childNode.y = childY;
|
||||
|
||||
adjustSubtreePosition(childNode, currentX, childY);
|
||||
currentX += childSubtreeWidth + siblingSpacing;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 计算子树宽度
|
||||
const calculateSubtreeWidth = (node: any): number => {
|
||||
const nodeSize = getNodeSize(node);
|
||||
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return nodeSize.width;
|
||||
}
|
||||
|
||||
let totalChildrenWidth = 0;
|
||||
const siblingSpacing = 40;
|
||||
|
||||
node.children.forEach((childId: string) => {
|
||||
const childNode = appState.treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
totalChildrenWidth += calculateSubtreeWidth(childNode);
|
||||
}
|
||||
});
|
||||
|
||||
if (node.children.length > 1) {
|
||||
totalChildrenWidth += (node.children.length - 1) * siblingSpacing;
|
||||
}
|
||||
|
||||
return Math.max(nodeSize.width, totalChildrenWidth);
|
||||
};
|
||||
|
||||
// 开始布局 - 从根节点开始
|
||||
const startX = 400; // 画布中心X
|
||||
const startY = 50; // 顶部留白
|
||||
|
||||
const treeInfo = layoutSubtree(rootNode, startX, startY);
|
||||
|
||||
// 处理孤立节点
|
||||
const connectedNodeIds = new Set<string>();
|
||||
const collectConnectedNodes = (node: any) => {
|
||||
connectedNodeIds.add(node.id);
|
||||
if (node.children) {
|
||||
node.children.forEach((childId: string) => {
|
||||
const childNode = appState.treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode && !visited.has(childId)) {
|
||||
queue.push({ node: childNode, level: level + 1 });
|
||||
if (childNode) {
|
||||
collectConnectedNodes(childNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
collectConnectedNodes(rootNode);
|
||||
|
||||
const orphanNodes = appState.treeNodes.value.filter(node => !connectedNodeIds.has(node.id));
|
||||
if (orphanNodes.length > 0) {
|
||||
const orphanY = startY + treeInfo.height + 100;
|
||||
orphanNodes.forEach((node, index) => {
|
||||
node.x = startX + (index - orphanNodes.length / 2) * 200;
|
||||
node.y = orphanY + Math.floor(index / 5) * 120;
|
||||
});
|
||||
}
|
||||
|
||||
const nodeWidth = 200;
|
||||
const nodeHeight = 150;
|
||||
const startX = 400;
|
||||
const startY = 100;
|
||||
|
||||
Object.keys(levelNodes).forEach(levelStr => {
|
||||
const level = parseInt(levelStr);
|
||||
const nodes = levelNodes[level];
|
||||
const totalWidth = (nodes.length - 1) * nodeWidth;
|
||||
const offsetX = -totalWidth / 2;
|
||||
|
||||
nodes.forEach((node, index) => {
|
||||
node.x = startX + offsetX + index * nodeWidth;
|
||||
node.y = startY + level * nodeHeight;
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// 强制更新连接线
|
||||
const forceUpdateConnections = () => {
|
||||
connectionManager.updateConnections();
|
||||
}, 100);
|
||||
|
||||
nextTick(() => {
|
||||
connectionManager.updateConnections();
|
||||
|
||||
setTimeout(() => {
|
||||
connectionManager.updateConnections();
|
||||
}, 150);
|
||||
});
|
||||
};
|
||||
|
||||
forceUpdateConnections();
|
||||
|
||||
console.log(`紧凑子树布局完成:${appState.treeNodes.value.length} 个节点已重新排列`);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 验证树结构
|
||||
const validateTree = () => {
|
||||
// 使用改进的验证函数
|
||||
@@ -830,6 +1055,13 @@ export function useBehaviorTreeEditor() {
|
||||
handleBlackboardDragLeave,
|
||||
clearBlackboardReference,
|
||||
|
||||
// 节点类型识别方法
|
||||
getOriginalNodeName,
|
||||
getNodeTemplate,
|
||||
getNodeCategory,
|
||||
isNodeNameCustomized,
|
||||
resetNodeToOriginalName,
|
||||
|
||||
blackboardCollapsed: computed({
|
||||
get: () => blackboardSidebarState.collapsed,
|
||||
set: (value: boolean) => blackboardSidebarState.collapsed = value
|
||||
|
||||
@@ -297,6 +297,17 @@ export function useBlackboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const resetBlackboardToDefaults = () => {
|
||||
if (confirm('确定要重置所有变量到默认值吗?')) {
|
||||
blackboardVariables.value.forEach((variable, name) => {
|
||||
if (variable.defaultValue !== undefined) {
|
||||
variable.value = variable.defaultValue;
|
||||
blackboardVariables.value.set(name, { ...variable });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const exportBlackboard = () => {
|
||||
const data = Array.from(blackboardVariables.value.values());
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
@@ -309,6 +320,19 @@ export function useBlackboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadBlackboardFromArray = (variables: BlackboardVariable[]) => {
|
||||
blackboardVariables.value.clear();
|
||||
variables.forEach(variable => {
|
||||
if (variable.name && variable.type) {
|
||||
blackboardVariables.value.set(variable.name, variable);
|
||||
|
||||
// 展开变量所在的组
|
||||
const groupName = variable.group || '未分组';
|
||||
expandedGroups.value.add(groupName);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importBlackboard = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
@@ -436,6 +460,8 @@ export function useBlackboard() {
|
||||
editVariable,
|
||||
selectVariable,
|
||||
clearBlackboard,
|
||||
resetBlackboardToDefaults,
|
||||
loadBlackboardFromArray,
|
||||
exportBlackboard,
|
||||
importBlackboard,
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ export function useCodeGeneration(
|
||||
treeNodes: Ref<TreeNode[]>,
|
||||
nodeTemplates: Ref<NodeTemplate[]>,
|
||||
getNodeByIdLocal: (id: string) => TreeNode | undefined,
|
||||
rootNode: () => TreeNode | null
|
||||
rootNode: () => TreeNode | null,
|
||||
blackboardVariables?: Ref<Map<string, any>>
|
||||
) {
|
||||
|
||||
// 生成行为树配置JSON
|
||||
@@ -20,7 +21,7 @@ export function useCodeGeneration(
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
const config: any = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
metadata: {
|
||||
@@ -30,6 +31,13 @@ export function useCodeGeneration(
|
||||
},
|
||||
tree: generateNodeConfig(root)
|
||||
};
|
||||
|
||||
// 包含黑板数据
|
||||
if (blackboardVariables && blackboardVariables.value.size > 0) {
|
||||
config.blackboard = Array.from(blackboardVariables.value.values());
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// 生成可读的配置JSON字符串
|
||||
|
||||
@@ -462,25 +462,49 @@ export function useConnectionManager(
|
||||
updateConnections();
|
||||
};
|
||||
|
||||
// 更新连接线
|
||||
// 改进的连接线更新方法
|
||||
const updateConnections = () => {
|
||||
// 立即清空现有连接
|
||||
connections.value.length = 0;
|
||||
|
||||
// 添加一个小延迟,确保DOM已经更新
|
||||
setTimeout(() => {
|
||||
treeNodes.value.forEach(node => {
|
||||
if (node.children) {
|
||||
// 创建新的连接数据
|
||||
const newConnections: Connection[] = [];
|
||||
|
||||
// 遍历所有节点建立连接
|
||||
treeNodes.value.forEach(node => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(childId => {
|
||||
const childNode = treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
// 尝试获取端口位置
|
||||
const parentPos = getPortPosition(node.id, 'output');
|
||||
const childPos = getPortPosition(childId, 'input');
|
||||
|
||||
if (parentPos && childPos) {
|
||||
const controlOffset = Math.abs(childPos.y - parentPos.y) * 0.5;
|
||||
// 计算贝塞尔曲线路径
|
||||
const deltaY = Math.abs(childPos.y - parentPos.y);
|
||||
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
|
||||
|
||||
const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`;
|
||||
|
||||
connections.value.push({
|
||||
newConnections.push({
|
||||
id: `${node.id}-${childId}`,
|
||||
sourceId: node.id,
|
||||
targetId: childId,
|
||||
path: path,
|
||||
active: false
|
||||
});
|
||||
} else {
|
||||
// 如果无法获取实际位置,使用计算位置作为后备
|
||||
const fallbackParentPos = getCalculatedPortPosition(node, 'output');
|
||||
const fallbackChildPos = getCalculatedPortPosition(childNode, 'input');
|
||||
|
||||
const deltaY = Math.abs(fallbackChildPos.y - fallbackParentPos.y);
|
||||
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
|
||||
|
||||
const path = `M ${fallbackParentPos.x} ${fallbackParentPos.y} C ${fallbackParentPos.x} ${fallbackParentPos.y + controlOffset} ${fallbackChildPos.x} ${fallbackChildPos.y - controlOffset} ${fallbackChildPos.x} ${fallbackChildPos.y}`;
|
||||
|
||||
newConnections.push({
|
||||
id: `${node.id}-${childId}`,
|
||||
sourceId: node.id,
|
||||
targetId: childId,
|
||||
@@ -492,7 +516,50 @@ export function useConnectionManager(
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 50); // 50ms延迟,确保DOM渲染完成
|
||||
|
||||
// 批量更新连接
|
||||
connections.value.push(...newConnections);
|
||||
|
||||
// 如果有DOM元素,进行二次精确更新
|
||||
if (canvasAreaRef.value) {
|
||||
setTimeout(() => {
|
||||
// 二次更新,使用实际DOM位置
|
||||
const updatedConnections: Connection[] = [];
|
||||
|
||||
treeNodes.value.forEach(node => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(childId => {
|
||||
const childNode = treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
const parentPos = getPortPosition(node.id, 'output');
|
||||
const childPos = getPortPosition(childId, 'input');
|
||||
|
||||
if (parentPos && childPos) {
|
||||
const deltaY = Math.abs(childPos.y - parentPos.y);
|
||||
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
|
||||
|
||||
const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`;
|
||||
|
||||
updatedConnections.push({
|
||||
id: `${node.id}-${childId}`,
|
||||
sourceId: node.id,
|
||||
targetId: childId,
|
||||
path: path,
|
||||
active: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 如果二次更新得到了有效结果,替换连接数据
|
||||
if (updatedConnections.length > 0) {
|
||||
connections.value.length = 0;
|
||||
connections.value.push(...updatedConnections);
|
||||
}
|
||||
}, 100); // 100ms延迟,确保DOM完全渲染
|
||||
}
|
||||
};
|
||||
|
||||
// 删除连接线
|
||||
|
||||
@@ -11,11 +11,17 @@ interface FileOperationOptions {
|
||||
createTreeFromConfig: (config: any) => TreeNode[];
|
||||
};
|
||||
updateConnections?: () => void;
|
||||
blackboardOperations?: {
|
||||
getBlackboardVariables: () => any[];
|
||||
loadBlackboardVariables: (variables: any[]) => void;
|
||||
clearBlackboard: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface FileData {
|
||||
nodes: TreeNode[];
|
||||
connections: Connection[];
|
||||
blackboard?: any[];
|
||||
metadata: {
|
||||
name: string;
|
||||
created: string;
|
||||
@@ -31,7 +37,8 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
tempConnection,
|
||||
showExportModal,
|
||||
codeGeneration,
|
||||
updateConnections
|
||||
updateConnections,
|
||||
blackboardOperations
|
||||
} = options;
|
||||
|
||||
const hasUnsavedChanges = ref(false);
|
||||
@@ -70,7 +77,7 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
};
|
||||
|
||||
const exportBehaviorTreeData = (): FileData => {
|
||||
return {
|
||||
const data: FileData = {
|
||||
nodes: treeNodes.value,
|
||||
connections: connections.value,
|
||||
metadata: {
|
||||
@@ -79,6 +86,16 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
|
||||
// 包含黑板数据
|
||||
if (blackboardOperations) {
|
||||
const blackboardVariables = blackboardOperations.getBlackboardVariables();
|
||||
if (blackboardVariables.length > 0) {
|
||||
data.blackboard = blackboardVariables;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const showMessage = (message: string, type: 'success' | 'error' = 'success') => {
|
||||
@@ -165,6 +182,12 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
selectedNodeId.value = null;
|
||||
connections.value = [];
|
||||
tempConnection.value.path = '';
|
||||
|
||||
// 清空黑板
|
||||
if (blackboardOperations) {
|
||||
blackboardOperations.clearBlackboard();
|
||||
}
|
||||
|
||||
clearCurrentFile();
|
||||
markAsSaved();
|
||||
}
|
||||
@@ -409,6 +432,11 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
setCurrentFile('untitled', filePath);
|
||||
}
|
||||
|
||||
// 加载黑板数据
|
||||
if (blackboardOperations && parsedData.blackboard && Array.isArray(parsedData.blackboard)) {
|
||||
blackboardOperations.loadBlackboardVariables(parsedData.blackboard);
|
||||
}
|
||||
|
||||
selectedNodeId.value = null;
|
||||
tempConnection.value.path = '';
|
||||
|
||||
@@ -463,6 +491,11 @@ export function useFileOperations(options: FileOperationOptions) {
|
||||
|
||||
tempConnection.value.path = '';
|
||||
|
||||
// 加载黑板数据
|
||||
if (blackboardOperations && config.blackboard && Array.isArray(config.blackboard)) {
|
||||
blackboardOperations.loadBlackboardVariables(config.blackboard);
|
||||
}
|
||||
|
||||
const fileName = file.name.replace(/\.(json|bt)$/, '');
|
||||
setCurrentFile(fileName, '');
|
||||
|
||||
|
||||
@@ -1106,4 +1106,10 @@ export const nodeTemplates: NodeTemplate[] = [
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
];
|
||||
|
||||
// 导出所有节点模板
|
||||
export const allNodeTemplates: NodeTemplate[] = nodeTemplates;
|
||||
|
||||
// 为了保持向后兼容,保留原来的导出
|
||||
export { nodeTemplates as default };
|
||||
@@ -389,14 +389,15 @@
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: #1a202c;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
border: 2px solid #4a5568;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
@@ -404,14 +405,35 @@
|
||||
.form-textarea:focus {
|
||||
border-color: #667eea;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.form-input:hover,
|
||||
.form-select:hover,
|
||||
.form-textarea:hover {
|
||||
border-color: #718096;
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 美化表单下拉选择框 */
|
||||
.form-select {
|
||||
appearance: none;
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 5"><path fill="%23718096" d="M2 0L0 2h4zm0 5L0 3h4z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 16px center;
|
||||
background-size: 12px;
|
||||
padding-right: 48px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-select option {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.form-input::placeholder,
|
||||
@@ -522,8 +544,40 @@
|
||||
}
|
||||
|
||||
.allowed-values textarea {
|
||||
margin-top: 6px;
|
||||
margin-top: 8px;
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
|
||||
border: 2px solid #4a5568;
|
||||
border-radius: 8px;
|
||||
color: #e2e8f0;
|
||||
padding: 12px 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.allowed-values textarea:focus {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.allowed-values textarea:hover {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.allowed-values textarea::placeholder {
|
||||
color: #718096;
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
|
||||
@@ -328,4 +328,66 @@
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 节点类型提示 */
|
||||
.tree-node[data-original-name]:not([data-original-name=""]):hover::after {
|
||||
content: "原始: " attr(data-original-name);
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: #f7fafc;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
animation: tooltipFadeIn 0.3s ease forwards;
|
||||
border: 1px solid #4a5568;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tree-node.node-selected[data-original-name]:not([data-original-name=""]) .node-header::before {
|
||||
content: "📝";
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 8px;
|
||||
border: 2px solid #2d3748;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
@keyframes tooltipFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 节点标题自定义指示器 */
|
||||
.node-title.customized {
|
||||
color: #f6ad55;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.node-title.customized::after {
|
||||
content: " ✏️";
|
||||
font-size: 10px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -235,13 +235,24 @@
|
||||
.property-item select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: #1a202c;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
color: #e2e8f0;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.property-item input:hover,
|
||||
.property-item textarea:hover,
|
||||
.property-item select:hover {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.property-item input:focus,
|
||||
@@ -249,7 +260,26 @@
|
||||
.property-item select:focus {
|
||||
border-color: #667eea;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 美化下拉选择框箭头 */
|
||||
.property-item select {
|
||||
appearance: none;
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 5"><path fill="%23718096" d="M2 0L0 2h4zm0 5L0 3h4z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 10px;
|
||||
padding-right: 36px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.property-item select option {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.property-item .code-input {
|
||||
@@ -521,6 +551,16 @@
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.blackboard-sidebar .reset-btn {
|
||||
background: #ed8936;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .reset-btn:hover {
|
||||
background: #dd6b20;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.blackboard-sidebar .clear-btn {
|
||||
background: #f56565;
|
||||
color: white;
|
||||
@@ -812,6 +852,112 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 黑板变量值区域样式 */
|
||||
.blackboard-sidebar .variable-value {
|
||||
margin-top: 6px;
|
||||
min-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 美化约束值下拉选择框 */
|
||||
.blackboard-sidebar .variable-value select {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 6px;
|
||||
color: #e2e8f0;
|
||||
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
outline: none;
|
||||
appearance: none;
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 5"><path fill="%23718096" d="M2 0L0 2h4zm0 5L0 3h4z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 8px;
|
||||
padding-right: 28px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* 下拉框 hover 状态 */
|
||||
.blackboard-sidebar .variable-value select:hover {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 下拉框 focus 状态 */
|
||||
.blackboard-sidebar .variable-value select:focus {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 下拉框选项样式 */
|
||||
.blackboard-sidebar .variable-value select option {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 不同类型变量的下拉框颜色 */
|
||||
.blackboard-sidebar .variable-item.string .variable-value select {
|
||||
border-left: 3px solid #f59e0b;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-item.string .variable-value select:focus {
|
||||
border-left-color: #f59e0b;
|
||||
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.2), 0 4px 12px rgba(245, 158, 11, 0.15);
|
||||
}
|
||||
|
||||
/* 约束值下拉框的特殊标识 */
|
||||
.blackboard-sidebar .variable-value select::before {
|
||||
content: '🔒';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 8px;
|
||||
transform: translateY(-50%);
|
||||
font-size: 8px;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 添加约束值提示 */
|
||||
.blackboard-sidebar .variable-item:has(.variable-value select) .variable-constraints {
|
||||
color: #f59e0b;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-item:has(.variable-value select) .variable-constraints::before {
|
||||
content: '🔐 ';
|
||||
font-size: 10px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* 禁用状态样式 */
|
||||
.blackboard-sidebar .variable-value select:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: #1a202c;
|
||||
border-color: #2d3748;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .variable-value select:disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.blackboard-sidebar .empty-blackboard {
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
@@ -1098,4 +1244,181 @@
|
||||
font-size: 11px;
|
||||
color: #e83e8c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 节点类型信息区域样式 */
|
||||
.node-type-info {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.node-type-info::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.node-type-info h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
padding: 12px 16px 8px 16px;
|
||||
color: #e2e8f0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.node-type-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.reset-name-btn {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.reset-name-btn:hover {
|
||||
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(217, 119, 6, 0.3);
|
||||
}
|
||||
|
||||
.node-type-details {
|
||||
padding: 12px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.type-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.type-info-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
min-width: 80px;
|
||||
color: #a0aec0;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
color: #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.original-type {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.node-id {
|
||||
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
|
||||
background: rgba(45, 55, 72, 0.8);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
border: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
.original-description {
|
||||
font-style: italic;
|
||||
color: #cbd5e0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.custom-name {
|
||||
font-weight: 600;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
font-size: 9px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.badge-根节点 {
|
||||
background: linear-gradient(135deg, #f6ad55 0%, #ed8936 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-组合 {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-装饰器 {
|
||||
background: linear-gradient(135deg, #9f7aea 0%, #805ad5 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-动作 {
|
||||
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-条件 {
|
||||
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-ecs {
|
||||
background: linear-gradient(135deg, #38b2ac 0%, #319795 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-unknown {
|
||||
background: linear-gradient(135deg, #718096 0%, #4a5568 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 名称输入容器样式 */
|
||||
.name-input-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-indicator {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #f59e0b;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -71,6 +71,42 @@
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* 布局组样式 */
|
||||
.layout-group {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.layout-group button {
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255,255,255,0.8);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.layout-group button:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.layout-group button:active {
|
||||
transform: translateY(0);
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tool-btn.has-changes {
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
</div>
|
||||
<div class="canvas-actions">
|
||||
<button @click="centerView">居中</button>
|
||||
<button @click="autoLayout">自动布局</button>
|
||||
<button @click="autoLayout" title="紧凑子树布局 - 子节点紧贴父节点">自动布局</button>
|
||||
<button @click="validateTree">验证</button>
|
||||
<button @click="clearAllConnections" title="清除所有连接线" v-if="connections.length > 0">清除连线</button>
|
||||
</div>
|
||||
@@ -279,6 +279,7 @@
|
||||
v-for="node in treeNodes"
|
||||
:key="node.id"
|
||||
:data-node-id="node.id"
|
||||
:data-original-name="isNodeNameCustomized(node) ? getOriginalNodeName(node.type) : ''"
|
||||
class="tree-node"
|
||||
:class="[
|
||||
'node-' + node.type,
|
||||
@@ -303,7 +304,7 @@
|
||||
>
|
||||
<div class="node-header">
|
||||
<span class="node-icon">{{ node.icon }}</span>
|
||||
<span class="node-title">{{ node.name }}</span>
|
||||
<span class="node-title" :class="{ 'customized': isNodeNameCustomized(node) }">{{ node.name }}</span>
|
||||
<button class="node-delete" @click.stop="deleteNode(node.id)">×</button>
|
||||
</div>
|
||||
<div class="node-body">
|
||||
@@ -424,17 +425,60 @@
|
||||
</div>
|
||||
|
||||
<div v-if="activeNode" class="node-properties">
|
||||
<!-- 节点类型信息区域 -->
|
||||
<div class="property-section node-type-info">
|
||||
<h4>
|
||||
<span class="node-type-icon">{{ activeNode.icon }}</span>
|
||||
节点类型信息
|
||||
<button
|
||||
class="reset-name-btn"
|
||||
@click="resetNodeToOriginalName"
|
||||
:title="'重置为原始名称: ' + getOriginalNodeName(activeNode.type)"
|
||||
v-if="isNodeNameCustomized(activeNode)"
|
||||
>
|
||||
🔄 恢复原名
|
||||
</button>
|
||||
</h4>
|
||||
<div class="node-type-details">
|
||||
<div class="type-info-row">
|
||||
<span class="info-label">原始类型:</span>
|
||||
<span class="info-value original-type">
|
||||
{{ getOriginalNodeName(activeNode.type) }}
|
||||
<span class="type-badge" :class="'badge-' + getNodeCategory(activeNode.type)">
|
||||
{{ getNodeCategory(activeNode.type) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="type-info-row">
|
||||
<span class="info-label">节点ID:</span>
|
||||
<span class="info-value node-id">{{ activeNode.type }}</span>
|
||||
</div>
|
||||
<div class="type-info-row" v-if="getNodeTemplate(activeNode.type)">
|
||||
<span class="info-label">原始描述:</span>
|
||||
<span class="info-value original-description">{{ getNodeTemplate(activeNode.type).description }}</span>
|
||||
</div>
|
||||
<div class="type-info-row" v-if="isNodeNameCustomized(activeNode)">
|
||||
<span class="info-label">⚠️ 自定义名称:</span>
|
||||
<span class="info-value custom-name">{{ activeNode.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-section">
|
||||
<h4>基本信息</h4>
|
||||
<div class="property-item">
|
||||
<label>节点名称:</label>
|
||||
<input
|
||||
type="text"
|
||||
:value="activeNode.name"
|
||||
@input="updateNodeProperty('name', $event.target.value)"
|
||||
:key="activeNode.id + '_name'"
|
||||
:disabled="activeNode.isConditionNode"
|
||||
>
|
||||
<div class="name-input-container">
|
||||
<input
|
||||
type="text"
|
||||
:value="activeNode.name"
|
||||
@input="updateNodeProperty('name', $event.target.value)"
|
||||
:key="activeNode.id + '_name'"
|
||||
:disabled="activeNode.isConditionNode"
|
||||
:placeholder="getOriginalNodeName(activeNode.type)"
|
||||
>
|
||||
<span v-if="isNodeNameCustomized(activeNode)" class="custom-indicator" title="已自定义名称">✏️</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="property-item">
|
||||
<label>描述:</label>
|
||||
@@ -443,6 +487,7 @@
|
||||
@input="updateNodeProperty('description', $event.target.value)"
|
||||
:key="activeNode.id + '_description'"
|
||||
:disabled="activeNode.isConditionNode"
|
||||
:placeholder="getNodeTemplate(activeNode.type)?.description || '请输入节点描述...'"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
@@ -722,6 +767,7 @@
|
||||
|
||||
<div class="blackboard-actions">
|
||||
<button @click="showBlackboardModal = true" class="add-var-btn">+ 添加变量</button>
|
||||
<button @click="resetBlackboardToDefaults" v-if="blackboardVariables.length > 0" class="reset-btn" title="重置所有变量到默认值">🔄 重置</button>
|
||||
<button @click="clearBlackboard" v-if="blackboardVariables.length > 0" class="clear-btn">清空</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PoolManager } from './Utils/Pool';
|
||||
import { ECSFluentAPI, createECSAPI } from './ECS/Core/FluentAPI';
|
||||
import { Scene } from './ECS/Scene';
|
||||
import { DebugReporter } from './Utils/DebugReporter';
|
||||
import { ICoreConfig, IECSDebugConfig } from './Types';
|
||||
import { ICoreConfig, IECSDebugConfig } from './types';
|
||||
|
||||
/**
|
||||
* 游戏引擎核心类
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IComponent } from '../Types';
|
||||
import type { IComponent } from '../types';
|
||||
|
||||
/**
|
||||
* 游戏组件基类
|
||||
|
||||
@@ -6,8 +6,6 @@ import { ComponentIndexManager, IndexType } from './ComponentIndex';
|
||||
import { ArchetypeSystem } from './ArchetypeSystem';
|
||||
import { DirtyTrackingSystem, DirtyFlag } from './DirtyTrackingSystem';
|
||||
import { EventBus } from './EventBus';
|
||||
import { ECSEventType } from '../CoreEvents';
|
||||
import { IEntityEventData, IComponentEventData } from '../../Types';
|
||||
|
||||
/**
|
||||
* 实体查询构建器
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ISystemEventData,
|
||||
ISceneEventData,
|
||||
IPerformanceEventData
|
||||
} from '../../Types';
|
||||
} from '../../types';
|
||||
import {
|
||||
TypeSafeEventSystem,
|
||||
EventListenerConfig,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Component } from './Component';
|
||||
import { ComponentRegistry, ComponentType } from './Core/ComponentStorage';
|
||||
import { EventBus } from './Core/EventBus';
|
||||
import { ECSEventType } from './CoreEvents';
|
||||
import { IComponentEventData } from '../Types';
|
||||
|
||||
/**
|
||||
* 实体比较器
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Core } from '../../Core';
|
||||
import { Matcher } from '../Utils/Matcher';
|
||||
import { PerformanceMonitor } from '../../Utils/PerformanceMonitor';
|
||||
import type { Scene } from '../Scene';
|
||||
import type { ISystemBase } from '../../Types';
|
||||
import type { ISystemBase } from '../../types';
|
||||
|
||||
/**
|
||||
* 实体系统的基类
|
||||
@@ -115,10 +115,17 @@ export abstract class EntitySystem implements ISystemBase {
|
||||
/**
|
||||
* 系统初始化
|
||||
*
|
||||
* 在系统创建时调用,子类可以重写此方法进行初始化操作。
|
||||
* 在系统创建时调用,自动检查场景中已存在的实体是否匹配此系统。
|
||||
* 子类可以重写此方法进行额外的初始化操作。
|
||||
*/
|
||||
public initialize(): void {
|
||||
// 子类可以重写此方法
|
||||
if (this.scene?.entities?.buffer) {
|
||||
for (const entity of this.scene.entities.buffer) {
|
||||
this.onChanged(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// 子类可以重写此方法进行额外初始化
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
IPerformanceDebugData,
|
||||
IComponentDebugData,
|
||||
ISceneDebugData
|
||||
} from '../Types';
|
||||
} from '../types';
|
||||
import { Core } from '../Core';
|
||||
import { Time } from './Time';
|
||||
|
||||
|
||||
@@ -18,4 +18,4 @@ export * from './ECS';
|
||||
|
||||
// 工具类和类型定义
|
||||
export * from './Utils';
|
||||
export * from './Types';
|
||||
export * from './types';
|
||||
Reference in New Issue
Block a user