remove cocos-extensions

This commit is contained in:
YHH
2025-06-19 21:00:42 +08:00
parent 0107f1f58a
commit 80e2f7df71
67 changed files with 4 additions and 19332 deletions

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "thirdparty/BehaviourTree-ai"]
path = thirdparty/BehaviourTree-ai
url = https://github.com/esengine/BehaviourTree-ai.git
[submodule "thirdparty/admin-backend"]
path = thirdparty/admin-backend
url = https://github.com/esengine/admin-backend.git

View File

@@ -1,54 +0,0 @@
{
"codeGeneration": {
"template": "typescript",
"useStrictMode": true,
"generateComments": true,
"generateImports": true,
"componentSuffix": "Component",
"systemSuffix": "System",
"indentStyle": "spaces",
"indentSize": 4
},
"performance": {
"enableMonitoring": true,
"warningThreshold": 16.67,
"criticalThreshold": 33.33,
"memoryWarningMB": 100,
"memoryCriticalMB": 200,
"maxRecentSamples": 60,
"enableFpsMonitoring": true,
"targetFps": 120
},
"debugging": {
"enableDebugMode": true,
"showEntityCount": true,
"showSystemExecutionTime": true,
"enablePerformanceWarnings": true,
"logLevel": "info",
"enableDetailedLogs": false
},
"editor": {
"autoRefreshAssets": true,
"showWelcomePanelOnStartup": true,
"enableAutoUpdates": false,
"updateChannel": "stable",
"enableNotifications": true
},
"template": {
"defaultEntityName": "ModifiedEntity",
"defaultComponentName": "TestComponent",
"defaultSystemName": "TestSystem",
"createExampleFiles": true,
"includeDocumentation": true,
"useFactoryPattern": true
},
"events": {
"enableEventSystem": true,
"defaultEventPriority": 0,
"enableAsyncEvents": true,
"enableEventBatching": false,
"batchSize": 10,
"batchDelay": 16,
"maxEventListeners": 100
}
}

View File

@@ -1,81 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"description": "面板数据 / Panel data",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z0-9_-]+$": {
"type": "object",
"description": "面板名 / Panel name",
"properties": {
"title": {
"type": "string",
"default": "Default Panel",
"description": "面板标题,支持 i18n:key / Panel title, support for i18n:key (required)"
},
"main": {
"type": "string",
"default": "dist/panels/default/index.js",
"description": "入口函数 / Entry function (required)"
},
"icon": {
"type": "string",
"description": "面板图标存放相对目录 / Relative directory for panel icon storage"
},
"type": {
"type": "string",
"enum": ["dockable", "simple"],
"default": "dockable",
"description": "面板类型dockable | simple / Panel type (dockable | simple)"
},
"flags": {
"type": "object",
"properties": {
"resizable": {
"type": "boolean",
"default": true,
"description": "是否可以改变大小,默认 true / Whether the size can be changed, default true"
},
"save": {
"type": "boolean",
"default": true,
"description": "是否需要保存,默认 false / Whether to save, default false"
},
"alwaysOnTop": {
"type": "boolean",
"default": true,
"description": "是否保持顶层显示,默认 false / Whether to keep the top level display, default false"
}
}
},
"size": {
"type": "object",
"description": "面板大小信息 / Panel size information",
"properties": {
"min-width": {
"type": "number",
"default": 200,
"description": "面板最小宽度 / Minimum panel width"
},
"min-height": {
"type": "number",
"default": 200,
"description": "面板最小高度 / Minimum panel height"
},
"width": {
"type": "number",
"default": 400,
"description": " 面板默认宽度 / Panel Default Width"
},
"height": {
"type": "number",
"default": 600,
"description": "面板默认高度 / Panel Default Height"
}
}
}
},
"required": ["title", "main"]
}
}
}

View File

@@ -1,9 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"description": "其他扩展插件的扩展配置 / Extended configuration for other extension plugins",
"properties": {
},
"required": []
}

View File

@@ -1,64 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"description": "插件定义文件 / Extension definition file",
"properties": {
"author": {
"type": "string",
"description": "作者 / Author",
"default": "Cocos Creator Developer"
},
"contributions": {
"$ref": "./contributions/index.json"
},
"dependencies": {
"type": "object",
"description": "发布时所需的依赖库 / Dependencies required for publishing"
},
"description": {
"type": "string",
"description": "简要介绍扩展关键特性、用途,支持 i18n / Brief introduction of the key features and uses of the extension, supporting i18n"
},
"devDependencies": {
"type": "object",
"description": "开发时所需的依赖库 / Dependencies required for development"
},
"editor": {
"type": "string",
"description": "支持的 Cocos Creator 编辑器版本,支持 semver 格式 / Supported Cocos Creator editor version, supporting semver format"
},
"main": {
"type": "string",
"description": "入口函数 / Entry function",
"default": "./dist/index.js"
},
"name": {
"type": "string",
"description": "不能以 _ 或 . 开头、不能含有大写字母,也不能含有 URL 的非法字符例如 .、' 和 ,。 / Cannot start with _ or., cannot contain uppercase letters, and cannot contain URL illegal characters such as.,'and,",
"default": "Custom Extension"
},
"package_version": {
"type": "number",
"description": "扩展系统预留版本号 / Extension system reserved version number",
"default": 2
},
"panels": {
"$ref": "./base/panels.json"
},
"scripts": {
"type": "object",
"description": "NPM 脚本 / NPM scripts"
},
"version": {
"type": "string",
"description": "版本号字符串 / Version number string",
"default": "1.0.0"
}
},
"required": [
"author",
"name",
"package_version",
"version"
]
}

View File

@@ -1,81 +0,0 @@
# ECS Framework for Cocos Creator - 开发扩展插件
专业的ECS框架开发助手为Cocos Creator提供完整的实体组件系统(ECS)开发工具链。
## 🎯 主要功能
### 📦 一键安装管理
- **自动检测**实时检测ECS框架安装状态和版本信息
- **一键安装**:快速安装 `@esengine/ecs-framework` 到当前项目
- **版本管理**:自动检查更新,支持一键更新到最新版本
- **智能卸载**:安全卸载框架,保护项目完整性
### 🚀 代码生成器
- **智能生成**:输入功能名称,自动生成对应的组件和系统代码
- **多种系统类型**支持EntitySystem、ProcessingSystem、IntervalSystem、PassiveSystem
- **组件配置**:可选择添加属性、注释等定制化选项
- **组件过滤**:支持生成带组件过滤的高级系统
### 🛠️ 项目模板
- **快速启动**一键生成完整的ECS项目结构
- **预设组件**包含位置、速度、Cocos节点等常用组件
- **系统示例**:提供移动系统、节点同步系统等实用示例
- **工厂模式**:包含实体工厂和场景管理器模板
### 🔍 调试工具
- **实时监控**查看ECS框架运行状态和性能数据
- **组件池监控**:实时监控组件对象池使用情况
- **性能分析**:提供详细的性能统计和优化建议
## 📋 面板介绍
### 欢迎面板
- ECS框架安装状态检测
- 一键安装、更新、卸载操作
- 项目模板生成
- 快速访问文档和GitHub
### 代码生成器
- 可视化代码生成界面
- 实时预览生成的代码结构
- 支持批量生成多个文件
### 调试面板
- 实时性能监控
- 组件池状态查看
- 系统运行统计
## 🔧 开发环境
- **Cocos Creator**: >= 3.8.6
- **Node.js**: >= 14.0.0
- **依赖框架**: @esengine/ecs-framework
## 📥 安装使用
1. 将插件复制到项目的 `extensions` 目录
2. 在Cocos Creator中启用插件
3. 通过菜单 `面板 -> ECS Framework -> 欢迎面板` 打开主界面
4. 按照界面提示安装ECS框架并开始开发
## 🚀 快速开始
1. **安装框架**:在欢迎面板点击"安装 ECS Framework"
2. **创建模板**:点击"创建ECS模板"生成项目结构
3. **生成代码**:使用代码生成器快速创建组件和系统
4. **开始开发**基于生成的模板开始您的ECS游戏开发
## 📚 更多资源
- **GitHub仓库**[https://github.com/esengine/ecs-framework](https://github.com/esengine/ecs-framework)
- **完整文档**包含详细的API文档和教程
- **技术交流**加入QQ群获取技术支持和交流
## ⭐ 特色优势
- **零配置**:开箱即用,无需复杂配置
- **可视化**:图形化界面,操作简单直观
- **高效率**:大幅减少重复代码编写
- **专业性**基于成熟的ECS框架设计模式
让ECS开发变得简单高效专注于游戏逻辑而非框架配置

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://schemastore.azurewebsites.net/schemas/json/tsconfig.json",
"compilerOptions": {
"target": "ES2017",
"module": "CommonJS",
"moduleResolution": "node",
"inlineSourceMap": true,
"inlineSources": true,
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./source",
"types": [
"node",
"@cocos/creator-types/editor",
]
}
}

View File

@@ -1,15 +0,0 @@
"use strict";
module.exports = {
description: "Professional ECS Framework Development Assistant: One-click installation of @esengine/ecs-framework, intelligent code generator for quick creation of components and systems, project template generation, real-time status detection and version management. Provides welcome panel, debug panel, code generator and behavior tree AI component library to make ECS development in Cocos Creator more efficient and convenient.",
open_panel: "Default Panel",
send_to_panel: "Send message to panel",
menu: {
panel: "Panel",
develop: "Develop",
create: "Create",
open: "Open"
}
};

View File

@@ -1,18 +0,0 @@
"use strict";
module.exports = {
// 插件描述
description: "专业的ECS框架开发助手一键安装@esengine/ecs-framework智能代码生成器快速创建组件和系统项目模板生成实时状态检测和版本管理。提供欢迎面板、调试面板、代码生成器和行为树AI组件库让Cocos Creator的ECS开发更高效便捷。",
// 面板相关
open_panel: "默认面板",
send_to_panel: "发送消息给面板",
// 菜单相关
menu: {
panel: "面板",
develop: "开发",
create: "创建",
open: "打开"
}
};

View File

@@ -1,318 +0,0 @@
{
"name": "cocos-ecs-extension",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cocos-ecs-extension",
"version": "1.0.0",
"hasInstallScript": true,
"dependencies": {
"fs-extra": "^10.0.0",
"vue": "^3.1.4",
"ws": "^8.14.2"
},
"devDependencies": {
"@cocos/creator-types": "^3.8.6",
"@types/fs-extra": "^9.0.5",
"@types/node": "^18.17.1",
"@types/ws": "^8.5.10",
"typescript": "^5.8.2"
}
},
"node_modules/@babel/parser": {
"version": "7.23.0",
"license": "MIT",
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@cocos/creator-types": {
"version": "3.8.6",
"resolved": "https://registry.npmjs.org/@cocos/creator-types/-/creator-types-3.8.6.tgz",
"integrity": "sha512-hyZ4aoqqLxoRtKbBLSJM5RgtK3oGOlTEryHDcyH4znq3h9cFk+MSbQC2aJHvK5/bMlJzsZ641/hD77RGSrvo8Q==",
"dev": true
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"license": "MIT"
},
"node_modules/@types/fs-extra": {
"version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
"integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "18.19.111",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.111.tgz",
"integrity": "sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.21.3",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-ssr": "3.3.4",
"@vue/reactivity-transform": "3.3.4",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0",
"postcss": "^8.1.10",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/reactivity": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/reactivity-transform": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.0"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/runtime-core": "3.3.4",
"@vue/shared": "3.3.4",
"csstype": "^3.1.1"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.3.4",
"@vue/shared": "3.3.4"
},
"peerDependencies": {
"vue": "3.3.4"
}
},
"node_modules/@vue/shared": {
"version": "3.3.4",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.2",
"license": "MIT"
},
"node_modules/estree-walker": {
"version": "2.0.2",
"license": "MIT"
},
"node_modules/fs-extra": {
"version": "10.1.0",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"license": "ISC"
},
"node_modules/jsonfile": {
"version": "6.1.0",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/magic-string": {
"version": "0.30.3",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/nanoid": {
"version": "3.3.6",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.4.30",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/universalify": {
"version": "2.0.0",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/vue": {
"version": "3.3.4",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-sfc": "3.3.4",
"@vue/runtime-dom": "3.3.4",
"@vue/server-renderer": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@@ -1,234 +0,0 @@
{
"$schema": "./@types/schema/package/index.json",
"package_version": 2,
"name": "cocos-ecs-extension",
"version": "1.0.0",
"author": "esengine",
"editor": ">=3.8.6",
"scripts": {
"preinstall": "node ./scripts/preinstall.js",
"build": "npx tsc"
},
"description": "i18n:cocos-ecs-extension.description",
"main": "./dist/main.js",
"dependencies": {
"vue": "^3.1.4",
"fs-extra": "^10.0.0",
"ws": "^8.14.2",
"adm-zip": "^0.5.10"
},
"devDependencies": {
"@cocos/creator-types": "^3.8.6",
"@types/fs-extra": "^9.0.5",
"@types/node": "^18.17.1",
"@types/ws": "^8.5.10",
"@types/adm-zip": "^0.5.0",
"typescript": "^5.8.2"
},
"panels": {
"default": {
"title": "ECS Framework - 欢迎面板",
"type": "dockable",
"main": "dist/panels/default/index.js",
"size": {
"min-width": 450,
"min-height": 600,
"width": 850,
"height": 800
}
},
"debug": {
"title": "ECS Framework - 调试面板",
"type": "dockable",
"main": "dist/panels/debug/index.js",
"size": {
"min-width": 400,
"min-height": 500,
"width": 500,
"height": 600
}
},
"generator": {
"title": "ECS Framework - 代码生成器",
"type": "dockable",
"main": "dist/panels/generator/index.js",
"size": {
"min-width": 600,
"min-height": 500,
"width": 900,
"height": 700
}
},
"behavior-tree": {
"title": "ECS Framework - 行为树AI组件库",
"type": "dockable",
"main": "dist/panels/behavior-tree/index.js",
"size": {
"min-width": 700,
"min-height": 600,
"width": 1000,
"height": 800
}
}
},
"contributions": {
"scene": {
"script": "./dist/scene.js"
},
"menu": [
{
"path": "i18n:menu.panel/ECS Framework",
"label": "欢迎面板",
"message": "open-panel"
},
{
"path": "i18n:menu.panel/ECS Framework",
"label": "调试面板",
"message": "open-debug"
},
{
"path": "i18n:menu.panel/ECS Framework",
"label": "代码生成器",
"message": "open-generator"
},
{
"path": "i18n:menu.panel/ECS Framework",
"label": "行为树AI组件库",
"message": "open-behavior-tree"
},
{
"path": "i18n:menu.develop/ECS Framework",
"label": "ECS 开发工具",
"message": "open-panel"
},
{
"path": "i18n:menu.panel/ECS Framework",
"label": "检查更新",
"message": "check-plugin-updates"
}
],
"assets": {
"menu": {
"methods": "./dist/assets-menu.js",
"assetMenu": "onAssetMenu"
}
},
"messages": {
"open-panel": {
"methods": [
"openPanel"
]
},
"install-ecs-framework": {
"methods": [
"install-ecs-framework"
]
},
"update-ecs-framework": {
"methods": [
"update-ecs-framework"
]
},
"uninstall-ecs-framework": {
"methods": [
"uninstall-ecs-framework"
]
},
"open-documentation": {
"methods": [
"open-documentation"
]
},
"create-ecs-template": {
"methods": [
"create-ecs-template"
]
},
"open-github": {
"methods": [
"open-github"
]
},
"open-qq-group": {
"methods": [
"open-qq-group"
]
},
"open-debug": {
"methods": [
"open-debug"
]
},
"open-generator": {
"methods": [
"open-generator"
]
},
"open-behavior-tree": {
"methods": [
"open-behavior-tree"
]
},
"install-behavior-tree": {
"methods": [
"install-behavior-tree"
]
},
"update-behavior-tree": {
"methods": [
"update-behavior-tree"
]
},
"check-behavior-tree-installed": {
"methods": [
"check-behavior-tree-installed"
]
},
"check-plugin-updates": {
"methods": [
"check-plugin-updates"
]
},
"set-hot-update-config": {
"methods": [
"set-hot-update-config"
]
},
"get-hot-update-config": {
"methods": [
"get-hot-update-config"
]
},
"open-behavior-tree-docs": {
"methods": [
"open-behavior-tree-docs"
]
},
"create-behavior-tree-file": {
"methods": [
"create-behavior-tree-file"
]
},
"load-behavior-tree-file": {
"methods": [
"load-behavior-tree-file"
]
},
"create-behavior-tree-from-editor": {
"methods": [
"create-behavior-tree-from-editor"
]
},
"overwrite-behavior-tree-file": {
"methods": [
"overwrite-behavior-tree-file"
]
},
"behavior-tree-panel-load-file": {
"methods": [
"behavior-tree.loadBehaviorTreeFile"
]
}
}
}
}

View File

@@ -1 +0,0 @@
const readFileSync=require("fs")["readFileSync"],join=require("path")["join"],spawnSync=require("child_process")["spawnSync"],PATH={packageJSON:join(__dirname,"../package.json")};function checkCreatorTypesVersion(e){var o="win32"===process.platform?"npm.cmd":"npm";let n=spawnSync(o,["view","@cocos/creator-types","versions"]).stdout.toString();try{n=JSON.parse(listString)}catch(e){}return!!n.includes(e)}try{const e=readFileSync(PATH.packageJSON,"utf8"),f=JSON.parse(e),g=f.devDependencies["@cocos/creator-types"].replace(/^[^\d]+/,"");checkCreatorTypesVersion(g)||(console.log("Warning:"),console.log(" @en"),console.log(" Version check of @cocos/creator-types failed."),console.log(` The definition of ${g} has not been released yet. Please export the definition to the ./node_modules directory by selecting "Developer -> Export Interface Definition" in the menu of the Creator editor.`),console.log(" The definition of the corresponding version will be released on npm after the editor is officially released."),console.log(" @zh"),console.log(" @cocos/creator-types 版本检查失败。"),console.log(` ${g} 定义还未发布,请先通过 Creator 编辑器菜单 "开发者 -> 导出接口定义",导出定义到 ./node_modules 目录。`),console.log(" 对应版本的定义会在编辑器正式发布后同步发布到 npm 上。"))}catch(e){console.error(e)}

View File

@@ -1,274 +0,0 @@
import { ensureDir, writeFile } from 'fs-extra';
import { join } from 'path';
/**
* 代码生成器工具类
* 用于生成基础的ECS框架代码
*/
interface ComponentOptions {
includeComments: boolean;
addProperties: string[];
}
interface SystemOptions {
includeComments: boolean;
systemType: 'EntitySystem' | 'ProcessingSystem' | 'IntervalSystem' | 'PassiveSystem';
requiredComponents: string[];
}
export class CodeGenerator {
/**
* 生成组件代码
*/
public async generateComponent(
name: string,
targetDir: string,
options: ComponentOptions = {
includeComments: true,
addProperties: []
}
): Promise<void> {
const className = `${name}Component`;
const fileName = `${className}.ts`;
const filePath = join(targetDir, fileName);
await ensureDir(targetDir);
const comments = options.includeComments ? this.generateComponentComments(className) : '';
const properties = this.generateComponentProperties(options.addProperties);
const content = `import { Component } from '@esengine/ecs-framework';
${comments}
export class ${className} extends Component {
${properties}
constructor() {
super();
}
/**
* 重置组件状态
*/
public reset(): void {
// 重置组件属性到默认值
${this.generateResetCode(options.addProperties)}
}
}
`;
await writeFile(filePath, content, 'utf-8');
}
/**
* 生成系统代码
*/
public async generateSystem(
name: string,
targetDir: string,
options: SystemOptions = {
includeComments: true,
systemType: 'EntitySystem',
requiredComponents: []
}
): Promise<void> {
const className = `${name}System`;
const fileName = `${className}.ts`;
const filePath = join(targetDir, fileName);
await ensureDir(targetDir);
const comments = options.includeComments ? this.generateSystemComments(className, options.systemType) : '';
const imports = this.getSystemImports(options.systemType, options.requiredComponents);
const matcherSetup = options.requiredComponents.length > 0 ?
`Matcher.empty().all(${options.requiredComponents.join(', ')})` :
`Matcher.empty()`;
const processMethod = this.generateProcessMethod(options.systemType, options.requiredComponents, className);
const content = `${imports}
${comments}
export class ${className} extends ${options.systemType} {
constructor() {
super(${matcherSetup}${options.systemType === 'IntervalSystem' ? ', 1000 / 60' : ''})${options.systemType === 'IntervalSystem' ? '; // 60fps' : ';'}
}
${processMethod}
/**
* 系统开始时调用
*/
public begin(): void {
super.begin();
// 添加系统初始化逻辑
}
/**
* 系统结束时调用
*/
public end(): void {
// 添加系统清理逻辑
super.end();
}
}
`;
await writeFile(filePath, content, 'utf-8');
}
// ============ 辅助方法 ============
private generateComponentComments(className: string): string {
return `/**
* ${className}
*
* 组件描述
*
* @example
* \`\`\`typescript
* const entity = scene.createEntity("Example");
* const component = entity.addComponent(new ${className}());
* \`\`\`
*/`;
}
private generateSystemComments(className: string, systemType: string): string {
const descriptions = {
'EntitySystem': '处理拥有特定组件的实体',
'ProcessingSystem': '执行全局游戏逻辑',
'IntervalSystem': '按时间间隔处理实体',
'PassiveSystem': '被动响应事件或手动调用'
};
return `/**
* ${className}
*
* ${descriptions[systemType as keyof typeof descriptions] || '处理游戏逻辑'}
*
* @example
* \`\`\`typescript
* const system = new ${className}();
* scene.addEntityProcessor(system);
* \`\`\`
*/`;
}
private generateComponentProperties(properties: string[]): string {
if (properties.length === 0) {
return ' // 添加组件属性\n // public value: number = 0;';
}
return properties.map(prop => {
const [name, type = 'number', defaultValue = '0'] = prop.split(':');
return ` public ${name}: ${type} = ${defaultValue};`;
}).join('\n');
}
private generateResetCode(properties: string[]): string {
if (properties.length === 0) {
return ' // this.value = 0;';
}
return properties.map(prop => {
const [name, , defaultValue = '0'] = prop.split(':');
return ` this.${name} = ${defaultValue};`;
}).join('\n');
}
private getSystemImports(systemType: string, requiredComponents: string[]): string {
const imports = [systemType, 'Entity'];
// 所有系统类型都可能需要Matcher来过滤组件
if (requiredComponents.length > 0 || systemType === 'EntitySystem' || systemType === 'IntervalSystem' || systemType === 'PassiveSystem') {
imports.push('Matcher');
}
return `import { ${imports.join(', ')} } from '@esengine/ecs-framework';${requiredComponents.length > 0 ? '\n' + this.generateComponentImports(requiredComponents) : ''}`;
}
private generateComponentImports(components: string[]): string {
return components.map(comp => `import { ${comp} } from '../components/${comp}';`).join('\n');
}
private generateProcessMethod(systemType: string, requiredComponents: string[], className: string): string {
switch (systemType) {
case 'EntitySystem':
return ` protected process(entities: Entity[]): void {
for (const entity of entities) {
this.processEntity(entity);
}
}
private processEntity(entity: Entity): void {
${this.generateProcessingLogic(requiredComponents)}
}`;
case 'ProcessingSystem':
return ` public processSystem(): void {
// 添加全局系统逻辑
console.log('${className} processSystem called');
}`;
case 'IntervalSystem':
return ` protected process(entities: Entity[]): void {
const intervalDelta = this.getIntervalDelta();
console.log(\`${className} executing with interval delta: \${intervalDelta}\`);
for (const entity of entities) {
this.processEntity(entity, intervalDelta);
}
}
private processEntity(entity: Entity, delta: number): void {
${this.generateProcessingLogic(requiredComponents)}
}`;
case 'PassiveSystem':
return ` /**
* 被动系统不主动处理实体
* 通常用于响应事件或被其他系统调用
*/
public processEntity(entity: Entity): void {
${this.generateProcessingLogic(requiredComponents)}
}
/**
* 手动触发处理
*/
public trigger(): void {
for (const entity of this.entities) {
this.processEntity(entity);
}
}`;
default:
return '';
}
}
private generateProcessingLogic(requiredComponents: string[]): string {
if (requiredComponents.length === 0) {
return ' // 添加处理逻辑';
}
const componentVars = requiredComponents.map((comp: string) => {
const varName = comp.replace('Component', '').toLowerCase();
return ` const ${varName} = entity.getComponent(${comp});`;
}).join('\n');
const nullCheck = requiredComponents.map((comp: string) => {
const varName = comp.replace('Component', '').toLowerCase();
return varName;
}).join(' && ');
return `${componentVars}
if (${nullCheck}) {
// 添加处理逻辑
}`;
}
}

View File

@@ -1,436 +0,0 @@
import * as path from 'path';
import * as fs from 'fs';
/**
* ECS启动模板生成器
* 生成最基础的ECS框架启动模板不包含业务逻辑
*/
export class TemplateGenerator {
private projectPath: string;
private ecsDir: string;
constructor(projectPath: string) {
this.projectPath = projectPath;
this.ecsDir = path.join(projectPath, 'assets', 'scripts', 'ecs');
}
/**
* 检查是否已经存在ECS模板
*/
public checkTemplateExists(): boolean {
return fs.existsSync(this.ecsDir);
}
/**
* 获取已存在的文件列表
*/
public getExistingFiles(): string[] {
if (!this.checkTemplateExists()) return [];
const files: string[] = [];
this.scanDirectory(this.ecsDir, '', files);
return files;
}
private scanDirectory(dirPath: string, relativePath: string, files: string[]): void {
if (!fs.existsSync(dirPath)) return;
const items = fs.readdirSync(dirPath);
for (const item of items) {
const fullPath = path.join(dirPath, item);
const relativeFilePath = relativePath ? `${relativePath}/${item}` : item;
if (fs.statSync(fullPath).isDirectory()) {
this.scanDirectory(fullPath, relativeFilePath, files);
} else {
files.push(relativeFilePath);
}
}
}
/**
* 删除现有的ECS模板
*/
public removeExistingTemplate(): void {
if (fs.existsSync(this.ecsDir)) {
fs.rmSync(this.ecsDir, { recursive: true, force: true });
console.log('Removed existing ECS template');
}
}
/**
* 创建ECS启动模板
*/
public createTemplate(): void {
// 创建目录结构
this.createDirectories();
// 创建ECS启动管理器
this.createECSManager();
// 创建基础游戏场景
this.createBaseGameScene();
// 创建README文档
this.createReadme();
console.log('ECS启动模板创建成功');
}
/**
* 创建目录结构
*/
private createDirectories(): void {
const dirs = [
this.ecsDir,
path.join(this.ecsDir, 'scenes'),
path.join(this.ecsDir, 'components'),
path.join(this.ecsDir, 'systems')
];
dirs.forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
console.log(`Created directory: ${path.relative(this.projectPath, dir)}`);
}
});
}
/**
* 创建ECS管理器
*/
private createECSManager(): void {
this.writeFile(path.join(this.ecsDir, 'ECSManager.ts'), `import { Core } from '@esengine/ecs-framework';
import { Component, _decorator } from 'cc';
import { GameScene } from './scenes/GameScene';
const { ccclass, property } = _decorator;
/**
* ECS管理器 - Cocos Creator组件
* 将此组件添加到场景中的任意节点上即可启动ECS框架
*
* 使用说明:
* 1. 在Cocos Creator场景中创建一个空节点
* 2. 将此ECSManager组件添加到该节点
* 3. 运行场景即可自动启动ECS框架
*/
@ccclass('ECSManager')
export class ECSManager extends Component {
@property({
tooltip: '是否启用调试模式(建议开发阶段开启)'
})
public debugMode: boolean = true;
private isInitialized: boolean = false;
/**
* 组件启动时初始化ECS
*/
start() {
this.initializeECS();
}
/**
* 初始化ECS框架
*/
private initializeECS(): void {
if (this.isInitialized) return;
console.log('🎮 正在初始化ECS框架...');
try {
// 1. 创建Core实例启用调试功能
if (this.debugMode) {
Core.create({
debugConfig: {
enabled: true,
websocketUrl: 'ws://localhost:8080/ecs-debug',
autoReconnect: true,
updateInterval: 100,
channels: {
entities: true,
systems: true,
performance: true,
components: true,
scenes: true
}
}
});
console.log('🔧 ECS调试模式已启用可在Cocos Creator扩展面板中查看调试信息');
} else {
Core.create(false);
}
// 2. 创建游戏场景
const gameScene = new GameScene();
// 3. 设置为当前场景会自动调用scene.begin()
Core.scene = gameScene;
this.isInitialized = true;
console.log('✅ ECS框架初始化成功');
console.log('📖 请查看 assets/scripts/ecs/README.md 了解如何添加组件和系统');
} catch (error) {
console.error('❌ ECS框架初始化失败:', error);
}
}
/**
* 每帧更新ECS框架
*/
update(deltaTime: number) {
if (this.isInitialized) {
// 更新ECS核心系统
Core.update(deltaTime);
}
}
/**
* 组件销毁时清理ECS
*/
onDestroy() {
if (this.isInitialized) {
console.log('🧹 清理ECS框架...');
// ECS框架会自动处理场景清理
this.isInitialized = false;
}
}
}
`);
}
/**
* 创建基础游戏场景
*/
private createBaseGameScene(): void {
this.writeFile(path.join(this.ecsDir, 'scenes', 'GameScene.ts'), `import { Scene } from '@esengine/ecs-framework';
/**
* 游戏场景
*
* 这是您的主游戏场景。在这里可以:
* - 添加游戏系统
* - 创建初始实体
* - 设置场景参数
*/
export class GameScene extends Scene {
/**
* 场景初始化
* 在场景创建时调用,用于设置基础配置
*/
public initialize(): void {
super.initialize();
// 设置场景名称
this.name = "MainGameScene";
console.log('🎯 游戏场景已创建');
// TODO: 在这里添加您的游戏系统
// 例如this.addEntityProcessor(new MovementSystem());
// TODO: 在这里创建初始实体
// 例如this.createEntity("Player");
}
/**
* 场景开始运行
* 在场景开始时调用,用于执行启动逻辑
*/
public onStart(): void {
super.onStart();
console.log('🚀 游戏场景已启动');
// TODO: 在这里添加场景启动逻辑
// 例如创建UI、播放音乐、初始化游戏状态等
}
/**
* 场景卸载
* 在场景结束时调用,用于清理资源
*/
public unload(): void {
console.log('🛑 游戏场景已结束');
// TODO: 在这里添加清理逻辑
// 例如:清理缓存、释放资源等
super.unload();
}
}
`);
}
/**
* 创建README文档
*/
private createReadme(): void {
this.writeFile(path.join(this.ecsDir, 'README.md'), `# ECS框架启动模板
欢迎使用ECS框架这是一个最基础的启动模板帮助您快速开始ECS项目开发。
## 📁 项目结构
\`\`\`
ecs/
├── components/ # 组件目录(请在此添加您的组件)
├── systems/ # 系统目录(请在此添加您的系统)
├── scenes/ # 场景目录
│ └── GameScene.ts # 主游戏场景
├── ECSManager.ts # ECS管理器组件
└── README.md # 本文档
\`\`\`
## 🚀 快速开始
### 1. 启动ECS框架
ECS框架已经配置完成您只需要
1. 在Cocos Creator中打开您的场景
2. 创建一个空节点(例如命名为"ECSManager"
3. 将 \`ECSManager\` 组件添加到该节点
4. 运行场景ECS框架将自动启动
### 2. 查看控制台输出
如果一切正常,您将在控制台看到:
\`\`\`
🎮 正在初始化ECS框架...
🔧 ECS调试模式已启用可在Cocos Creator扩展面板中查看调试信息
🎯 游戏场景已创建
✅ ECS框架初始化成功
🚀 游戏场景已启动
\`\`\`
### 3. 使用调试面板
ECS框架已启用调试功能您可以
1. 在Cocos Creator编辑器菜单中选择 "扩展" → "ECS Framework" → "调试面板"
2. 调试面板将显示实时的ECS运行状态
- 实体数量和状态
- 系统执行信息
- 性能监控数据
- 组件统计信息
**注意**:调试功能会消耗一定性能,正式发布时建议关闭调试模式。
## 📚 下一步开发
### 创建您的第一个组件
\`components/\` 目录下创建组件:
\`\`\`typescript
// components/PositionComponent.ts
import { Component } from '@esengine/ecs-framework';
import { Vec3 } from 'cc';
export class PositionComponent extends Component {
public position: Vec3 = new Vec3();
constructor(x: number = 0, y: number = 0, z: number = 0) {
super();
this.position.set(x, y, z);
}
}
\`\`\`
### 创建您的第一个系统
\`systems/\` 目录下创建系统:
\`\`\`typescript
// systems/MovementSystem.ts
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
import { PositionComponent } from '../components/PositionComponent';
export class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(PositionComponent));
}
protected process(entities: Entity[]): void {
for (const entity of entities) {
const position = entity.getComponent(PositionComponent);
if (position) {
// TODO: 在这里编写移动逻辑
console.log(\`实体 \${entity.name} 位置: \${position.position}\`);
}
}
}
}
\`\`\`
### 在场景中注册系统
\`scenes/GameScene.ts\`\`initialize()\` 方法中添加:
\`\`\`typescript
import { MovementSystem } from '../systems/MovementSystem';
public initialize(): void {
super.initialize();
this.name = "MainGameScene";
// 添加系统
this.addEntityProcessor(new MovementSystem());
// 创建测试实体
const testEntity = this.createEntity("TestEntity");
testEntity.addComponent(new PositionComponent(0, 0, 0));
}
\`\`\`
## 🔗 学习资源
- [ECS框架完整文档](https://github.com/esengine/ecs-framework)
- [ECS概念详解](https://github.com/esengine/ecs-framework/blob/master/docs/concepts-explained.md)
- [新手教程](https://github.com/esengine/ecs-framework/blob/master/docs/beginner-tutorials.md)
- [组件设计指南](https://github.com/esengine/ecs-framework/blob/master/docs/component-design-guide.md)
- [系统开发指南](https://github.com/esengine/ecs-framework/blob/master/docs/system-guide.md)
## 💡 开发提示
1. **组件只存储数据**:避免在组件中编写复杂逻辑
2. **系统处理逻辑**:所有业务逻辑应该在系统中实现
3. **使用Matcher过滤实体**系统通过Matcher指定需要处理的实体类型
4. **性能优化**:大量实体时考虑使用位掩码查询和组件索引
## ❓ 常见问题
### Q: 如何创建实体?
A: 在场景中使用 \`this.createEntity("实体名称")\`
### Q: 如何给实体添加组件?
A: 使用 \`entity.addComponent(new YourComponent())\`
### Q: 如何获取实体的组件?
A: 使用 \`entity.getComponent(YourComponent)\`
### Q: 如何删除实体?
A: 使用 \`entity.destroy()\`\`this.destroyEntity(entity)\`
---
🎮 **开始您的ECS开发之旅吧**
如有问题请查阅官方文档或提交Issue。
`);
}
/**
* 写入文件
*/
private writeFile(filePath: string, content: string): void {
fs.writeFileSync(filePath, content, 'utf8');
console.log(`Created file: ${path.relative(this.projectPath, filePath)}`);
}
}

View File

@@ -1,46 +0,0 @@
export function onAssetMenu(assetInfo: any) {
console.log('[AssetMenu] onAssetMenu 被调用,资源信息:', assetInfo);
console.log('[AssetMenu] assetInfo 完整结构:', JSON.stringify(assetInfo, null, 2));
const menuItems = [];
// 检查是否为行为树文件
const isTargetFile = (assetInfo && assetInfo.name && assetInfo.name.endsWith('.bt.json')) ||
(assetInfo && assetInfo.file && assetInfo.file.endsWith('.bt.json'));
if (isTargetFile) {
console.log('[AssetMenu] 发现 .bt.json 文件,添加菜单项');
menuItems.push({
label: '用行为树编辑器打开',
click() {
console.log('[AssetMenu] 菜单项被点击,文件信息:', assetInfo);
// 直接调用主进程的方法,不需要复杂的序列化
try {
Editor.Message.send('cocos-ecs-extension', 'load-behavior-tree-file', assetInfo);
console.log('[AssetMenu] 消息发送成功');
} catch (error) {
console.error('[AssetMenu] 消息发送失败:', error);
}
}
});
}
// 在目录中添加创建选项
if (assetInfo && assetInfo.isDirectory) {
menuItems.push({
label: '创建行为树文件',
click() {
console.log('[AssetMenu] 在目录中创建行为树文件:', assetInfo);
try {
Editor.Message.send('cocos-ecs-extension', 'create-behavior-tree-file');
} catch (error) {
console.error('[AssetMenu] 创建消息发送失败:', error);
}
}
});
}
console.log('[AssetMenu] 返回菜单项数量:', menuItems.length);
return menuItems;
}

View File

@@ -1,351 +0,0 @@
import { exec } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import * as fsExtra from 'fs-extra';
/**
* 行为树相关的处理器
*/
export class BehaviorTreeHandler {
/**
* 安装行为树AI系统
*/
static async install(): Promise<boolean> {
const projectPath = Editor.Project.path;
const command = 'npm install @esengine/ai';
return new Promise((resolve) => {
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
if (error) {
console.error('AI系统安装失败:', error.message);
resolve(false);
} else {
// 验证安装是否成功
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ai');
const installSuccess = fs.existsSync(nodeModulesPath);
if (!installSuccess) {
console.warn('安装完成但未找到AI系统目录请检查网络连接');
}
resolve(installSuccess);
}
});
});
}
/**
* 更新行为树AI系统
*/
static async update(): Promise<boolean> {
const projectPath = Editor.Project.path;
const command = 'npm update @esengine/ai';
return new Promise((resolve) => {
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
if (error) {
console.error('AI系统更新失败:', error.message);
resolve(false);
} else {
// 验证更新是否成功
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ai');
const updateSuccess = fs.existsSync(nodeModulesPath);
if (!updateSuccess) {
console.warn('更新完成但未找到AI系统目录');
}
resolve(updateSuccess);
}
});
});
}
/**
* 检查行为树AI是否已安装
*/
static checkInstalled(): boolean {
try {
const projectPath = Editor.Project.path;
const packageJsonPath = path.join(projectPath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return false;
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
return '@esengine/ai' in dependencies;
} catch (error) {
console.error('检查AI系统安装状态失败:', error);
return false;
}
}
/**
* 打开行为树文档
*/
static openDocumentation(): void {
const url = 'https://github.com/esengine/ai/blob/master/README.md';
try {
const { shell } = require('electron');
shell.openExternal(url);
console.log('Behavior Tree documentation opened successfully');
} catch (error) {
console.error('Failed to open Behavior Tree documentation:', error);
Editor.Dialog.info('打开行为树文档', {
detail: `请手动访问以下链接查看文档:\n\n${url}`,
});
}
}
/**
* 创建行为树文件
*/
static async createFile(assetInfo?: any): Promise<void> {
try {
const projectPath = Editor.Project.path;
const assetsPath = path.join(projectPath, 'assets');
// 生成唯一文件名
let fileName = 'NewBehaviorTree';
let counter = 1;
let filePath = path.join(assetsPath, `${fileName}.bt.json`);
while (fs.existsSync(filePath)) {
fileName = `NewBehaviorTree_${counter}`;
filePath = path.join(assetsPath, `${fileName}.bt.json`);
counter++;
}
// 创建默认的行为树配置
const defaultConfig = {
version: "1.0.0",
type: "behavior-tree",
metadata: {
createdAt: new Date().toISOString(),
nodeCount: 1
},
tree: {
id: "root",
type: "sequence",
namespace: "behaviourTree/composites",
properties: {},
children: []
}
};
// 写入文件
await fsExtra.writeFile(filePath, JSON.stringify(defaultConfig, null, 2));
// 刷新资源管理器 - 使用正确的资源路径
const relativeAssetPath = path.relative(projectPath, filePath).replace(/\\/g, '/');
const dbAssetPath = 'db://' + relativeAssetPath;
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
console.log(`Behavior tree file created: ${filePath}`);
Editor.Dialog.info('创建成功', {
detail: `行为树文件 "${fileName}.bt.json" 已创建完成!\n\n文件位置assets/${fileName}.bt.json\n\n您可以右键点击文件选择"用行为树编辑器打开"来编辑它。`,
});
} catch (error) {
console.error('Failed to create behavior tree file:', error);
Editor.Dialog.error('创建失败', {
detail: `创建行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
});
}
}
/**
* 打开行为树文件
*/
static async openFile(assetInfo: any): Promise<void> {
try {
if (!assetInfo || !assetInfo.file) {
throw new Error('无效的文件信息');
}
const filePath = assetInfo.file;
const fileData = await this.loadFileData(filePath);
await this.openPanel();
await this.sendDataToPanel(fileData);
} catch (error) {
Editor.Dialog.error('打开失败', {
detail: `打开行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`
});
}
}
/**
* 读取并解析文件数据
*/
private static async loadFileData(filePath: string): Promise<any> {
try {
let assetPath = filePath;
if (path.isAbsolute(filePath)) {
const projectPath = Editor.Project.path;
if (filePath.startsWith(projectPath)) {
assetPath = path.relative(projectPath, filePath);
assetPath = assetPath.replace(/\\/g, '/');
}
}
if (!assetPath.startsWith('db://')) {
assetPath = 'db://' + assetPath;
}
try {
const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', assetPath);
if (assetInfo && assetInfo.source) {
const content = await fsExtra.readFile(assetInfo.source, 'utf8');
let fileContent: any;
try {
fileContent = JSON.parse(content);
} catch (parseError) {
fileContent = {
version: "1.0.0",
type: "behavior-tree",
rawContent: content
};
}
const fileData = {
...fileContent,
_fileInfo: {
fileName: path.basename(assetInfo.source, path.extname(assetInfo.source)),
filePath: assetInfo.source,
assetPath: assetPath
}
};
return fileData;
}
} catch (assetError) {
// 资源系统读取失败,尝试直接文件读取
}
const actualFilePath = path.isAbsolute(filePath) ? filePath : path.join(Editor.Project.path, filePath);
if (!fs.existsSync(actualFilePath)) {
throw new Error(`文件不存在: ${actualFilePath}`);
}
const content = await fsExtra.readFile(actualFilePath, 'utf8');
let fileContent: any;
try {
fileContent = JSON.parse(content);
} catch (parseError) {
fileContent = {
version: "1.0.0",
type: "behavior-tree",
rawContent: content
};
}
const fileData = {
...fileContent,
_fileInfo: {
fileName: path.basename(actualFilePath, path.extname(actualFilePath)),
filePath: actualFilePath
}
};
return fileData;
} catch (error) {
throw new Error(`文件读取失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* 打开行为树面板
*/
private static async openPanel(): Promise<void> {
await Editor.Panel.open('cocos-ecs-extension.behavior-tree');
await new Promise(resolve => setTimeout(resolve, 300));
}
/**
* 发送数据到面板
*/
private static async sendDataToPanel(fileData: any): Promise<void> {
try {
const result = await Editor.Message.request('cocos-ecs-extension.behavior-tree', 'loadBehaviorTreeFile', fileData);
} catch (error) {
setTimeout(() => {
try {
Editor.Message.send('cocos-ecs-extension.behavior-tree', 'loadBehaviorTreeFile', fileData);
} catch (delayError) {
// 静默失败
}
}, 100);
}
}
/**
* 从编辑器创建行为树文件
*/
static async createFromEditor(data: { fileName: string, content: string }): Promise<void> {
try {
const projectPath = Editor.Project.path;
const assetsPath = path.join(projectPath, 'assets');
let fileName = data.fileName;
let counter = 1;
let filePath = path.join(assetsPath, `${fileName}.bt.json`);
while (fs.existsSync(filePath)) {
fileName = `${data.fileName}_${counter}`;
filePath = path.join(assetsPath, `${fileName}.bt.json`);
counter++;
}
await fsExtra.writeFile(filePath, data.content);
const relativeAssetPath = path.relative(projectPath, filePath).replace(/\\/g, '/');
const dbAssetPath = 'db://' + relativeAssetPath;
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
Editor.Dialog.info('保存成功', {
detail: `行为树文件 "${fileName}.bt.json" 已保存到 assets 目录中!`,
});
} catch (error) {
Editor.Dialog.error('保存失败', {
detail: `保存行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
});
}
}
/**
* 覆盖现有行为树文件
*/
static async overwriteFile(data: { filePath: string, content: string }): Promise<void> {
try {
await fsExtra.writeFile(data.filePath, data.content);
const projectPath = Editor.Project.path;
const relativeAssetPath = path.relative(projectPath, data.filePath).replace(/\\/g, '/');
const dbAssetPath = 'db://' + relativeAssetPath;
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
const fileName = path.basename(data.filePath, path.extname(data.filePath));
Editor.Dialog.info('覆盖成功', {
detail: `行为树文件 "${fileName}.bt.json" 已更新!`,
});
} catch (error) {
Editor.Dialog.error('覆盖失败', {
detail: `覆盖行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
});
}
}
}

View File

@@ -1,234 +0,0 @@
import { exec } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { TemplateGenerator } from '../TemplateGenerator';
/**
* ECS框架相关的处理器
*/
export class EcsFrameworkHandler {
/**
* 安装ECS Framework
*/
static async install(): Promise<void> {
const projectPath = Editor.Project.path;
const command = 'npm install @esengine/ecs-framework';
console.log(`Installing ECS Framework to project: ${projectPath}`);
return new Promise((resolve, reject) => {
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
console.log('Install stdout:', stdout);
if (stderr) console.log('Install stderr:', stderr);
if (error) {
console.error('Installation failed:', error);
reject(error);
} else {
console.log('Installation completed successfully');
// 验证安装是否成功
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
const installSuccess = fs.existsSync(nodeModulesPath);
if (installSuccess) {
console.log('ECS Framework installed successfully');
resolve();
} else {
console.warn('ECS Framework directory not found after install');
reject(new Error('安装验证失败'));
}
}
});
});
}
/**
* 更新ECS Framework
*/
static async update(targetVersion?: string): Promise<void> {
const projectPath = Editor.Project.path;
const version = targetVersion ? `@${targetVersion}` : '@latest';
const command = `npm install @esengine/ecs-framework${version}`;
console.log(`Updating ECS Framework to ${version} in project: ${projectPath}`);
return new Promise((resolve, reject) => {
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
console.log('Update stdout:', stdout);
if (stderr) console.log('Update stderr:', stderr);
if (error) {
console.error('Update failed:', error);
reject(error);
} else {
console.log('Update completed successfully');
// 验证更新是否成功
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
const updateSuccess = fs.existsSync(nodeModulesPath);
if (updateSuccess) {
console.log(`ECS Framework updated successfully to ${version}`);
resolve();
} else {
console.warn('ECS Framework directory not found after update');
reject(new Error('更新验证失败'));
}
}
});
});
}
/**
* 卸载ECS Framework
*/
static async uninstall(): Promise<void> {
const projectPath = Editor.Project.path;
const command = 'npm uninstall @esengine/ecs-framework';
console.log(`Uninstalling ECS Framework from project: ${projectPath}`);
return new Promise((resolve, reject) => {
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
console.log('Uninstall stdout:', stdout);
if (stderr) console.log('Uninstall stderr:', stderr);
if (error) {
console.error('Uninstall failed:', error);
reject(error);
} else {
console.log('Uninstall completed successfully');
// 检查是否真的卸载了
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
const stillExists = fs.existsSync(nodeModulesPath);
if (stillExists) {
console.warn('ECS Framework directory still exists after uninstall');
reject(new Error('卸载验证失败'));
} else {
console.log('ECS Framework uninstalled successfully');
resolve();
}
}
});
});
}
/**
* 打开文档
*/
static openDocumentation(): void {
const url = 'https://github.com/esengine/ecs-framework/blob/master/README.md';
try {
// 使用Electron的shell模块打开外部链接
const { shell } = require('electron');
shell.openExternal(url);
console.log('Documentation link opened successfully');
} catch (error) {
console.error('Failed to open documentation:', error);
Editor.Dialog.info('打开文档', {
detail: `请手动访问以下链接查看文档:\n\n${url}`,
});
}
}
/**
* 创建ECS模板
*/
static createTemplate(): void {
const projectPath = Editor.Project.path;
console.log(`Creating ECS template in project: ${projectPath}`);
try {
const templateGenerator = new TemplateGenerator(projectPath);
// 检查是否已存在模板
if (templateGenerator.checkTemplateExists()) {
const existingFiles = templateGenerator.getExistingFiles();
const fileList = existingFiles.length > 0 ? existingFiles.join('\n• ') : '未检测到具体文件';
Editor.Dialog.warn('模板已存在', {
detail: `检测到已存在ECS模板包含以下文件\n\n• ${fileList}\n\n是否要覆盖现有模板`,
buttons: ['覆盖', '取消'],
}).then((result: any) => {
if (result.response === 0) {
// 用户选择覆盖
console.log('User chose to overwrite existing template');
templateGenerator.removeExistingTemplate();
templateGenerator.createTemplate();
this.showTemplateCreatedDialog();
} else {
console.log('User cancelled template creation');
}
});
return;
}
// 创建新模板
templateGenerator.createTemplate();
console.log('ECS template created successfully');
this.showTemplateCreatedDialog();
} catch (error) {
console.error('Failed to create ECS template:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
Editor.Dialog.error('模板创建失败', {
detail: `创建ECS模板时发生错误\n\n${errorMessage}\n\n请检查项目权限和目录结构。`,
});
}
}
/**
* 显示模板创建成功的对话框
*/
private static showTemplateCreatedDialog(): void {
Editor.Dialog.info('模板创建成功', {
detail: '✅ ECS项目模板已创建完成\n\n已为您的Cocos Creator项目生成了完整的ECS架构模板包括\n\n' +
'• 位置、速度、Cocos节点组件\n' +
'• 移动系统和节点同步系统\n' +
'• 实体工厂和场景管理器\n' +
'• ECS管理器组件(可直接添加到节点)\n' +
'• 完整的使用文档\n\n' +
'请刷新资源管理器查看新创建的文件。',
});
}
/**
* 打开GitHub仓库
*/
static openGitHub(): void {
const url = 'https://github.com/esengine/ecs-framework';
try {
const { shell } = require('electron');
shell.openExternal(url);
console.log('GitHub repository opened successfully');
} catch (error) {
console.error('Failed to open GitHub repository:', error);
Editor.Dialog.info('打开GitHub', {
detail: `请手动访问以下链接:\n\n${url}`,
});
}
}
/**
* 打开QQ群
*/
static openQQGroup(): void {
const url = 'https://qm.qq.com/cgi-bin/qm/qr?k=your-qq-group-key';
try {
const { shell } = require('electron');
shell.openExternal(url);
console.log('QQ group opened successfully');
} catch (error) {
console.error('Failed to open QQ group:', error);
Editor.Dialog.info('QQ群', {
detail: '请手动搜索QQ群号或访问相关链接加入讨论群。',
});
}
}
}

View File

@@ -1,571 +0,0 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import * as crypto from 'crypto';
import { exec } from 'child_process';
/**
* 热更新配置接口
*/
interface HotUpdateConfig {
serverUrl: string;
currentVersion: string;
updateChannel: 'stable' | 'beta' | 'dev';
autoCheck: boolean;
checkInterval: number; // 分钟
}
/**
* 版本信息接口
*/
interface VersionInfo {
version: string;
releaseDate: string;
description: string;
downloadUrl: string;
fileSize: number;
checksum: string;
mandatory: boolean; // 是否强制更新
files: UpdateFile[];
}
/**
* 更新文件接口
*/
interface UpdateFile {
path: string;
hash: string;
size: number;
action: 'add' | 'update' | 'delete';
}
/**
* 热更新处理器
*/
export class HotUpdateHandler {
private static readonly CONFIG_FILE = 'hot-update-config.json';
private static readonly VERSION_FILE = 'version-info.json';
private static readonly EXTENSION_PATH = Editor.Package.getPath('cocos-ecs-extension') || '';
private static config: HotUpdateConfig;
private static updateTimer: NodeJS.Timeout | null = null;
/**
* 初始化热更新系统
*/
static async initialize(): Promise<void> {
console.log('[HotUpdate] 初始化热更新系统...');
try {
await this.loadConfig();
await this.startAutoCheck();
console.log('[HotUpdate] 热更新系统初始化完成');
} catch (error) {
console.error('[HotUpdate] 初始化失败:', error);
}
}
/**
* 加载配置
*/
private static async loadConfig(): Promise<void> {
const configPath = path.join(this.EXTENSION_PATH, this.CONFIG_FILE);
try {
if (await fs.pathExists(configPath)) {
this.config = await fs.readJSON(configPath);
} else {
// 创建默认配置
this.config = {
serverUrl: 'https://earthonline-game.cn/api/plugin-updates',
currentVersion: this.getCurrentVersion(),
updateChannel: 'stable',
autoCheck: true,
checkInterval: 60 // 60分钟检查一次
};
await this.saveConfig();
}
} catch (error) {
console.error('[HotUpdate] 配置加载失败:', error);
throw error;
}
}
/**
* 保存配置
*/
private static async saveConfig(): Promise<void> {
const configPath = path.join(this.EXTENSION_PATH, this.CONFIG_FILE);
await fs.writeJSON(configPath, this.config, { spaces: 2 });
}
/**
* 获取当前版本
*/
private static getCurrentVersion(): string {
try {
const packagePath = path.join(this.EXTENSION_PATH, 'package.json');
const packageInfo = fs.readJSONSync(packagePath);
return packageInfo.version;
} catch (error) {
console.error('[HotUpdate] 无法获取当前版本:', error);
return '1.0.0';
}
}
/**
* 开始自动检查
*/
private static async startAutoCheck(): Promise<void> {
if (!this.config.autoCheck) {
return;
}
// 立即检查一次
await this.checkForUpdates(true);
// 设置定时检查
if (this.updateTimer) {
clearInterval(this.updateTimer);
}
this.updateTimer = setInterval(async () => {
await this.checkForUpdates(true);
}, this.config.checkInterval * 60 * 1000);
}
/**
* 检查更新
*/
static async checkForUpdates(silent: boolean = false): Promise<VersionInfo | null> {
console.log('[HotUpdate] 检查更新中...');
try {
const response = await fetch(`${this.config.serverUrl}/check`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
currentVersion: this.config.currentVersion,
pluginId: 'cocos-ecs-extension', // 当前插件ID
channel: this.config.updateChannel,
platform: process.platform,
editorVersion: Editor.App.version
})
});
if (!response.ok) {
throw new Error(`服务器响应错误: ${response.status}`);
}
const versionInfo: VersionInfo = await response.json();
if (this.isNewerVersion(versionInfo.version, this.config.currentVersion)) {
console.log(`[HotUpdate] 发现新版本: ${versionInfo.version}`);
if (!silent) {
await this.showUpdateDialog(versionInfo);
}
return versionInfo;
} else {
if (!silent) {
Editor.Dialog.info('检查更新', {
detail: '当前已是最新版本!'
});
}
return null;
}
} catch (error) {
console.error('[HotUpdate] 检查更新失败:', error);
if (!silent) {
Editor.Dialog.error('检查更新失败', {
detail: `无法连接到更新服务器:\n\n${error instanceof Error ? error.message : String(error)}`
});
}
return null;
}
}
/**
* 比较版本号
*/
private static isNewerVersion(newVersion: string, currentVersion: string): boolean {
const parseVersion = (version: string) => {
return version.split('.').map(Number);
};
const newParts = parseVersion(newVersion);
const currentParts = parseVersion(currentVersion);
const maxLength = Math.max(newParts.length, currentParts.length);
for (let i = 0; i < maxLength; i++) {
const newPart = newParts[i] || 0;
const currentPart = currentParts[i] || 0;
if (newPart > currentPart) return true;
if (newPart < currentPart) return false;
}
return false;
}
/**
* 显示更新对话框
*/
private static async showUpdateDialog(versionInfo: VersionInfo): Promise<void> {
const message = `发现新版本 ${versionInfo.version}!\n\n` +
`发布时间: ${versionInfo.releaseDate}\n\n` +
`更新内容:\n${versionInfo.description}\n\n` +
`文件大小: ${this.formatFileSize(versionInfo.fileSize)}`;
const buttons = versionInfo.mandatory ? ['立即更新'] : ['立即更新', '稍后提醒', '跳过此版本'];
const result = await Editor.Dialog.info('插件更新', {
detail: message,
buttons: buttons
});
switch (result.response) {
case 0: // 立即更新
await this.downloadAndInstallUpdate(versionInfo);
break;
case 1: // 稍后提醒
if (!versionInfo.mandatory) {
console.log('[HotUpdate] 用户选择稍后更新');
}
break;
case 2: // 跳过此版本
if (!versionInfo.mandatory) {
await this.skipVersion(versionInfo.version);
}
break;
}
}
/**
* 下载并安装更新
*/
private static async downloadAndInstallUpdate(versionInfo: VersionInfo): Promise<void> {
console.log(`[HotUpdate] 开始下载更新: ${versionInfo.version}`);
try {
// 显示进度对话框
const progressDialog = this.showProgressDialog('正在下载更新...');
// 下载更新包
const updatePath = await this.downloadUpdate(versionInfo, (progress) => {
// 更新进度
console.log(`[HotUpdate] 下载进度: ${progress}%`);
});
progressDialog.detail = '正在验证文件...';
// 验证文件完整性
const isValid = await this.verifyUpdate(updatePath, versionInfo.checksum);
if (!isValid) {
throw new Error('文件校验失败,更新包可能已损坏');
}
progressDialog.detail = '正在安装更新...';
// 安装更新
await this.installUpdate(updatePath, versionInfo);
// 更新版本信息
this.config.currentVersion = versionInfo.version;
await this.saveConfig();
// 显示安装完成对话框
const result = await Editor.Dialog.info('更新完成', {
detail: `插件已成功更新到版本 ${versionInfo.version}!\n\n为了使更新生效需要重启Cocos Creator编辑器。`,
buttons: ['立即重启', '稍后重启']
});
if (result.response === 0) {
this.restartEditor();
}
} catch (error) {
console.error('[HotUpdate] 更新失败:', error);
Editor.Dialog.error('更新失败', {
detail: `更新过程中发生错误:\n\n${error instanceof Error ? error.message : String(error)}`
});
}
}
/**
* 下载更新
*/
private static async downloadUpdate(versionInfo: VersionInfo, onProgress?: (progress: number) => void): Promise<string> {
const response = await fetch(versionInfo.downloadUrl);
if (!response.ok) {
throw new Error(`下载失败: ${response.status}`);
}
const totalSize = parseInt(response.headers.get('content-length') || '0');
let downloadedSize = 0;
const tempPath = path.join(this.EXTENSION_PATH, 'temp', `update-${versionInfo.version}.zip`);
await fs.ensureDir(path.dirname(tempPath));
const writer = fs.createWriteStream(tempPath);
const reader = response.body?.getReader();
if (!reader) {
throw new Error('无法创建下载流');
}
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
writer.write(value);
downloadedSize += value.length;
if (onProgress && totalSize > 0) {
const progress = Math.round((downloadedSize / totalSize) * 100);
onProgress(progress);
}
}
writer.end();
return tempPath;
} catch (error) {
writer.destroy();
await fs.remove(tempPath).catch(() => {}); // 忽略删除错误
throw error;
}
}
/**
* 验证更新包
*/
private static async verifyUpdate(filePath: string, expectedChecksum: string): Promise<boolean> {
try {
const fileBuffer = await fs.readFile(filePath);
const hash = crypto.createHash('sha256');
hash.update(fileBuffer);
const actualChecksum = hash.digest('hex');
return actualChecksum === expectedChecksum;
} catch (error) {
console.error('[HotUpdate] 文件校验失败:', error);
return false;
}
}
/**
* 安装更新
*/
private static async installUpdate(updatePath: string, versionInfo: VersionInfo): Promise<void> {
const extractPath = path.join(this.EXTENSION_PATH, 'temp', 'extract');
try {
// 清理临时目录
await fs.remove(extractPath);
await fs.ensureDir(extractPath);
// 解压更新包
await this.extractZip(updatePath, extractPath);
// 备份当前版本
const backupPath = path.join(this.EXTENSION_PATH, 'backup', this.config.currentVersion);
await this.createBackup(backupPath);
// 应用更新文件
await this.applyUpdateFiles(extractPath, versionInfo.files);
// 清理临时文件
await fs.remove(path.dirname(updatePath));
} catch (error) {
console.error('[HotUpdate] 安装更新失败:', error);
// 尝试恢复备份
await this.restoreBackup();
throw error;
}
}
/**
* 解压ZIP文件
*/
private static async extractZip(zipPath: string, extractPath: string): Promise<void> {
return new Promise((resolve, reject) => {
// 这里使用node的解压库您可能需要安装 yauzl 或 adm-zip
const AdmZip = require('adm-zip');
try {
const zip = new AdmZip(zipPath);
zip.extractAllTo(extractPath, true);
resolve();
} catch (error) {
reject(error);
}
});
}
/**
* 应用更新文件
*/
private static async applyUpdateFiles(extractPath: string, files: UpdateFile[]): Promise<void> {
for (const file of files) {
const sourcePath = path.join(extractPath, file.path);
const targetPath = path.join(this.EXTENSION_PATH, file.path);
try {
switch (file.action) {
case 'add':
case 'update':
await fs.ensureDir(path.dirname(targetPath));
await fs.copy(sourcePath, targetPath, { overwrite: true });
break;
case 'delete':
await fs.remove(targetPath);
break;
}
} catch (error) {
console.error(`[HotUpdate] 处理文件失败 ${file.path}:`, error);
throw error;
}
}
}
/**
* 创建备份
*/
private static async createBackup(backupPath: string): Promise<void> {
await fs.ensureDir(backupPath);
const sourceFiles = [
'source',
'static',
'package.json',
'README.md'
];
for (const file of sourceFiles) {
const sourcePath = path.join(this.EXTENSION_PATH, file);
const targetPath = path.join(backupPath, file);
if (await fs.pathExists(sourcePath)) {
await fs.copy(sourcePath, targetPath);
}
}
}
/**
* 恢复备份
*/
private static async restoreBackup(): Promise<void> {
const backupPath = path.join(this.EXTENSION_PATH, 'backup', this.config.currentVersion);
if (await fs.pathExists(backupPath)) {
console.log('[HotUpdate] 正在恢复备份...');
const backupFiles = await fs.readdir(backupPath);
for (const file of backupFiles) {
const sourcePath = path.join(backupPath, file);
const targetPath = path.join(this.EXTENSION_PATH, file);
await fs.copy(sourcePath, targetPath, { overwrite: true });
}
}
}
/**
* 跳过版本
*/
private static async skipVersion(version: string): Promise<void> {
const skipPath = path.join(this.EXTENSION_PATH, 'skipped-versions.json');
let skippedVersions: string[] = [];
if (await fs.pathExists(skipPath)) {
skippedVersions = await fs.readJSON(skipPath);
}
if (!skippedVersions.includes(version)) {
skippedVersions.push(version);
await fs.writeJSON(skipPath, skippedVersions);
}
}
/**
* 显示进度对话框
*/
private static showProgressDialog(message: string) {
// 这是一个简化版本,实际可能需要创建自定义进度条
return {
detail: message
};
}
/**
* 格式化文件大小
*/
private static formatFileSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
/**
* 重启编辑器
*/
private static restartEditor(): void {
// 注意:这个功能需要特殊权限,可能需要用户手动重启
console.log('[HotUpdate] 请求重启编辑器');
try {
// 尝试重启编辑器(可能不会成功)
Editor.App.quit();
} catch (error) {
console.warn('[HotUpdate] 无法自动重启编辑器:', error);
}
}
/**
* 设置更新配置
*/
static async setConfig(newConfig: Partial<HotUpdateConfig>): Promise<void> {
this.config = { ...this.config, ...newConfig };
await this.saveConfig();
// 重新启动自动检查
if (this.config.autoCheck) {
await this.startAutoCheck();
} else if (this.updateTimer) {
clearInterval(this.updateTimer);
this.updateTimer = null;
}
}
/**
* 获取配置
*/
static getConfig(): HotUpdateConfig {
return { ...this.config };
}
/**
* 清理资源
*/
static cleanup(): void {
if (this.updateTimer) {
clearInterval(this.updateTimer);
this.updateTimer = null;
}
}
}

View File

@@ -1,64 +0,0 @@
/**
* 面板管理相关的处理器
*/
export class PanelHandler {
/**
* 打开默认面板
*/
static openDefaultPanel(): void {
try {
Editor.Panel.open('cocos-ecs-extension');
console.log('Default panel opened successfully');
} catch (error) {
console.error('Failed to open default panel:', error);
Editor.Dialog.error('打开面板失败', {
detail: `无法打开面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
});
}
}
/**
* 打开调试面板
*/
static openDebugPanel(): void {
try {
Editor.Panel.open('cocos-ecs-extension.debug');
console.log('Debug panel opened successfully');
} catch (error) {
console.error('Failed to open debug panel:', error);
Editor.Dialog.error('打开调试面板失败', {
detail: `无法打开调试面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
});
}
}
/**
* 打开代码生成器面板
*/
static openGeneratorPanel(): void {
try {
Editor.Panel.open('cocos-ecs-extension.generator');
console.log('Generator panel opened successfully');
} catch (error) {
console.error('Failed to open generator panel:', error);
Editor.Dialog.error('打开代码生成器失败', {
detail: `无法打开代码生成器面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
});
}
}
/**
* 打开行为树面板
*/
static openBehaviorTreePanel(): void {
try {
Editor.Panel.open('cocos-ecs-extension.behavior-tree');
console.log('Behavior Tree panel opened successfully');
} catch (error) {
console.error('Failed to open behavior tree panel:', error);
Editor.Dialog.error('打开行为树面板失败', {
detail: `无法打开行为树AI组件库面板\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
});
}
}
}

View File

@@ -1,4 +0,0 @@
export { EcsFrameworkHandler } from './EcsFrameworkHandler';
export { BehaviorTreeHandler } from './BehaviorTreeHandler';
export { PanelHandler } from './PanelHandler';
export { HotUpdateHandler } from './HotUpdateHandler';

View File

@@ -1,229 +0,0 @@
// @ts-ignore
import packageJSON from '../package.json';
import { EcsFrameworkHandler, BehaviorTreeHandler, PanelHandler, HotUpdateHandler } from './handlers';
import { readJSON } from 'fs-extra';
import * as path from 'path';
import { AssetInfo } from '@cocos/creator-types/editor/packages/asset-db/@types/public';
/**
* @en Registration method for the main process of Extension
* @zh 为扩展的主进程的注册方法
*/
export const methods: { [key: string]: (...any: any) => any } = {
// ================ 面板管理 ================
/**
* 打开默认面板
*/
openPanel() {
PanelHandler.openDefaultPanel();
},
/**
* 打开调试面板
*/
'open-debug'() {
PanelHandler.openDebugPanel();
},
/**
* 打开代码生成器面板
*/
'open-generator'() {
PanelHandler.openGeneratorPanel();
},
/**
* 打开行为树面板
*/
'open-behavior-tree'() {
PanelHandler.openBehaviorTreePanel();
},
// ================ ECS框架管理 ================
/**
* 安装ECS Framework
*/
'install-ecs-framework'() {
EcsFrameworkHandler.install();
},
/**
* 更新ECS Framework
*/
'update-ecs-framework'() {
EcsFrameworkHandler.update();
},
/**
* 卸载ECS Framework
*/
'uninstall-ecs-framework'() {
EcsFrameworkHandler.uninstall();
},
/**
* 打开文档
*/
'open-documentation'() {
EcsFrameworkHandler.openDocumentation();
},
/**
* 创建ECS模板
*/
'create-ecs-template'() {
EcsFrameworkHandler.createTemplate();
},
/**
* 打开GitHub仓库
*/
'open-github'() {
EcsFrameworkHandler.openGitHub();
},
/**
* 打开QQ群
*/
'open-qq-group'() {
EcsFrameworkHandler.openQQGroup();
},
// ================ 行为树管理 ================
/**
* 安装行为树AI系统
*/
async 'install-behavior-tree'() {
try {
return await BehaviorTreeHandler.install();
} catch (error) {
console.error('安装行为树AI系统失败:', error);
return false;
}
},
/**
* 更新行为树AI系统
*/
async 'update-behavior-tree'() {
try {
return await BehaviorTreeHandler.update();
} catch (error) {
console.error('更新行为树AI系统失败:', error);
return false;
}
},
/**
* 检查行为树AI是否已安装
*/
'check-behavior-tree-installed'() {
return BehaviorTreeHandler.checkInstalled();
},
/**
* 打开行为树文档
*/
'open-behavior-tree-docs'() {
BehaviorTreeHandler.openDocumentation();
},
/**
* 创建行为树文件
*/
'create-behavior-tree-file'() {
BehaviorTreeHandler.createFile();
},
/**
* 加载行为树文件到编辑器
*/
async 'load-behavior-tree-file'(...args: any[]) {
const assetInfo = args.length >= 2 ? args[1] : args[0];
try {
if (!assetInfo || (!assetInfo.file && !assetInfo.path)) {
throw new Error('无效的文件信息');
}
await Editor.Panel.open('cocos-ecs-extension.behavior-tree');
await new Promise(resolve => setTimeout(resolve, 500));
const result = await Editor.Message.request('cocos-ecs-extension', 'behavior-tree-panel-load-file', assetInfo);
} catch (error) {
Editor.Dialog.error('打开失败', {
detail: `打开行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`
});
}
},
/**
* 从编辑器创建行为树文件
*/
'create-behavior-tree-from-editor'(event: any, data: any) {
BehaviorTreeHandler.createFromEditor(data);
},
/**
* 覆盖现有行为树文件
*/
'overwrite-behavior-tree-file'(...args: any[]) {
const data = args.length >= 2 ? args[1] : args[0];
if (data && data.filePath) {
BehaviorTreeHandler.overwriteFile(data);
} else {
throw new Error('文件路径不存在或数据无效');
}
},
// ================ 热更新管理 ================
/**
* 检查插件更新
*/
'check-plugin-updates'() {
return HotUpdateHandler.checkForUpdates(false);
},
/**
* 设置热更新配置
*/
'set-hot-update-config'(...args: any[]) {
const config = args.length >= 2 ? args[1] : args[0];
return HotUpdateHandler.setConfig(config);
},
/**
* 获取热更新配置
*/
'get-hot-update-config'() {
return HotUpdateHandler.getConfig();
},
};
/**
* @en Method triggered when the extension is started
* @zh 启动扩展时触发的方法
*/
export function load() {
console.log('[Cocos ECS Extension] 扩展已加载');
// 初始化热更新系统
HotUpdateHandler.initialize().catch(error => {
console.error('[Cocos ECS Extension] 热更新初始化失败:', error);
});
}
/**
* @en Method triggered when the extension is uninstalled
* @zh 卸载扩展时触发的方法
*/
export function unload() {
console.log('[Cocos ECS Extension] 扩展已卸载');
// 清理热更新资源
HotUpdateHandler.cleanup();
}

View File

@@ -1,124 +0,0 @@
import { ref } from 'vue';
import { TreeNode, DragState, Connection } from '../types';
import { allNodeTemplates as nodeTemplates } from '../data/nodeTemplates';
/**
* 应用状态管理
*/
export function useAppState() {
// 安装状态
const checkingStatus = ref(true);
const isInstalled = ref(false);
const version = ref<string | null>(null);
const isInstalling = ref(false);
// 编辑器状态
const nodeTemplates_ = ref(nodeTemplates);
const treeNodes = ref<TreeNode[]>([]);
const selectedNodeId = ref<string | null>(null);
const selectedConditionNodeId = ref<string | null>(null); // 选中的条件节点ID
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);
const zoomLevel = ref(1);
const panX = ref(0);
const panY = ref(0);
const dragState = ref<DragState>({
isDraggingCanvas: false,
isDraggingNode: false,
isConnecting: false,
dragStartX: 0,
dragStartY: 0,
dragNodeId: null,
dragNodeStartX: 0,
dragNodeStartY: 0,
connectionStart: null,
connectionEnd: { x: 0, y: 0 }
});
// 连接状态
const connections = ref<Connection[]>([]);
const tempConnection = ref({ path: '' });
// UI状态
const showExportModal = ref(false);
const exportFormat = ref('json'); // 默认JSON格式TypeScript暂时禁用
// 工具函数
const getNodeByIdLocal = (id: string): TreeNode | undefined => {
return treeNodes.value.find(node => node.id === id);
};
const selectNode = (nodeId: string) => {
selectedNodeId.value = nodeId;
};
const newBehaviorTree = () => {
treeNodes.value = [];
selectedNodeId.value = null;
connections.value = [];
tempConnection.value.path = '';
};
const updateCanvasSize = () => {
const canvasArea = document.querySelector('.canvas-area') as HTMLElement;
if (canvasArea) {
const rect = canvasArea.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
canvasWidth.value = Math.max(rect.width, 800);
canvasHeight.value = Math.max(rect.height, 600);
}
}
};
return {
// 安装状态
checkingStatus,
isInstalled,
version,
isInstalling,
// 编辑器状态
nodeTemplates: nodeTemplates_,
treeNodes,
selectedNodeId,
selectedConditionNodeId,
nodeSearchText,
// 画布状态
canvasWidth,
canvasHeight,
zoomLevel,
panX,
panY,
dragState,
// 连接状态
connections,
tempConnection,
// UI状态
showExportModal,
exportFormat,
// 工具函数
getNodeByIdLocal,
selectNode,
newBehaviorTree,
updateCanvasSize
};
}

View File

@@ -1,474 +0,0 @@
import { ref, computed, reactive } from 'vue';
export interface BlackboardVariable {
name: string;
type: 'string' | 'number' | 'boolean' | 'vector2' | 'vector3' | 'object' | 'array';
value: any;
defaultValue: any;
description?: string;
group?: string;
readOnly?: boolean;
constraints?: {
min?: number;
max?: number;
step?: number;
allowedValues?: string[];
};
}
export interface BlackboardModalData {
name: string;
type: 'string' | 'number' | 'boolean' | 'vector2' | 'vector3' | 'object' | 'array';
defaultValue: any;
description: string;
group: string;
readOnly: boolean;
constraints: {
min?: number;
max?: number;
step?: number;
};
useAllowedValues: boolean;
allowedValuesText: string;
}
export function useBlackboard() {
const blackboardVariables = ref<Map<string, BlackboardVariable>>(new Map());
const expandedGroups = ref<Set<string>>(new Set(['未分组']));
const selectedVariable = ref<BlackboardVariable | null>(null);
const showBlackboardModal = ref(false);
const editingBlackboardVariable = ref<BlackboardVariable | null>(null);
const blackboardModalData = reactive<BlackboardModalData>({
name: '',
type: 'string',
defaultValue: '',
description: '',
group: '',
readOnly: false,
constraints: {},
useAllowedValues: false,
allowedValuesText: ''
});
const blackboardCollapsed = ref(false);
const blackboardTransparent = ref(true);
const blackboardVariablesArray = computed(() => {
return Array.from(blackboardVariables.value.values());
});
const blackboardVariableGroups = computed(() => {
const groups: Record<string, BlackboardVariable[]> = {};
blackboardVariables.value.forEach(variable => {
const groupName = variable.group || '未分组';
if (!groups[groupName]) {
groups[groupName] = [];
}
groups[groupName].push(variable);
});
const sortedGroups: Record<string, BlackboardVariable[]> = {};
const groupNames = Object.keys(groups).sort((a, b) => {
if (a === '未分组') return -1;
if (b === '未分组') return 1;
return a.localeCompare(b);
});
groupNames.forEach(groupName => {
groups[groupName].sort((a, b) => a.name.localeCompare(b.name));
sortedGroups[groupName] = groups[groupName];
});
return sortedGroups;
});
const groupedBlackboardVariables = () => {
return Object.entries(blackboardVariableGroups.value);
};
const isGroupExpanded = (groupName: string): boolean => {
return expandedGroups.value.has(groupName);
};
const toggleGroup = (groupName: string) => {
if (expandedGroups.value.has(groupName)) {
expandedGroups.value.delete(groupName);
} else {
expandedGroups.value.add(groupName);
}
};
const getVariableTypeIcon = (type: string): string => {
const iconMap: Record<string, string> = {
string: '📝',
number: '🔢',
boolean: '☑️',
vector2: '📐',
vector3: '🧊',
object: '📦',
array: '📋'
};
return iconMap[type] || '❓';
};
const formatBlackboardValue = (variable: BlackboardVariable): string => {
if (variable.value === null || variable.value === undefined) {
return 'null';
}
switch (variable.type) {
case 'boolean':
return variable.value ? 'true' : 'false';
case 'string':
return `"${variable.value}"`;
case 'number':
return variable.value.toString();
default:
return String(variable.value);
}
};
const hasVisibleConstraints = (variable: BlackboardVariable): boolean => {
if (!variable.constraints) return false;
return !!(
variable.constraints.min !== undefined ||
variable.constraints.max !== undefined ||
variable.constraints.allowedValues?.length
);
};
const formatConstraints = (constraints: BlackboardVariable['constraints']): string => {
const parts: string[] = [];
if (constraints?.min !== undefined) {
parts.push(`最小: ${constraints.min}`);
}
if (constraints?.max !== undefined) {
parts.push(`最大: ${constraints.max}`);
}
if (constraints?.allowedValues?.length) {
parts.push(`可选: ${constraints.allowedValues.join(', ')}`);
}
return parts.join(', ');
};
const getTypeDisplayName = (type: string): string => {
const typeMap: Record<string, string> = {
string: 'STR',
number: 'NUM',
boolean: 'BOOL',
vector2: 'VEC2',
vector3: 'VEC3',
object: 'OBJ',
array: 'ARR'
};
return typeMap[type] || type.toUpperCase();
};
const getDisplayValue = (variable: BlackboardVariable): string => {
if (variable.value === null || variable.value === undefined) {
return 'null';
}
switch (variable.type) {
case 'string':
return String(variable.value);
case 'number':
return variable.value.toString();
case 'boolean':
return variable.value ? 'true' : 'false';
case 'vector2':
if (typeof variable.value === 'object' && variable.value.x !== undefined && variable.value.y !== undefined) {
return `(${variable.value.x}, ${variable.value.y})`;
}
return String(variable.value);
case 'vector3':
if (typeof variable.value === 'object' && variable.value.x !== undefined && variable.value.y !== undefined && variable.value.z !== undefined) {
return `(${variable.value.x}, ${variable.value.y}, ${variable.value.z})`;
}
return String(variable.value);
case 'object':
case 'array':
try {
const jsonStr = JSON.stringify(variable.value);
return jsonStr.length > 20 ? jsonStr.substring(0, 17) + '...' : jsonStr;
} catch {
return String(variable.value);
}
default:
return String(variable.value);
}
};
const saveBlackboardVariable = () => {
if (!blackboardModalData.name.trim()) {
alert('请输入变量名称');
return;
}
let finalValue = blackboardModalData.defaultValue;
if (blackboardModalData.type === 'object' || blackboardModalData.type === 'array') {
try {
if (typeof blackboardModalData.defaultValue === 'string') {
finalValue = blackboardModalData.defaultValue ? JSON.parse(blackboardModalData.defaultValue) : (blackboardModalData.type === 'array' ? [] : {});
}
} catch (error) {
alert('JSON格式错误请检查输入');
return;
}
}
const constraints: BlackboardVariable['constraints'] = {};
if (blackboardModalData.constraints.min !== undefined) constraints.min = blackboardModalData.constraints.min;
if (blackboardModalData.constraints.max !== undefined) constraints.max = blackboardModalData.constraints.max;
if (blackboardModalData.constraints.step !== undefined) constraints.step = blackboardModalData.constraints.step;
if (blackboardModalData.useAllowedValues && blackboardModalData.allowedValuesText.trim()) {
constraints.allowedValues = blackboardModalData.allowedValuesText
.split('\n')
.map(val => val.trim())
.filter(val => val.length > 0);
}
const variable: BlackboardVariable = {
name: blackboardModalData.name,
type: blackboardModalData.type,
value: finalValue,
defaultValue: finalValue,
description: blackboardModalData.description,
group: blackboardModalData.group || undefined,
readOnly: blackboardModalData.readOnly,
constraints: Object.keys(constraints).length > 0 ? constraints : undefined
};
blackboardVariables.value.set(variable.name, variable);
const groupName = variable.group || '未分组';
expandedGroups.value.add(groupName);
showBlackboardModal.value = false;
editingBlackboardVariable.value = null;
Object.assign(blackboardModalData, {
name: '',
type: 'string',
defaultValue: '',
description: '',
group: '',
readOnly: false,
constraints: {},
useAllowedValues: false,
allowedValuesText: ''
});
};
const deleteBlackboardVariable = (variableName: string) => {
if (confirm(`确定要删除变量 "${variableName}" 吗?`)) {
blackboardVariables.value.delete(variableName);
}
};
const updateBlackboardVariable = (variableName: string, newValue: any) => {
const variable = blackboardVariables.value.get(variableName);
if (!variable) return;
if (variable.readOnly) {
alert('该变量为只读,无法修改');
return;
}
const updatedVariable = { ...variable, value: newValue };
blackboardVariables.value.set(variableName, updatedVariable);
};
const selectVariable = (variable: BlackboardVariable) => {
selectedVariable.value = variable;
};
const clearBlackboard = () => {
if (confirm('确定要清空所有变量吗?此操作不可恢复。')) {
blackboardVariables.value.clear();
selectedVariable.value = null;
}
};
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);
try {
navigator.clipboard.writeText(json);
alert('Blackboard配置已复制到剪贴板');
} catch (error) {
console.error('复制失败:', error);
}
};
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';
input.accept = '.json';
input.onchange = (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
if (!Array.isArray(data)) {
throw new Error('格式错误:期望数组格式');
}
let importCount = 0;
data.forEach(varData => {
if (varData.name && varData.type) {
blackboardVariables.value.set(varData.name, varData);
importCount++;
}
});
alert(`成功导入 ${importCount} 个变量`);
} catch (error) {
alert('导入失败:' + (error as Error).message);
}
};
reader.readAsText(file);
};
input.click();
};
const onVariableDragStart = (event: DragEvent, variable: BlackboardVariable) => {
if (!event.dataTransfer) {
return;
}
const dragData = {
name: variable.name,
type: variable.type,
value: variable.value
};
event.dataTransfer.setData('application/blackboard-variable', JSON.stringify(dragData));
event.dataTransfer.effectAllowed = 'copy';
// 添加视觉反馈
const dragElement = event.currentTarget as HTMLElement;
if (dragElement) {
dragElement.style.opacity = '0.8';
setTimeout(() => {
dragElement.style.opacity = '1';
}, 100);
}
};
const editVariable = (variable: BlackboardVariable) => {
editingBlackboardVariable.value = variable;
Object.assign(blackboardModalData, {
name: variable.name,
type: variable.type,
defaultValue: (variable.type === 'object' || variable.type === 'array') ? JSON.stringify(variable.value, null, 2) : variable.value,
description: variable.description || '',
group: variable.group || '',
readOnly: variable.readOnly || false,
constraints: {
min: variable.constraints?.min,
max: variable.constraints?.max,
step: variable.constraints?.step
},
useAllowedValues: !!(variable.constraints?.allowedValues?.length),
allowedValuesText: variable.constraints?.allowedValues?.join('\n') || ''
});
showBlackboardModal.value = true;
};
const addBlackboardVariable = () => {
editingBlackboardVariable.value = null;
Object.assign(blackboardModalData, {
name: '',
type: 'string',
defaultValue: '',
description: '',
group: '',
readOnly: false,
constraints: {},
useAllowedValues: false,
allowedValuesText: ''
});
showBlackboardModal.value = true;
};
return {
blackboardVariables: blackboardVariablesArray,
selectedVariable,
showBlackboardModal,
editingBlackboardVariable,
blackboardModalData,
expandedGroups,
blackboardVariableGroups,
blackboardCollapsed,
blackboardTransparent,
groupedBlackboardVariables,
isGroupExpanded,
toggleGroup,
getVariableTypeIcon,
formatBlackboardValue,
hasVisibleConstraints,
formatConstraints,
getTypeDisplayName,
getDisplayValue,
addBlackboardVariable,
saveBlackboardVariable,
deleteBlackboardVariable,
removeBlackboardVariable: deleteBlackboardVariable,
updateBlackboardVariable,
editVariable,
selectVariable,
clearBlackboard,
resetBlackboardToDefaults,
loadBlackboardFromArray,
exportBlackboard,
importBlackboard,
onBlackboardDragStart: onVariableDragStart,
editBlackboardVariable: editVariable,
onBlackboardValueChange: (variable: BlackboardVariable) => {
updateBlackboardVariable(variable.name, variable.value);
}
};
}

View File

@@ -1,203 +0,0 @@
import { Ref, ref } from 'vue';
import { TreeNode, DragState } from '../types';
/**
* 画布管理功能
*/
export function useCanvasManager(
panX: Ref<number>,
panY: Ref<number>,
zoomLevel: Ref<number>,
treeNodes: Ref<TreeNode[]>,
selectedNodeId: Ref<string | null>,
canvasAreaRef: Ref<HTMLElement | null>,
updateConnections: () => void
) {
// 画布尺寸 - 使用默认值或从DOM获取
const canvasWidth = ref(800);
const canvasHeight = ref(600);
// 拖拽状态
const dragState = ref<DragState>({
isDraggingCanvas: false,
isDraggingNode: false,
dragNodeId: null,
dragStartX: 0,
dragStartY: 0,
dragNodeStartX: 0,
dragNodeStartY: 0,
isConnecting: false,
connectionStart: null,
connectionEnd: { x: 0, y: 0 }
});
// 如果有canvas引用更新尺寸
if (canvasAreaRef.value) {
const rect = canvasAreaRef.value.getBoundingClientRect();
canvasWidth.value = rect.width;
canvasHeight.value = rect.height;
}
// 画布操作功能
const onCanvasWheel = (event: WheelEvent) => {
event.preventDefault();
const zoomSpeed = 0.1;
const delta = event.deltaY > 0 ? -zoomSpeed : zoomSpeed;
const newZoom = Math.max(0.1, Math.min(3, zoomLevel.value + delta));
zoomLevel.value = newZoom;
};
const onCanvasMouseDown = (event: MouseEvent) => {
// 只在空白区域开始画布拖拽
if (event.target === event.currentTarget) {
dragState.value.isDraggingCanvas = true;
dragState.value.dragStartX = event.clientX;
dragState.value.dragStartY = event.clientY;
document.addEventListener('mousemove', onCanvasMouseMove);
document.addEventListener('mouseup', onCanvasMouseUp);
}
};
const onCanvasMouseMove = (event: MouseEvent) => {
if (dragState.value.isDraggingCanvas) {
const deltaX = event.clientX - dragState.value.dragStartX;
const deltaY = event.clientY - dragState.value.dragStartY;
panX.value += deltaX;
panY.value += deltaY;
dragState.value.dragStartX = event.clientX;
dragState.value.dragStartY = event.clientY;
}
};
const onCanvasMouseUp = (event: MouseEvent) => {
if (dragState.value.isDraggingCanvas) {
dragState.value.isDraggingCanvas = false;
document.removeEventListener('mousemove', onCanvasMouseMove);
document.removeEventListener('mouseup', onCanvasMouseUp);
}
};
// 缩放控制
const zoomIn = () => {
zoomLevel.value = Math.min(3, zoomLevel.value + 0.1);
};
const zoomOut = () => {
zoomLevel.value = Math.max(0.1, zoomLevel.value - 0.1);
};
const resetZoom = () => {
zoomLevel.value = 1;
};
const centerView = () => {
if (treeNodes.value.length === 0) {
panX.value = 0;
panY.value = 0;
return;
}
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
treeNodes.value.forEach(node => {
// 尝试从DOM获取实际节点尺寸否则使用默认值
const nodeElement = document.querySelector(`[data-node-id="${node.id}"]`);
let nodeWidth = 150;
let nodeHeight = 80; // 使用基础高度
if (nodeElement) {
const rect = nodeElement.getBoundingClientRect();
nodeWidth = rect.width / zoomLevel.value;
nodeHeight = rect.height / zoomLevel.value;
}
minX = Math.min(minX, node.x);
minY = Math.min(minY, node.y);
maxX = Math.max(maxX, node.x + nodeWidth);
maxY = Math.max(maxY, node.y + nodeHeight);
});
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
panX.value = canvasWidth.value / 2 - centerX * zoomLevel.value;
panY.value = canvasHeight.value / 2 - centerY * zoomLevel.value;
};
// 网格样式计算
const gridStyle = () => {
const gridSize = 20 * zoomLevel.value;
return {
backgroundSize: `${gridSize}px ${gridSize}px`,
backgroundPosition: `${panX.value % gridSize}px ${panY.value % gridSize}px`
};
};
// 节点拖拽功能
const startNodeDrag = (event: MouseEvent, node: any) => {
event.preventDefault();
event.stopPropagation();
dragState.value.isDraggingNode = true;
dragState.value.dragNodeId = node.id;
dragState.value.dragStartX = event.clientX;
dragState.value.dragStartY = event.clientY;
dragState.value.dragNodeStartX = node.x;
dragState.value.dragNodeStartY = node.y;
const nodeElement = event.currentTarget as HTMLElement;
nodeElement.classList.add('dragging');
document.addEventListener('mousemove', onNodeDrag);
document.addEventListener('mouseup', onNodeDragEnd);
};
const onNodeDrag = (event: MouseEvent) => {
if (!dragState.value.isDraggingNode || !dragState.value.dragNodeId) return;
const deltaX = (event.clientX - dragState.value.dragStartX) / zoomLevel.value;
const deltaY = (event.clientY - dragState.value.dragStartY) / zoomLevel.value;
const node = treeNodes.value.find(n => n.id === dragState.value.dragNodeId);
if (node) {
node.x = dragState.value.dragNodeStartX + deltaX;
node.y = dragState.value.dragNodeStartY + deltaY;
updateConnections();
}
};
const onNodeDragEnd = (event: MouseEvent) => {
if (dragState.value.isDraggingNode) {
const draggingNodes = document.querySelectorAll('.tree-node.dragging');
draggingNodes.forEach(node => node.classList.remove('dragging'));
dragState.value.isDraggingNode = false;
dragState.value.dragNodeId = null;
updateConnections();
document.removeEventListener('mousemove', onNodeDrag);
document.removeEventListener('mouseup', onNodeDragEnd);
}
};
return {
onCanvasWheel,
onCanvasMouseDown,
onCanvasMouseMove,
onCanvasMouseUp,
zoomIn,
zoomOut,
resetZoom,
centerView,
gridStyle,
startNodeDrag
};
}

View File

@@ -1,490 +0,0 @@
import { Ref } from 'vue';
import { TreeNode } from '../types';
import { NodeTemplate } from '../data/nodeTemplates';
/**
* 代码生成管理
*/
export function useCodeGeneration(
treeNodes: Ref<TreeNode[]>,
nodeTemplates: Ref<NodeTemplate[]>,
getNodeByIdLocal: (id: string) => TreeNode | undefined,
rootNode: () => TreeNode | null,
blackboardVariables?: Ref<Map<string, any>>
) {
// 生成行为树配置JSON
const generateBehaviorTreeConfig = () => {
const root = rootNode();
if (!root) {
return null;
}
const config: any = {
version: "1.0.0",
type: "behavior-tree",
metadata: {
createdAt: new Date().toISOString(),
hasECSNodes: hasECSNodes(),
nodeCount: treeNodes.value.length
},
tree: generateNodeConfig(root)
};
// 包含黑板数据
if (blackboardVariables && blackboardVariables.value.size > 0) {
config.blackboard = Array.from(blackboardVariables.value.values());
}
return config;
};
// 生成可读的配置JSON字符串
const generateConfigJSON = (): string => {
const config = generateBehaviorTreeConfig();
if (!config) {
return '// 请先添加根节点';
}
return JSON.stringify(config, null, 2);
};
// 生成TypeScript构建代码用于运行时从配置创建行为树
const generateTypeScriptCode = (): string => {
const config = generateBehaviorTreeConfig();
if (!config) {
return '// 请先添加根节点';
}
const { behaviorTreeImports, ecsImports } = getRequiredImports();
let importsCode = '';
if (behaviorTreeImports.length > 0) {
importsCode += `import { ${behaviorTreeImports.join(', ')}, BehaviorTreeBuilder } from '@esengine/ai';\n`;
}
if (ecsImports.length > 0) {
importsCode += `import { ${ecsImports.join(', ')} } from '@esengine/ecs-framework';\n`;
}
const contextType = hasECSNodes() ? 'Entity' : 'any';
const configString = JSON.stringify(config, null, 4);
return `${importsCode}
// 行为树配置
const behaviorTreeConfig = ${configString};
// 从配置创建行为树
export function createBehaviorTree<T extends ${contextType}>(context?: T): BehaviorTree<T> {
return BehaviorTreeBuilder.fromConfig<T>(behaviorTreeConfig, context);
}
// 直接导出配置(用于序列化保存)
export const config = behaviorTreeConfig;`;
};
const getRequiredImports = (): { behaviorTreeImports: string[], ecsImports: string[] } => {
const behaviorTreeImports = new Set<string>();
const ecsImports = new Set<string>();
// 总是需要这些基础类
behaviorTreeImports.add('BehaviorTree');
behaviorTreeImports.add('TaskStatus');
treeNodes.value.forEach(node => {
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
if (template?.className) {
if (template.namespace?.includes('ecs-integration')) {
behaviorTreeImports.add(template.className);
ecsImports.add('Entity');
ecsImports.add('Component');
} else {
behaviorTreeImports.add(template.className);
}
}
});
return {
behaviorTreeImports: Array.from(behaviorTreeImports),
ecsImports: Array.from(ecsImports)
};
};
const hasECSNodes = (): boolean => {
return treeNodes.value.some(node => {
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
return template?.namespace?.includes('ecs-integration');
});
};
// 生成节点配置对象
const generateNodeConfig = (node: TreeNode): any => {
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
if (!template || !template.className) {
return {
type: node.type,
error: "未知节点类型"
};
}
const nodeConfig: any = {
id: node.id,
type: template.className,
namespace: template.namespace || 'behaviourTree',
properties: {}
};
// 处理节点属性
if (node.properties) {
Object.entries(node.properties).forEach(([key, prop]) => {
if (prop.value !== undefined && prop.value !== '') {
nodeConfig.properties[key] = {
type: prop.type,
value: prop.value
};
}
});
}
// 处理子节点
if (node.children && node.children.length > 0) {
nodeConfig.children = node.children
.map(childId => getNodeByIdLocal(childId))
.filter(Boolean)
.map(child => generateNodeConfig(child!));
}
return nodeConfig;
};
const generateNodeCode = (node: TreeNode, indent: number = 0): string => {
const spaces = ' '.repeat(indent);
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
if (!template || !template.className) {
return `${spaces}// 未知节点类型: ${node.type}`;
}
let code = `${spaces}new ${template.className}(`;
const params: string[] = [];
// 处理特定节点的构造函数参数
if (template.namespace?.includes('ecs-integration')) {
// ECS节点的特殊处理
switch (template.className) {
case 'HasComponentCondition':
case 'AddComponentAction':
case 'RemoveComponentAction':
case 'ModifyComponentAction':
if (node.properties?.componentType?.value) {
params.push(node.properties.componentType.value);
}
if (template.className === 'AddComponentAction' && node.properties?.componentFactory?.value) {
params.push(node.properties.componentFactory.value);
}
if (template.className === 'ModifyComponentAction' && node.properties?.modifierCode?.value) {
params.push(node.properties.modifierCode.value);
}
break;
case 'HasTagCondition':
if (node.properties?.tag?.value !== undefined) {
params.push(node.properties.tag.value.toString());
}
break;
case 'IsActiveCondition':
if (node.properties?.checkHierarchy?.value !== undefined) {
params.push(node.properties.checkHierarchy.value.toString());
}
break;
case 'WaitTimeAction':
if (node.properties?.waitTime?.value !== undefined) {
params.push(node.properties.waitTime.value.toString());
}
break;
}
} else {
// 普通行为树节点的处理
switch (template.className) {
case 'ExecuteAction':
case 'ExecuteActionConditional':
if (node.properties?.actionCode?.value || node.properties?.conditionCode?.value) {
const code = node.properties.actionCode?.value || node.properties.conditionCode?.value;
params.push(code);
if (node.properties?.actionName?.value) {
params.push(`{ name: "${node.properties.actionName.value}" }`);
}
}
break;
case 'WaitAction':
if (node.properties?.waitTime?.value !== undefined) {
params.push(node.properties.waitTime.value.toString());
}
break;
case 'LogAction':
if (node.properties?.message?.value) {
params.push(`"${node.properties.message.value}"`);
}
break;
case 'Repeater':
if (node.properties?.repeatCount?.value !== undefined) {
params.push(node.properties.repeatCount.value.toString());
}
break;
case 'Sequence':
case 'Selector':
if (node.properties?.abortType?.value && node.properties.abortType.value !== 'None') {
params.push(`AbortTypes.${node.properties.abortType.value}`);
}
break;
}
}
code += params.join(', ');
code += ')';
// 处理子节点(对于复合节点和装饰器)
if (template.canHaveChildren && node.children && node.children.length > 0) {
const children = node.children
.map(childId => getNodeByIdLocal(childId))
.filter(Boolean)
.map(child => generateNodeCode(child!, indent + 1));
if (children.length > 0) {
const className = template.className; // 保存到局部变量
if (template.category === 'decorator') {
// 装饰器只有一个子节点
code = code.slice(0, -1); // 移除最后的 ')'
const varName = className.toLowerCase();
code += `;\n${spaces}${varName}.child = ${children[0].trim()};\n${spaces}return ${varName}`;
} else if (template.category === 'composite') {
// 复合节点需要添加子节点
code = code.slice(0, -1); // 移除最后的 ')'
code += `;\n`;
children.forEach(child => {
code += `${spaces}${className.toLowerCase()}.addChild(${child.trim()});\n`;
});
code += `${spaces}return ${className.toLowerCase()}`;
}
}
}
return code;
};
// 从配置创建行为树节点
const createTreeFromConfig = (config: any): TreeNode[] => {
console.log('createTreeFromConfig被调用接收到的配置:', config);
console.log('nodeTemplates当前数量:', nodeTemplates.value.length);
// 处理两种不同的文件格式
if (config.nodes && Array.isArray(config.nodes)) {
console.log('使用nodes格式处理节点数量:', config.nodes.length);
const result = createTreeFromNodesFormat(config);
console.log('nodes格式处理结果:', result);
return result;
} else if (config.tree) {
console.log('使用tree格式处理');
const result = createTreeFromTreeFormat(config);
console.log('tree格式处理结果:', result);
return result;
} else {
console.log('配置格式不匹配,返回空数组');
return [];
}
};
// 处理新格式nodes数组格式
const createTreeFromNodesFormat = (config: any): TreeNode[] => {
console.log('createTreeFromNodesFormat开始处理');
if (!config.nodes || !Array.isArray(config.nodes)) {
console.log('nodes数据无效');
return [];
}
const nodes: TreeNode[] = [];
config.nodes.forEach((nodeConfig: any, index: number) => {
console.log(`处理第${index + 1}个节点:`, nodeConfig);
const template = findTemplateByType(nodeConfig.type);
console.log(`为节点类型 "${nodeConfig.type}" 找到的模板:`, template);
if (!template) {
console.warn(`未找到节点类型 "${nodeConfig.type}" 的模板`);
return;
}
const node: TreeNode = {
id: nodeConfig.id || generateNodeId(),
type: template.type,
name: nodeConfig.name || template.name,
icon: nodeConfig.icon || template.icon,
description: nodeConfig.description || template.description,
canHaveChildren: template.canHaveChildren,
canHaveParent: template.canHaveParent,
x: nodeConfig.x || 400,
y: nodeConfig.y || 100,
properties: {},
children: nodeConfig.children || [],
parent: nodeConfig.parent,
hasError: false
};
// 恢复属性
if (nodeConfig.properties && template.properties) {
Object.entries(nodeConfig.properties).forEach(([key, propConfig]: [string, any]) => {
if (template.properties![key]) {
node.properties![key] = {
...template.properties![key],
value: propConfig.value !== undefined ? propConfig.value : template.properties![key].value
};
}
});
}
// 确保所有模板属性都有默认值
if (template.properties) {
Object.entries(template.properties).forEach(([key, propDef]) => {
if (!node.properties![key]) {
node.properties![key] = { ...propDef };
}
});
}
console.log(`创建的节点:`, node);
nodes.push(node);
});
console.log(`createTreeFromNodesFormat完成总共创建了${nodes.length}个节点`);
return nodes;
};
// 处理旧格式tree对象格式
const createTreeFromTreeFormat = (config: any): TreeNode[] => {
if (!config.tree) {
return [];
}
const nodes: TreeNode[] = [];
const processNode = (nodeConfig: any, parent?: TreeNode): TreeNode => {
const template = findTemplateByType(nodeConfig.type);
if (!template) {
throw new Error(`未知节点类型: ${nodeConfig.type}`);
}
const node: TreeNode = {
id: nodeConfig.id || generateNodeId(),
type: template.type,
name: template.name,
icon: template.icon,
description: template.description,
canHaveChildren: template.canHaveChildren,
canHaveParent: template.canHaveParent,
x: 400,
y: 100,
properties: {},
children: [],
parent: parent?.id,
hasError: false
};
// 恢复属性
if (nodeConfig.properties && template.properties) {
Object.entries(nodeConfig.properties).forEach(([key, propConfig]: [string, any]) => {
if (template.properties![key]) {
node.properties![key] = {
...template.properties![key],
value: propConfig.value !== undefined ? propConfig.value : template.properties![key].value
};
}
});
}
// 确保所有模板属性都有默认值
if (template.properties) {
Object.entries(template.properties).forEach(([key, propDef]) => {
if (!node.properties![key]) {
node.properties![key] = { ...propDef };
}
});
}
nodes.push(node);
// 处理子节点
if (nodeConfig.children && Array.isArray(nodeConfig.children)) {
nodeConfig.children.forEach((childConfig: any) => {
const childNode = processNode(childConfig, node);
node.children!.push(childNode.id);
});
}
return node;
};
processNode(config.tree);
return nodes;
};
// 通过类型名查找模板(支持多种匹配方式)
const findTemplateByType = (typeName: string): NodeTemplate | undefined => {
// 直接匹配 type 字段
let template = nodeTemplates.value.find(t => t.type === typeName);
if (template) return template;
// 匹配 className 字段
template = nodeTemplates.value.find(t => t.className === typeName);
if (template) return template;
// 大小写不敏感匹配 type
template = nodeTemplates.value.find(t => t.type.toLowerCase() === typeName.toLowerCase());
if (template) return template;
// 大小写不敏感匹配 className
template = nodeTemplates.value.find(t => t.className && t.className.toLowerCase() === typeName.toLowerCase());
if (template) return template;
// 特殊映射处理
const typeMapping: Record<string, string> = {
'Sequence': 'sequence',
'Selector': 'selector',
'Parallel': 'parallel',
'Inverter': 'inverter',
'Repeater': 'repeater',
'AlwaysSucceed': 'always-succeed',
'AlwaysFail': 'always-fail',
'UntilSuccess': 'until-success',
'UntilFail': 'until-fail',
'ExecuteAction': 'execute-action',
'LogAction': 'log-action',
'WaitAction': 'wait-action'
};
const mappedType = typeMapping[typeName];
if (mappedType) {
template = nodeTemplates.value.find(t => t.type === mappedType);
if (template) return template;
}
return undefined;
};
// 生成唯一节点ID
const generateNodeId = (): string => {
return 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
};
return {
generateBehaviorTreeConfig,
generateConfigJSON,
generateTypeScriptCode,
generateNodeCode,
generateNodeConfig,
createTreeFromConfig,
getRequiredImports
};
}

View File

@@ -1,372 +0,0 @@
import { Ref, computed } from 'vue';
import { TreeNode } from '../types';
import { NodeTemplate } from '../data/nodeTemplates';
import { getRootNode } from '../utils/nodeUtils';
import { getInstallStatusText, getInstallStatusClass } from '../utils/installUtils';
import { getGridStyle } from '../utils/canvasUtils';
/**
* 计算属性管理
*/
export function useComputedProperties(
nodeTemplates: Ref<NodeTemplate[]>,
nodeSearchText: Ref<string>,
treeNodes: Ref<TreeNode[]>,
selectedNodeId: Ref<string | null>,
selectedConditionNodeId: Ref<string | null>,
checkingStatus: Ref<boolean>,
isInstalling: Ref<boolean>,
isInstalled: Ref<boolean>,
version: Ref<string | null>,
exportFormat: Ref<string>,
panX: Ref<number>,
panY: Ref<number>,
zoomLevel: Ref<number>,
getNodeByIdLocal: (id: string) => TreeNode | undefined,
codeGeneration?: {
generateConfigJSON: () => string;
generateTypeScriptCode: () => string;
}
) {
// 过滤节点
const filteredRootNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'root' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
const filteredCompositeNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'composite' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
const filteredDecoratorNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'decorator' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
const filteredActionNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'action' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
const filteredConditionNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'condition' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
const filteredECSNodes = () => {
return nodeTemplates.value.filter(node =>
node.category === 'ecs' &&
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
);
};
// 选中的节点 - 使用computed确保响应式更新
const selectedNode = computed(() => {
if (!selectedNodeId.value) return null;
// 直接从treeNodes数组中查找确保获取最新的节点状态
const node = treeNodes.value.find(n => n.id === selectedNodeId.value);
return node || null;
});
// 当前选中的条件节点(用于编辑条件属性)
const selectedConditionNode = computed(() => {
if (!selectedConditionNodeId.value) return null;
const decoratorNode = treeNodes.value.find(n => n.id === selectedConditionNodeId.value);
if (!decoratorNode || !decoratorNode.attachedCondition) return null;
// 根据条件类型重新构建属性结构
const conditionProperties = reconstructConditionProperties(
decoratorNode.attachedCondition.type,
decoratorNode.properties || {}
);
// 创建一个虚拟的条件节点对象,用于属性编辑
return {
id: decoratorNode.id + '_condition',
name: decoratorNode.attachedCondition.name + '(条件)',
type: decoratorNode.attachedCondition.type,
icon: decoratorNode.attachedCondition.icon,
properties: conditionProperties,
isConditionNode: true,
parentDecorator: decoratorNode
};
});
/**
* 根据条件类型重新构建属性结构
* 将装饰器的扁平属性转换回条件模板的属性结构
*/
const reconstructConditionProperties = (conditionType: string, decoratorProperties: Record<string, any>) => {
switch (conditionType) {
case 'condition-random':
return {
successProbability: {
type: 'number',
name: '成功概率',
value: decoratorProperties.successProbability || 0.5,
description: '条件成功的概率 (0.0 - 1.0)'
}
};
case 'condition-component':
return {
componentType: {
type: 'string',
name: '组件类型',
value: decoratorProperties.componentType || '',
description: '要检查的组件类型名称'
}
};
case 'condition-tag':
return {
tagValue: {
type: 'number',
name: '标签值',
value: decoratorProperties.tagValue || 0,
description: '要检查的标签值'
}
};
case 'condition-active':
return {
checkHierarchy: {
type: 'boolean',
name: '检查层级激活',
value: decoratorProperties.checkHierarchy || false,
description: '是否检查整个层级的激活状态'
}
};
case 'condition-numeric':
return {
propertyPath: {
type: 'string',
name: '属性路径',
value: decoratorProperties.propertyPath || 'context.someValue',
description: '要比较的数值属性路径'
},
compareOperator: {
type: 'select',
name: '比较操作符',
value: decoratorProperties.compareOperator || 'greater',
options: ['greater', 'less', 'equal', 'greaterEqual', 'lessEqual', 'notEqual'],
description: '数值比较的操作符'
},
compareValue: {
type: 'number',
name: '比较值',
value: decoratorProperties.compareValue || 0,
description: '用于比较的目标值'
}
};
case 'condition-property':
return {
propertyPath: {
type: 'string',
name: '属性路径',
value: decoratorProperties.propertyPath || 'context.someProperty',
description: '要检查的属性路径'
}
};
case 'condition-custom':
return {
conditionCode: {
type: 'code',
name: '条件代码',
value: decoratorProperties.conditionCode || '(context) => true',
description: '自定义条件判断函数'
}
};
// Blackboard相关条件使用实际的模板类型名
case 'blackboard-variable-exists':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要检查的黑板变量名'
},
invert: {
type: 'boolean',
name: '反转结果',
value: decoratorProperties.invert || false,
description: '是否反转检查结果'
}
};
case 'blackboard-value-comparison':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要比较的黑板变量名'
},
operator: {
type: 'select',
name: '比较操作符',
value: decoratorProperties.operator || 'equal',
options: ['equal', 'notEqual', 'greater', 'greaterOrEqual', 'less', 'lessOrEqual', 'contains', 'notContains'],
description: '比较操作类型'
},
compareValue: {
type: 'string',
name: '比较值',
value: decoratorProperties.compareValue || '',
description: '用于比较的值(留空则使用比较变量)'
},
compareVariable: {
type: 'string',
name: '比较变量名',
value: decoratorProperties.compareVariable || '',
description: '用于比较的另一个黑板变量名'
}
};
case 'blackboard-variable-type-check':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要检查的黑板变量名'
},
expectedType: {
type: 'select',
name: '期望类型',
value: decoratorProperties.expectedType || 'string',
options: ['string', 'number', 'boolean', 'vector2', 'vector3', 'object', 'array'],
description: '期望的变量类型'
}
};
case 'blackboard-variable-range-check':
return {
variableName: {
type: 'string',
name: '变量名',
value: decoratorProperties.variableName || '',
description: '要检查的数值型黑板变量名'
},
minValue: {
type: 'number',
name: '最小值',
value: decoratorProperties.minValue || 0,
description: '范围的最小值(包含)'
},
maxValue: {
type: 'number',
name: '最大值',
value: decoratorProperties.maxValue || 100,
description: '范围的最大值(包含)'
}
};
default:
// 对于未知的条件类型,尝试从装饰器属性中推断
const reconstructed: Record<string, any> = {};
Object.keys(decoratorProperties).forEach(key => {
if (key !== 'conditionType') {
reconstructed[key] = {
type: typeof decoratorProperties[key] === 'number' ? 'number' :
typeof decoratorProperties[key] === 'boolean' ? 'boolean' : 'string',
name: key,
value: decoratorProperties[key],
description: `${key}参数`
};
}
});
return reconstructed;
}
};
// 当前显示在属性面板的节点(普通节点或条件节点)
const activeNode = computed(() => selectedConditionNode.value || selectedNode.value);
// 根节点
const rootNode = () => {
return getRootNode(treeNodes.value);
};
// 安装状态
const installStatusClass = () => {
return getInstallStatusClass(isInstalling.value, isInstalled.value);
};
const installStatusText = () => {
return getInstallStatusText(
checkingStatus.value,
isInstalling.value,
isInstalled.value,
version.value
);
};
// 验证结果
const validationResult = () => {
if (treeNodes.value.length === 0) {
return { isValid: false, message: '行为树为空' };
}
const root = rootNode();
if (!root) {
return { isValid: false, message: '缺少根节点' };
}
return { isValid: true, message: '行为树结构有效' };
};
// 导出代码
const exportedCode = () => {
if (!codeGeneration) {
return '// 代码生成器未初始化';
}
try {
if (exportFormat.value === 'json') {
return codeGeneration.generateConfigJSON();
} else {
return codeGeneration.generateTypeScriptCode();
}
} catch (error) {
return `// 代码生成失败: ${error}`;
}
};
// 网格样式
const gridStyle = () => {
return getGridStyle(panX.value, panY.value, zoomLevel.value);
};
return {
filteredRootNodes,
filteredCompositeNodes,
filteredDecoratorNodes,
filteredActionNodes,
filteredConditionNodes,
filteredECSNodes,
selectedNode,
selectedConditionNode,
activeNode,
rootNode,
installStatusClass,
installStatusText,
validationResult,
exportedCode,
gridStyle
};
}

View File

@@ -1,482 +0,0 @@
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<TreeNode[]>,
getNodeByIdLocal: (id: string) => TreeNode | undefined
) {
const dragState = reactive<DragState>({
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<string, any> => {
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'
};
// Blackboard相关条件支持
case 'blackboard-variable-exists':
return {
...baseConfig,
variableName: conditionTemplate.properties?.variableName?.value || '',
invert: conditionTemplate.properties?.invert?.value || false
};
case 'blackboard-value-comparison':
return {
...baseConfig,
variableName: conditionTemplate.properties?.variableName?.value || '',
operator: conditionTemplate.properties?.operator?.value || 'equal',
compareValue: conditionTemplate.properties?.compareValue?.value || '',
compareVariable: conditionTemplate.properties?.compareVariable?.value || ''
};
case 'blackboard-variable-type-check':
return {
...baseConfig,
variableName: conditionTemplate.properties?.variableName?.value || '',
expectedType: conditionTemplate.properties?.expectedType?.value || 'string'
};
case 'blackboard-variable-range-check':
return {
...baseConfig,
variableName: conditionTemplate.properties?.variableName?.value || '',
minValue: conditionTemplate.properties?.minValue?.value || 0,
maxValue: conditionTemplate.properties?.maxValue?.value || 100
};
default:
return baseConfig;
}
};
/**
* 获取条件类型字符串
*/
const getConditionTypeFromTemplate = (template: NodeTemplate): string => {
const typeMap: Record<string, string> = {
'condition-random': 'random',
'condition-component': 'hasComponent',
'condition-tag': 'hasTag',
'condition-active': 'isActive',
'condition-numeric': 'numericCompare',
'condition-property': 'propertyExists',
'condition-custom': 'custom',
// Blackboard相关条件
'blackboard-variable-exists': 'blackboardExists',
'blackboard-value-comparison': 'blackboardCompare',
'blackboard-variable-type-check': 'blackboardTypeCheck',
'blackboard-variable-range-check': 'blackboardRangeCheck'
};
return typeMap[template.type] || 'custom';
};
/**
* 执行条件吸附到装饰器
*/
const attachConditionToDecorator = (
event: DragEvent,
decoratorNode: TreeNode
): boolean => {
event.preventDefault();
event.stopPropagation();
if (!dragState.isDraggingCondition || !dragState.conditionTemplate) {
return false;
}
if (!isConditionalDecorator(decoratorNode)) {
return false;
}
// 获取条件配置
const conditionConfig = mapConditionToDecoratorProperties(dragState.conditionTemplate);
// 更新装饰器属性
if (!decoratorNode.properties) {
decoratorNode.properties = {};
}
Object.assign(decoratorNode.properties, conditionConfig);
// 标记装饰器已附加条件
decoratorNode.attachedCondition = {
type: dragState.conditionTemplate.type,
name: dragState.conditionTemplate.name,
icon: dragState.conditionTemplate.icon
};
// 初始化为收缩状态
if (decoratorNode.conditionExpanded === undefined) {
decoratorNode.conditionExpanded = false;
}
// 重置拖拽状态
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 = () => {
dragState.isDraggingCondition = false;
dragState.conditionTemplate = null;
dragState.mousePosition = null;
dragState.hoveredDecoratorId = null;
};
/**
* 获取条件显示文本(简化版始终显示条件名称)
*/
const getConditionDisplayText = (decoratorNode: TreeNode, expanded: boolean = false): string => {
if (!decoratorNode.attachedCondition) {
return '';
}
// 始终返回条件名称,不管是否展开
return decoratorNode.attachedCondition.name;
};
/**
* 获取条件的可见属性(用于展开时显示)
*/
const getConditionProperties = (decoratorNode: TreeNode): Record<string, any> => {
if (!decoratorNode.attachedCondition || !decoratorNode.properties) {
return {};
}
const conditionType = decoratorNode.attachedCondition.type;
const visibleProps: Record<string, any> = {};
// 根据条件类型筛选相关属性
switch (conditionType) {
case 'condition-random':
if ('successProbability' in decoratorNode.properties) {
visibleProps['成功概率'] = `${(decoratorNode.properties.successProbability * 100).toFixed(1)}%`;
}
break;
case 'condition-component':
if ('componentType' in decoratorNode.properties) {
visibleProps['组件类型'] = decoratorNode.properties.componentType;
}
break;
case 'condition-tag':
if ('tagValue' in decoratorNode.properties) {
visibleProps['标签值'] = decoratorNode.properties.tagValue;
}
break;
case 'condition-active':
if ('checkHierarchy' in decoratorNode.properties) {
visibleProps['检查层级'] = decoratorNode.properties.checkHierarchy ? '是' : '否';
}
break;
case 'condition-numeric':
if ('propertyPath' in decoratorNode.properties) {
visibleProps['属性路径'] = decoratorNode.properties.propertyPath;
}
if ('compareOperator' in decoratorNode.properties) {
visibleProps['比较操作'] = decoratorNode.properties.compareOperator;
}
if ('compareValue' in decoratorNode.properties) {
visibleProps['比较值'] = decoratorNode.properties.compareValue;
}
break;
case 'condition-property':
if ('propertyPath' in decoratorNode.properties) {
visibleProps['属性路径'] = decoratorNode.properties.propertyPath;
}
break;
case 'blackboard-variable-exists':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('invert' in decoratorNode.properties) {
visibleProps['反转结果'] = decoratorNode.properties.invert ? '是' : '否';
}
break;
case 'blackboard-value-comparison':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('operator' in decoratorNode.properties) {
visibleProps['操作符'] = decoratorNode.properties.operator;
}
if ('compareValue' in decoratorNode.properties) {
visibleProps['比较值'] = decoratorNode.properties.compareValue;
}
if ('compareVariable' in decoratorNode.properties) {
visibleProps['比较变量'] = decoratorNode.properties.compareVariable;
}
break;
case 'blackboard-variable-type-check':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('expectedType' in decoratorNode.properties) {
visibleProps['期望类型'] = decoratorNode.properties.expectedType;
}
break;
case 'blackboard-variable-range-check':
if ('variableName' in decoratorNode.properties) {
visibleProps['变量名'] = decoratorNode.properties.variableName;
}
if ('minValue' in decoratorNode.properties) {
visibleProps['最小值'] = decoratorNode.properties.minValue;
}
if ('maxValue' in decoratorNode.properties) {
visibleProps['最大值'] = decoratorNode.properties.maxValue;
}
break;
}
return visibleProps;
};
/**
* 切换条件展开状态
*/
const toggleConditionExpanded = (decoratorNode: TreeNode) => {
decoratorNode.conditionExpanded = !decoratorNode.conditionExpanded;
};
/**
* 移除装饰器的条件
*/
const removeConditionFromDecorator = (decoratorNode: TreeNode) => {
if (decoratorNode.attachedCondition) {
// 删除附加的条件信息
delete decoratorNode.attachedCondition;
// 重置展开状态
decoratorNode.conditionExpanded = false;
// 保留装饰器的基础属性,只删除条件相关的属性
const preservedProperties: Record<string, any> = {};
// 条件装饰器的基础属性
const baseDecoratorProperties = [
'executeWhenTrue',
'executeWhenFalse',
'checkInterval',
'abortType'
];
// 保留基础属性
if (decoratorNode.properties) {
baseDecoratorProperties.forEach(key => {
if (key in decoratorNode.properties!) {
preservedProperties[key] = decoratorNode.properties![key];
}
});
}
// 重置为只包含基础属性的对象
decoratorNode.properties = preservedProperties;
}
};
/**
* 检查装饰器是否可以接受条件吸附
*/
const canAcceptCondition = (decoratorNode: TreeNode): boolean => {
return isConditionalDecorator(decoratorNode);
};
return {
dragState,
startConditionDrag,
handleDecoratorDragOver,
handleDecoratorDragLeave,
attachConditionToDecorator,
handleCanvasDrop,
resetDragState,
getConditionDisplayText,
removeConditionFromDecorator,
canAcceptCondition,
isConditionalDecorator,
toggleConditionExpanded,
getConditionProperties
};
}

View File

@@ -1,610 +0,0 @@
import { Ref } from 'vue';
import { TreeNode, Connection, ConnectionState } from '../types';
/**
* 连接线管理功能
*/
export function useConnectionManager(
treeNodes: Ref<TreeNode[]>,
connections: Ref<Connection[]>,
connectionState: ConnectionState,
canvasAreaRef: Ref<HTMLElement | null>,
svgRef: Ref<SVGElement | null>,
panX: Ref<number>,
panY: Ref<number>,
zoomLevel: Ref<number>
) {
const getPortPosition = (nodeId: string, portType: 'input' | 'output') => {
const node = treeNodes.value.find(n => n.id === nodeId);
if (!node) return null;
const canvasArea = canvasAreaRef.value;
if (!canvasArea) {
return getCalculatedPortPosition(node, portType);
}
const selectors = [
`[data-node-id="${nodeId}"]`,
`.tree-node[data-node-id="${nodeId}"]`,
`div[data-node-id="${nodeId}"]`
];
let nodeElement: HTMLElement | null = null;
for (const selector of selectors) {
try {
const doc = canvasArea.ownerDocument || document;
const foundElement = doc.querySelector(selector);
if (foundElement && canvasArea.contains(foundElement)) {
nodeElement = foundElement as HTMLElement;
break;
}
} catch (error) {
continue;
}
}
if (!nodeElement) {
try {
const allTreeNodes = canvasArea.querySelectorAll('.tree-node');
for (let i = 0; i < allTreeNodes.length; i++) {
const el = allTreeNodes[i] as HTMLElement;
const dataNodeId = el.getAttribute('data-node-id');
if (dataNodeId === nodeId) {
nodeElement = el;
break;
}
}
} catch (error) {
// Fallback to calculated position
}
}
if (!nodeElement) {
return getCalculatedPortPosition(node, portType);
}
const portSelectors = [
`.port.port-${portType}`,
`.port-${portType}`,
`.port.${portType}`,
`.${portType}-port`
];
let portElement: HTMLElement | null = null;
for (const portSelector of portSelectors) {
try {
portElement = nodeElement.querySelector(portSelector) as HTMLElement;
if (portElement) {
break;
}
} catch (error) {
continue;
}
}
if (!portElement) {
return getNodeEdgePortPosition(nodeElement, node, portType);
}
const portRect = portElement.getBoundingClientRect();
const canvasRect = canvasAreaRef.value?.getBoundingClientRect();
if (!canvasRect) {
return getCalculatedPortPosition(node, portType);
}
const relativeX = portRect.left + portRect.width / 2 - canvasRect.left;
const relativeY = portRect.top + portRect.height / 2 - canvasRect.top;
const svgX = (relativeX - panX.value) / zoomLevel.value;
const svgY = (relativeY - panY.value) / zoomLevel.value;
return { x: svgX, y: svgY };
};
const getCalculatedPortPosition = (node: any, portType: 'input' | 'output') => {
let nodeWidth = 150;
let nodeHeight = 80;
if (node.properties) {
const propertyCount = Object.keys(node.properties).length;
if (propertyCount > 0) {
nodeHeight += propertyCount * 20 + 20;
nodeWidth = Math.max(150, nodeWidth + 50);
}
}
const portX = node.x + nodeWidth / 2;
const portY = portType === 'input'
? node.y - 8
: node.y + nodeHeight + 8;
return { x: portX, y: portY };
};
const getNodeEdgePortPosition = (nodeElement: HTMLElement, node: any, portType: 'input' | 'output') => {
const nodeRect = nodeElement.getBoundingClientRect();
const canvasRect = canvasAreaRef.value?.getBoundingClientRect();
if (!canvasRect) {
return getCalculatedPortPosition(node, portType);
}
// 计算节点在SVG坐标系中的实际大小和位置
const nodeWidth = nodeRect.width / zoomLevel.value;
const nodeHeight = nodeRect.height / zoomLevel.value;
// 端口位于节点的水平中心
const portX = node.x + nodeWidth / 2;
const portY = portType === 'input'
? node.y - 5
: node.y + nodeHeight + 5;
return { x: portX, y: portY };
};
const startConnection = (event: MouseEvent, nodeId: string, portType: 'input' | 'output') => {
event.preventDefault();
event.stopPropagation();
connectionState.isConnecting = true;
connectionState.startNodeId = nodeId;
connectionState.startPortType = portType;
connectionState.currentMousePos = { x: event.clientX, y: event.clientY };
const startPos = getPortPosition(nodeId, portType);
if (startPos) {
connectionState.startPortPos = startPos;
}
document.addEventListener('mousemove', onConnectionDrag);
document.addEventListener('mouseup', onConnectionEnd);
if (canvasAreaRef.value) {
canvasAreaRef.value.classList.add('connecting');
}
};
// 连接拖拽
const onConnectionDrag = (event: MouseEvent) => {
if (!connectionState.isConnecting || !connectionState.startNodeId || !connectionState.startPortType) return;
connectionState.currentMousePos = { x: event.clientX, y: event.clientY };
const svgPos = clientToSVGCoordinates(event.clientX, event.clientY);
const startPos = getPortPosition(connectionState.startNodeId, connectionState.startPortType);
if (startPos && svgPos) {
const controlOffset = Math.abs(svgPos.y - startPos.y) * 0.5;
let path: string;
if (connectionState.startPortType === 'output') {
path = `M ${startPos.x} ${startPos.y} C ${startPos.x} ${startPos.y + controlOffset} ${svgPos.x} ${svgPos.y - controlOffset} ${svgPos.x} ${svgPos.y}`;
} else {
path = `M ${startPos.x} ${startPos.y} C ${startPos.x} ${startPos.y - controlOffset} ${svgPos.x} ${svgPos.y + controlOffset} ${svgPos.x} ${svgPos.y}`;
}
if ('tempPath' in connectionState) {
(connectionState as any).tempPath = path;
}
}
const targetPort = findTargetPort(event.clientX, event.clientY);
if (targetPort && targetPort.nodeId !== connectionState.startNodeId) {
connectionState.hoveredPort = targetPort;
} else {
connectionState.hoveredPort = null;
}
};
// 结束连接
const onConnectionEnd = (event: MouseEvent) => {
if (!connectionState.isConnecting) return;
// 检查是否落在有效的端口上
const targetPort = findTargetPort(event.clientX, event.clientY);
if (targetPort && connectionState.startNodeId && connectionState.startPortType) {
const canConnectResult = canConnect(
connectionState.startNodeId,
connectionState.startPortType,
targetPort.nodeId,
targetPort.portType
);
if (canConnectResult) {
let parentId: string, childId: string;
if (connectionState.startPortType === 'output') {
parentId = connectionState.startNodeId;
childId = targetPort.nodeId;
} else {
parentId = targetPort.nodeId;
childId = connectionState.startNodeId;
}
createConnection(parentId, childId);
}
}
// 清理连接状态
cancelConnection();
};
// 取消连接
const cancelConnection = () => {
connectionState.isConnecting = false;
connectionState.startNodeId = null;
connectionState.startPortType = null;
connectionState.currentMousePos = null;
connectionState.startPortPos = null;
connectionState.hoveredPort = null;
if ('tempPath' in connectionState) {
(connectionState as any).tempPath = '';
}
document.removeEventListener('mousemove', onConnectionDrag);
document.removeEventListener('mouseup', onConnectionEnd);
if (canvasAreaRef.value) {
canvasAreaRef.value.classList.remove('connecting');
}
// 清除画布内的拖拽目标样式
if (canvasAreaRef.value) {
const allPorts = canvasAreaRef.value.querySelectorAll('.port.drag-target');
allPorts.forEach(port => port.classList.remove('drag-target'));
}
};
const clientToSVGCoordinates = (clientX: number, clientY: number) => {
if (!canvasAreaRef.value) return null;
try {
// 获取canvas容器的边界
const canvasRect = canvasAreaRef.value.getBoundingClientRect();
// 转换为相对于canvas的坐标
const canvasX = clientX - canvasRect.left;
const canvasY = clientY - canvasRect.top;
// 撤销SVG的transform转换为SVG坐标
// SVG transform: translate(panX, panY) scale(zoomLevel)
const svgX = (canvasX - panX.value) / zoomLevel.value;
const svgY = (canvasY - panY.value) / zoomLevel.value;
return { x: svgX, y: svgY };
} catch (e) {
return null;
}
};
// 查找目标端口
const findTargetPort = (clientX: number, clientY: number) => {
if (!canvasAreaRef.value) return null;
try {
const elementAtPoint = document.elementFromPoint(clientX, clientY);
if (elementAtPoint?.classList.contains('port') && canvasAreaRef.value.contains(elementAtPoint)) {
return getPortInfo(elementAtPoint as HTMLElement);
}
} catch (error) {
// 查询出错时静默处理
}
const allPorts = canvasAreaRef.value.querySelectorAll('.port');
for (const port of allPorts) {
const rect = port.getBoundingClientRect();
const margin = 10;
if (clientX >= rect.left - margin && clientX <= rect.right + margin &&
clientY >= rect.top - margin && clientY <= rect.bottom + margin) {
return getPortInfo(port as HTMLElement);
}
}
return null;
};
// 从端口元素获取端口信息
const getPortInfo = (portElement: HTMLElement) => {
const nodeElement = portElement.closest('.tree-node');
if (!nodeElement) return null;
const nodeId = nodeElement.getAttribute('data-node-id');
const portType = portElement.classList.contains('port-input') ? 'input' : 'output' as 'input' | 'output';
return nodeId ? { nodeId, portType } : null;
};
// 端口悬停处理
const onPortHover = (nodeId: string, portType: 'input' | 'output') => {
if (connectionState.isConnecting && connectionState.startNodeId !== nodeId) {
connectionState.hoveredPort = { nodeId, portType };
if (canConnect(connectionState.startNodeId!, connectionState.startPortType!, nodeId, portType)) {
// 在画布区域内查找端口元素
if (canvasAreaRef.value) {
const portElement = canvasAreaRef.value.querySelector(`[data-node-id="${nodeId}"] .port.port-${portType}`);
if (portElement) {
portElement.classList.add('drag-target');
}
}
}
}
};
const onPortLeave = () => {
if (connectionState.isConnecting) {
connectionState.hoveredPort = null;
// 清除画布内的拖拽目标样式
if (canvasAreaRef.value) {
const allPorts = canvasAreaRef.value.querySelectorAll('.port.drag-target');
allPorts.forEach(port => port.classList.remove('drag-target'));
}
}
};
// 验证连接目标是否有效
const isValidConnectionTarget = (nodeId: string, portType: 'input' | 'output') => {
if (!connectionState.isConnecting || !connectionState.startNodeId || connectionState.startNodeId === nodeId) {
return false;
}
return canConnect(connectionState.startNodeId, connectionState.startPortType!, nodeId, portType);
};
// 检查是否可以连接
const canConnect = (sourceNodeId: string, sourcePortType: string, targetNodeId: string, targetPortType: string) => {
if (sourceNodeId === targetNodeId) return false;
if (sourcePortType === targetPortType) return false;
let parentNodeId: string, childNodeId: string;
if (sourcePortType === 'output') {
parentNodeId = sourceNodeId;
childNodeId = targetNodeId;
} else {
parentNodeId = targetNodeId;
childNodeId = sourceNodeId;
}
const childNode = treeNodes.value.find(n => n.id === childNodeId);
if (childNode && childNode.parent && childNode.parent !== parentNodeId) {
return false;
}
const parentNode = treeNodes.value.find(n => n.id === parentNodeId);
if (!parentNode || !parentNode.canHaveChildren) return false;
if (!childNode || !childNode.canHaveParent) return false;
// 检查子节点数量限制
if (parentNode.maxChildren !== undefined) {
const currentChildrenCount = parentNode.children ? parentNode.children.length : 0;
if (currentChildrenCount >= parentNode.maxChildren) {
return false; // 已达到最大子节点数量
}
}
// 检查根节点限制:根节点不能有父节点
if (childNode.type === 'root') {
return false; // 根节点不能作为其他节点的子节点
}
// 检查是否只能有一个根节点
if (parentNode.type === 'root') {
// 根节点只能连接一个子节点
const rootNodes = treeNodes.value.filter(n => n.type === 'root');
if (rootNodes.length > 1) {
return false; // 不能有多个根节点
}
}
if (wouldCreateCycle(parentNodeId, childNodeId)) return false;
if (isDescendant(childNodeId, parentNodeId)) return false;
return true;
};
// 检查是否会创建循环
const wouldCreateCycle = (parentId: string, childId: string) => {
return isDescendant(parentId, childId);
};
const isDescendant = (ancestorId: string, descendantId: string): boolean => {
const visited = new Set<string>();
function checkPath(currentId: string): boolean {
if (currentId === ancestorId) return true;
if (visited.has(currentId)) return false;
visited.add(currentId);
const currentNode = treeNodes.value.find(n => n.id === currentId);
if (currentNode?.children) {
for (const childId of currentNode.children) {
if (checkPath(childId)) return true;
}
}
return false;
}
return checkPath(descendantId);
};
// 创建连接
const createConnection = (parentId: string, childId: string) => {
const parentNode = treeNodes.value.find(n => n.id === parentId);
const childNode = treeNodes.value.find(n => n.id === childId);
if (!parentNode || !childNode) return;
// 移除子节点的旧父子关系
if (childNode.parent) {
const oldParent = treeNodes.value.find(n => n.id === childNode.parent);
if (oldParent) {
const index = oldParent.children.indexOf(childId);
if (index > -1) {
oldParent.children.splice(index, 1);
}
}
}
// 建立新的父子关系
childNode.parent = parentId;
if (!parentNode.children.includes(childId)) {
parentNode.children.push(childId);
}
updateConnections();
};
// 改进的连接线更新方法
const updateConnections = () => {
// 立即清空现有连接
connections.value.length = 0;
// 创建新的连接数据
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 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}`;
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,
path: path,
active: false
});
}
}
});
}
});
// 批量更新连接
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完全渲染
}
};
// 删除连接线
const removeConnection = (connectionId: string) => {
const connection = connections.value.find(conn => conn.id === connectionId);
if (!connection) return;
const parentNode = treeNodes.value.find(n => n.id === connection.sourceId);
const childNode = treeNodes.value.find(n => n.id === connection.targetId);
if (parentNode && childNode) {
// 从父节点的children中移除
const index = parentNode.children.indexOf(connection.targetId);
if (index > -1) {
parentNode.children.splice(index, 1);
}
// 清除子节点的parent
childNode.parent = undefined;
// 更新连接线
updateConnections();
}
};
// 连接线点击事件处理
const onConnectionClick = (event: MouseEvent, connectionId: string) => {
event.preventDefault();
event.stopPropagation();
// 询问用户是否要删除连接
if (confirm('确定要删除这条连接线吗?')) {
removeConnection(connectionId);
}
};
return {
getPortPosition,
startConnection,
cancelConnection,
updateConnections,
removeConnection,
onConnectionClick,
onPortHover,
onPortLeave,
isValidConnectionTarget
};
}

View File

@@ -1,538 +0,0 @@
import { Ref, ref, watch } from 'vue';
import { TreeNode, Connection } from '../types';
interface FileOperationOptions {
treeNodes: Ref<TreeNode[]>;
selectedNodeId: Ref<string | null>;
connections: Ref<Connection[]>;
tempConnection: Ref<{ path: string }>;
showExportModal: Ref<boolean>;
codeGeneration?: {
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;
version: string;
};
}
export function useFileOperations(options: FileOperationOptions) {
const {
treeNodes,
selectedNodeId,
connections,
tempConnection,
showExportModal,
codeGeneration,
updateConnections,
blackboardOperations
} = options;
const hasUnsavedChanges = ref(false);
const lastSavedState = ref<string>('');
const currentFileName = ref('');
const currentFilePath = ref('');
const updateUnsavedStatus = () => {
const currentState = JSON.stringify({
nodes: treeNodes.value,
connections: connections.value
});
hasUnsavedChanges.value = currentState !== lastSavedState.value;
};
watch([treeNodes, connections], updateUnsavedStatus, { deep: true });
const markAsSaved = () => {
const currentState = JSON.stringify({
nodes: treeNodes.value,
connections: connections.value
});
lastSavedState.value = currentState;
hasUnsavedChanges.value = false;
};
const setCurrentFile = (fileName: string, filePath: string = '') => {
currentFileName.value = fileName;
currentFilePath.value = filePath;
markAsSaved();
};
const clearCurrentFile = () => {
currentFileName.value = '';
currentFilePath.value = '';
};
const exportBehaviorTreeData = (): FileData => {
const data: FileData = {
nodes: treeNodes.value,
connections: connections.value,
metadata: {
name: currentFileName.value || 'untitled',
created: new Date().toISOString(),
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') => {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
background: ${type === 'success' ? '#4caf50' : '#f44336'};
color: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 10001;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateX(0)';
}, 10);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 300);
}, 3000);
};
const sendToMain = (message: string, data: any): Promise<void> => {
return new Promise((resolve, reject) => {
try {
Editor.Message.request('cocos-ecs-extension', message, data)
.then((result) => {
resolve();
})
.catch((error) => {
reject(error);
});
} catch (error) {
reject(error);
}
});
};
const checkUnsavedChanges = (): Promise<boolean> => {
return new Promise((resolve) => {
if (!hasUnsavedChanges.value) {
resolve(true);
return;
}
const result = confirm(
'当前行为树有未保存的更改,是否要保存?\n\n' +
'点击"确定"保存更改\n' +
'点击"取消"丢弃更改'
);
if (result) {
saveBehaviorTree().then(() => {
resolve(true);
}).catch(() => {
resolve(false);
});
} else {
resolve(true);
}
});
};
const newBehaviorTree = async () => {
const canProceed = await checkUnsavedChanges();
if (canProceed) {
treeNodes.value = [];
selectedNodeId.value = null;
connections.value = [];
tempConnection.value.path = '';
// 清空黑板
if (blackboardOperations) {
blackboardOperations.clearBlackboard();
}
clearCurrentFile();
markAsSaved();
}
};
const saveBehaviorTree = async (): Promise<boolean> => {
if (currentFilePath.value) {
return await saveToCurrentFile();
} else {
return await saveAsBehaviorTree();
}
};
const saveToCurrentFile = async (): Promise<boolean> => {
if (!currentFilePath.value) {
return await saveAsBehaviorTree();
}
try {
const data = exportBehaviorTreeData();
const jsonString = JSON.stringify(data, null, 2);
await sendToMain('overwrite-behavior-tree-file', {
filePath: currentFilePath.value,
content: jsonString
});
markAsSaved();
showMessage('保存成功!');
return true;
} catch (error) {
showMessage('保存失败: ' + error, 'error');
return false;
}
};
const saveAsBehaviorTree = async (): Promise<boolean> => {
try {
const data = exportBehaviorTreeData();
const jsonString = JSON.stringify(data, null, 2);
const result = await Editor.Dialog.save({
title: '保存行为树文件',
filters: [
{ name: '行为树文件', extensions: ['bt.json', 'json'] },
{ name: '所有文件', extensions: ['*'] }
]
});
if (result.canceled || !result.filePath) {
return false;
}
const fs = require('fs-extra');
await fs.writeFile(result.filePath, jsonString);
const path = require('path');
const fileName = path.basename(result.filePath, path.extname(result.filePath));
setCurrentFile(fileName, result.filePath);
showMessage(`保存成功!文件: ${result.filePath}`);
return true;
} catch (error) {
showMessage('另存为失败: ' + error, 'error');
return false;
}
};
const saveToFile = async (fileName: string, jsonString: string): Promise<boolean> => {
try {
await sendToMain('create-behavior-tree-from-editor', {
fileName: fileName + '.json',
content: jsonString
});
setCurrentFile(fileName, `assets/${fileName}.bt.json`);
showMessage(`保存成功!文件名: ${fileName}.json`);
return true;
} catch (error) {
showMessage('保存失败: ' + error, 'error');
return false;
}
};
const getFileNameFromUser = (): Promise<string | null> => {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #2d2d2d;
color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
min-width: 300px;
`;
dialog.innerHTML = `
<h3 style="margin: 0 0 15px 0; color: #ffffff;">保存行为树</h3>
<p style="margin: 0 0 15px 0; color: #cccccc;">请输入文件名(不含扩展名):</p>
<input type="text" id="filename-input" value="${currentFileName.value || 'behavior_tree'}"
style="width: 100%; padding: 8px; border: 1px solid #555; background: #1a1a1a; color: #ffffff; border-radius: 4px; margin-bottom: 15px;">
<div style="text-align: right;">
<button id="cancel-btn" style="padding: 8px 16px; margin-right: 8px; background: #555; color: #fff; border: none; border-radius: 4px; cursor: pointer;">取消</button>
<button id="save-btn" style="padding: 8px 16px; background: #007acc; color: #fff; border: none; border-radius: 4px; cursor: pointer;">保存</button>
</div>
`;
overlay.appendChild(dialog);
document.body.appendChild(overlay);
const input = dialog.querySelector('#filename-input') as HTMLInputElement;
const saveBtn = dialog.querySelector('#save-btn') as HTMLButtonElement;
const cancelBtn = dialog.querySelector('#cancel-btn') as HTMLButtonElement;
input.focus();
input.select();
const cleanup = () => {
document.body.removeChild(overlay);
};
saveBtn.onclick = () => {
const fileName = input.value.trim();
cleanup();
resolve(fileName || null);
};
cancelBtn.onclick = () => {
cleanup();
resolve(null);
};
input.onkeydown = (e) => {
if (e.key === 'Enter') {
const fileName = input.value.trim();
cleanup();
resolve(fileName || null);
} else if (e.key === 'Escape') {
cleanup();
resolve(null);
}
};
});
};
const loadFileContent = (fileData: any, filePath: string = '') => {
try {
if (!fileData) {
return;
}
let parsedData = fileData;
if (fileData.rawContent) {
try {
parsedData = JSON.parse(fileData.rawContent);
} catch (e) {
parsedData = {
nodes: [],
connections: []
};
}
}
if (parsedData.nodes && Array.isArray(parsedData.nodes)) {
treeNodes.value = parsedData.nodes.map((node: any) => ({
...node,
x: node.x || 0,
y: node.y || 0,
children: node.children || [],
properties: node.properties || {},
canHaveChildren: node.canHaveChildren !== false,
canHaveParent: node.canHaveParent !== false,
hasError: node.hasError || false
}));
} else if (parsedData.tree) {
const treeNode = parsedData.tree;
const nodes = [treeNode];
const extractNodes = (node: any): any[] => {
const allNodes = [node];
if (node.children && Array.isArray(node.children)) {
node.children.forEach((child: any) => {
if (typeof child === 'object') {
allNodes.push(...extractNodes(child));
}
});
}
return allNodes;
};
const allNodes = extractNodes(treeNode);
treeNodes.value = allNodes.map((node: any, index: number) => ({
...node,
x: node.x || (300 + index * 150),
y: node.y || (100 + Math.floor(index / 3) * 200),
children: Array.isArray(node.children)
? node.children.filter((child: any) => typeof child === 'string')
: [],
properties: node.properties || {},
canHaveChildren: true,
canHaveParent: node.id !== 'root',
hasError: false
}));
} else {
treeNodes.value = [];
}
if (parsedData.connections && Array.isArray(parsedData.connections)) {
connections.value = parsedData.connections.map((conn: any) => ({
id: conn.id || Math.random().toString(36).substr(2, 9),
sourceId: conn.sourceId,
targetId: conn.targetId,
path: conn.path || '',
active: conn.active || false
}));
} else {
connections.value = [];
}
if (fileData._fileInfo) {
const fileName = fileData._fileInfo.fileName || 'untitled';
const fullPath = fileData._fileInfo.filePath || filePath;
setCurrentFile(fileName, fullPath);
} else if (parsedData.metadata?.name) {
setCurrentFile(parsedData.metadata.name, filePath);
} else {
setCurrentFile('untitled', filePath);
}
// 加载黑板数据
if (blackboardOperations && parsedData.blackboard && Array.isArray(parsedData.blackboard)) {
blackboardOperations.loadBlackboardVariables(parsedData.blackboard);
}
selectedNodeId.value = null;
tempConnection.value.path = '';
if (updateConnections) {
setTimeout(() => {
updateConnections();
}, 100);
}
} catch (error) {
console.error('文件加载失败:', error);
showMessage('文件加载失败: ' + error, 'error');
treeNodes.value = [];
connections.value = [];
selectedNodeId.value = null;
setCurrentFile('untitled', '');
}
};
const loadBehaviorTree = async () => {
const canProceed = await checkUnsavedChanges();
if (!canProceed) return;
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,.bt';
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
try {
const configText = event.target?.result as string;
const config = JSON.parse(configText);
if (codeGeneration) {
const newNodes = codeGeneration.createTreeFromConfig(config);
treeNodes.value = newNodes;
selectedNodeId.value = null;
if (config.connections && Array.isArray(config.connections)) {
connections.value = config.connections.map((conn: any) => ({
id: conn.id,
sourceId: conn.sourceId,
targetId: conn.targetId,
path: conn.path || '',
active: conn.active || false
}));
} else {
connections.value = [];
}
tempConnection.value.path = '';
// 加载黑板数据
if (blackboardOperations && config.blackboard && Array.isArray(config.blackboard)) {
blackboardOperations.loadBlackboardVariables(config.blackboard);
}
const fileName = file.name.replace(/\.(json|bt)$/, '');
setCurrentFile(fileName, '');
setTimeout(() => {
if (updateConnections) {
updateConnections();
}
}, 100);
} else {
showMessage('代码生成器未初始化', 'error');
}
} catch (error) {
showMessage('配置文件格式错误', 'error');
}
};
reader.readAsText(file);
}
};
input.click();
};
const exportConfig = () => {
showExportModal.value = true;
};
return {
newBehaviorTree,
saveBehaviorTree,
saveAsBehaviorTree,
loadBehaviorTree,
loadFileContent,
exportConfig,
hasUnsavedChanges,
markAsSaved,
setCurrentFile,
clearCurrentFile,
currentFileName,
currentFilePath
};
}

View File

@@ -1,60 +0,0 @@
import { Ref } from 'vue';
import { checkBehaviorTreeInstalled, installBehaviorTreeAI } from '../utils/installUtils';
/**
* 安装管理
*/
export function useInstallation(
checkingStatus: Ref<boolean>,
isInstalled: Ref<boolean>,
version: Ref<string | null>,
isInstalling: Ref<boolean>
) {
// 检查安装状态
const checkInstallStatus = async () => {
checkingStatus.value = true;
try {
const result = await checkBehaviorTreeInstalled(Editor.Project.path);
isInstalled.value = result.installed;
version.value = result.version;
} catch (error) {
console.error('检查AI系统安装状态失败:', error);
isInstalled.value = false;
version.value = null;
} finally {
checkingStatus.value = false;
}
};
// 处理安装
const handleInstall = async () => {
isInstalling.value = true;
try {
const result = await installBehaviorTreeAI(Editor.Project.path);
if (result) {
// 等待文件系统更新
await new Promise(resolve => setTimeout(resolve, 2000));
await checkInstallStatus();
// 如果第一次检查失败,再次尝试
if (!isInstalled.value) {
await new Promise(resolve => setTimeout(resolve, 1000));
await checkInstallStatus();
}
} else {
console.error('AI系统安装失败');
}
} catch (error) {
console.error('安装AI系统时发生错误:', error);
} finally {
isInstalling.value = false;
}
};
return {
checkInstallStatus,
handleInstall
};
}

View File

@@ -1,86 +0,0 @@
/**
* 节点显示管理功能
*/
export function useNodeDisplay() {
// 检查节点是否有可见属性
const hasVisibleProperties = (node: any) => {
if (!node.properties) return false;
return Object.keys(getVisibleProperties(node)).length > 0;
};
// 获取可见属性
const getVisibleProperties = (node: any) => {
if (!node.properties) return {};
const visibleProps: any = {};
for (const [key, prop] of Object.entries(node.properties)) {
if (shouldShowProperty(prop as any, key)) {
visibleProps[key] = prop;
}
}
return visibleProps;
};
// 判断属性是否应该显示
const shouldShowProperty = (prop: any, key: string) => {
// 总是显示这些重要属性
const alwaysShow = ['abortType', 'repeatCount', 'priority'];
if (alwaysShow.includes(key)) {
return true;
}
// 对于其他属性,只在非默认值时显示
if (prop.type === 'string' && prop.value && prop.value.trim() !== '') {
return true;
}
if (prop.type === 'number' && prop.value !== 0 && prop.value !== -1) {
return true;
}
if (prop.type === 'boolean' && prop.value === true) {
return true;
}
if (prop.type === 'select' && prop.value !== 'None' && prop.value !== '') {
return true;
}
if (prop.type === 'code' && prop.value && prop.value.trim() !== '' && prop.value !== '(context) => true') {
return true;
}
return false;
};
// 格式化属性值显示
const formatPropertyValue = (prop: any) => {
switch (prop.type) {
case 'boolean':
return prop.value ? '✓' : '✗';
case 'number':
return prop.value.toString();
case 'select':
return prop.value;
case 'string':
return prop.value.length > 15 ? prop.value.substring(0, 15) + '...' : prop.value;
case 'code':
const code = prop.value || '';
if (code.length > 20) {
// 尝试提取函数体的关键部分
const bodyMatch = code.match(/=>\s*(.+)/) || code.match(/{\s*(.+?)\s*}/);
if (bodyMatch) {
const body = bodyMatch[1].trim();
return body.length > 15 ? body.substring(0, 15) + '...' : body;
}
return code.substring(0, 20) + '...';
}
return code;
default:
return prop.value?.toString() || '';
}
};
return {
hasVisibleProperties,
getVisibleProperties,
formatPropertyValue
};
}

View File

@@ -1,190 +0,0 @@
import { Ref, nextTick } from 'vue';
import { TreeNode, Connection } from '../types';
import { NodeTemplate } from '../data/nodeTemplates';
import { createNodeFromTemplate } from '../utils/nodeUtils';
import { getCanvasCoordinates } from '../utils/canvasUtils';
/**
* 节点操作管理
*/
export function useNodeOperations(
treeNodes: Ref<TreeNode[]>,
selectedNodeId: Ref<string | null>,
connections: Ref<Connection[]>,
panX: Ref<number>,
panY: Ref<number>,
zoomLevel: Ref<number>,
getNodeByIdLocal: (id: string) => TreeNode | undefined,
updateConnections?: () => void
) {
// 获取相对于画布的坐标(用于节点拖放等操作)
const getCanvasCoords = (event: MouseEvent, canvasElement: HTMLElement | null) => {
return getCanvasCoordinates(event, canvasElement, panX.value, panY.value, zoomLevel.value);
};
// 拖拽事件处理
const onNodeDragStart = (event: DragEvent, template: NodeTemplate) => {
if (event.dataTransfer) {
// 检查是否为条件节点,如果是则标记为条件拖拽
const dragData = {
...template,
isConditionDrag: template.isDraggableCondition || false
};
event.dataTransfer.setData('application/json', JSON.stringify(dragData));
event.dataTransfer.effectAllowed = 'copy';
}
};
const onCanvasDragOver = (event: DragEvent) => {
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
}
};
const onCanvasDrop = (event: DragEvent) => {
event.preventDefault();
const templateData = event.dataTransfer?.getData('application/json');
if (!templateData) return;
try {
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);
const newNode = createNodeFromTemplate(template, x, y);
treeNodes.value.push(newNode);
selectedNodeId.value = newNode.id;
} catch (error) {
// 节点创建失败时静默处理
}
};
// 节点删除(递归删除子节点)
const deleteNode = (nodeId: string) => {
const deleteRecursive = (id: string) => {
const node = getNodeByIdLocal(id);
if (!node) return;
// 递归删除子节点
node.children.forEach(childId => deleteRecursive(childId));
// 从父节点的children中移除
if (node.parent) {
const parent = getNodeByIdLocal(node.parent);
if (parent) {
const index = parent.children.indexOf(id);
if (index > -1) {
parent.children.splice(index, 1);
}
}
}
// 移除连接
connections.value = connections.value.filter(conn =>
conn.sourceId !== id && conn.targetId !== id
);
// 从树中移除节点
const nodeIndex = treeNodes.value.findIndex(n => n.id === id);
if (nodeIndex > -1) {
treeNodes.value.splice(nodeIndex, 1);
}
};
deleteRecursive(nodeId);
if (selectedNodeId.value === nodeId) {
selectedNodeId.value = null;
}
// 更新连接线
if (updateConnections) {
updateConnections();
}
};
// 通用的属性更新方法
const setNestedProperty = (obj: any, path: string, value: any) => {
const keys = path.split('.');
let current = obj;
// 导航到目标属性的父对象
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
current[key] = {};
}
current = current[key];
}
// 设置最终值
const finalKey = keys[keys.length - 1];
current[finalKey] = value;
};
// 节点属性更新
const updateNodeProperty = (path: string, value: any) => {
const selectedNode = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
if (!selectedNode) return;
// 检查是否是条件节点的属性更新
if (selectedNode.isConditionNode && selectedNode.parentDecorator) {
// 条件节点的属性更新需要同步到装饰器
updateConditionNodeProperty(selectedNode.parentDecorator, path, value);
} else {
// 普通节点的属性更新
setNestedProperty(selectedNode, path, value);
// 强制触发响应式更新
const nodeIndex = treeNodes.value.findIndex(n => n.id === selectedNode.id);
if (nodeIndex > -1) {
const newNodes = [...treeNodes.value];
newNodes[nodeIndex] = { ...selectedNode };
treeNodes.value = newNodes;
}
}
};
// 更新条件节点属性到装饰器
const updateConditionNodeProperty = (decoratorNode: TreeNode, path: string, value: any) => {
// 解析属性路径,例如 "properties.variableName.value" -> "variableName"
const pathParts = path.split('.');
if (pathParts[0] === 'properties' && pathParts[2] === 'value') {
const propertyName = pathParts[1];
// 直接更新装饰器的属性
if (!decoratorNode.properties) {
decoratorNode.properties = {};
}
decoratorNode.properties[propertyName] = value;
// 强制触发响应式更新
const nodeIndex = treeNodes.value.findIndex(n => n.id === decoratorNode.id);
if (nodeIndex > -1) {
const newNodes = [...treeNodes.value];
newNodes[nodeIndex] = { ...decoratorNode };
treeNodes.value = newNodes;
}
}
};
return {
getCanvasCoords,
onNodeDragStart,
onCanvasDragOver,
onCanvasDrop,
deleteNode,
updateNodeProperty
};
}

View File

@@ -1,180 +0,0 @@
import { readFileSync } from 'fs-extra';
import { join } from 'path';
import { createApp, App, defineComponent } from 'vue';
import { useBehaviorTreeEditor } from './composables/useBehaviorTreeEditor';
import { EventManager } from './utils/EventManager';
// Vue应用实例
let panelDataMap = new WeakMap<any, any>();
// 待处理的文件队列
let pendingFileData: any = null;
// Vue应用是否已挂载完成
let vueAppMounted: boolean = false;
// 存储面板实例用于访问面板的DOM元素
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/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;
if (!filePath) {
throw new Error('无法获取文件路径');
}
const fs = require('fs-extra');
const path = require('path');
if (!fs.existsSync(filePath)) {
throw new Error(`文件不存在: ${filePath}`);
}
const content = await fs.readFile(filePath, 'utf8');
let fileContent: any;
try {
fileContent = JSON.parse(content);
} catch (parseError) {
fileContent = {
version: "1.0.0",
type: "behavior-tree",
tree: { id: "root", type: "sequence", children: [] }
};
}
const fileInfo = {
...fileContent,
_fileInfo: {
fileName: path.basename(filePath, path.extname(filePath)),
filePath: filePath
}
};
const notifyVueComponent = () => {
const appContainer = currentPanelInstance?.$.app;
if (appContainer && vueAppMounted) {
if (typeof (appContainer as any).loadFileContent === 'function') {
(appContainer as any).loadFileContent(fileInfo);
} else {
const event = new CustomEvent('load-behavior-tree-file', { detail: fileInfo });
document.dispatchEvent(event);
}
} else {
pendingFileData = fileInfo;
}
};
notifyVueComponent();
if (pendingFileData) {
setTimeout(() => {
if (pendingFileData) {
notifyVueComponent();
}
}, 500);
}
return { success: true, message: '文件加载成功' };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const event = new CustomEvent('file-load-error', { detail: { error: errorMessage } });
document.dispatchEvent(event);
return { success: false, error: errorMessage };
}
},
},
ready() {
currentPanelInstance = this;
if (this.$.app) {
try {
const BehaviorTreeEditor = defineComponent({
setup() {
const editor = useBehaviorTreeEditor();
return editor;
},
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/BehaviorTreeEditor.html'), 'utf-8')
});
const app = createApp(BehaviorTreeEditor);
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
app.config.errorHandler = (err, instance, info) => {
console.error('[BehaviorTreePanel] Vue错误:', err, info);
};
app.component('tree-node-item', defineComponent({
props: ['node', 'level', 'getNodeByIdLocal'],
emits: ['node-select'],
template: `
<div class="tree-node-item"
:class="'level-' + level"
@click="$emit('node-select', node)">
<span class="node-icon">{{ node.icon || '●' }}</span>
<span class="node-name">{{ node.name || node.type }}</span>
<span class="node-type">{{ node.type }}</span>
</div>
`
}));
app.mount(this.$.app);
panelDataMap.set(this, app);
vueAppMounted = true;
if (pendingFileData) {
const event = new CustomEvent('load-behavior-tree-file', { detail: pendingFileData });
document.dispatchEvent(event);
pendingFileData = null;
}
} catch (error) {
console.error('[BehaviorTreePanel] 初始化失败:', error);
}
}
},
/**
* 面板关闭时调用
*/
close() {
try {
const app = panelDataMap.get(this);
if (app) {
app.unmount();
panelDataMap.delete(this);
}
EventManager.getInstance().cleanup();
} catch (error) {
console.error('[BehaviorTreePanel] 清理资源时发生错误:', error);
}
}
};
// 导出面板定义 - 使用Editor.Panel.define()包装
module.exports = Editor.Panel.define(panelDefinition);

View File

@@ -1,81 +0,0 @@
import { PropertyDefinition } from '../data/nodeTemplates';
export interface TreeNode {
id: string;
type: string;
name: string;
icon: string;
description: string;
x: number;
y: number;
children: string[];
parent?: string;
properties?: Record<string, any>; // 改为any以支持动态属性值
canHaveChildren: boolean;
canHaveParent: boolean;
maxChildren?: number; // 最大子节点数量限制
minChildren?: number; // 最小子节点数量要求
hasError?: boolean;
// 条件装饰器相关
attachedCondition?: {
type: string;
name: string;
icon: string;
};
// 条件节点相关(用于虚拟条件节点)
isConditionNode?: boolean;
parentDecorator?: TreeNode;
// 条件显示状态
conditionExpanded?: boolean; // 条件是否展开显示详细信息
}
export interface Connection {
id: string;
sourceId: string;
targetId: string;
path: string;
active: boolean;
}
export interface DragState {
isDraggingCanvas: boolean;
isDraggingNode: boolean;
isConnecting: boolean;
dragStartX: number;
dragStartY: number;
dragNodeId: string | null;
dragNodeStartX: number;
dragNodeStartY: number;
connectionStart: { nodeId: string; portType: string } | null;
connectionEnd: { x: number; y: number };
}
export interface InstallStatus {
installed: boolean;
version: string | null;
packageExists: boolean;
}
export interface ValidationResult {
isValid: boolean;
message: string;
}
export interface ConnectionPort {
nodeId: string;
portType: string;
}
export interface CanvasCoordinates {
x: number;
y: number;
}
export interface ConnectionState {
isConnecting: boolean;
startNodeId: string | null;
startPortType: 'input' | 'output' | null;
currentMousePos: { x: number; y: number } | null;
startPortPos: { x: number; y: number } | null;
hoveredPort: { nodeId: string; portType: 'input' | 'output' } | null;
}

View File

@@ -1,104 +0,0 @@
/**
* 事件管理器 - 统一处理面板的事件通信
*/
export class EventManager {
private static instance: EventManager;
private eventListeners: Map<string, EventListener[]> = new Map();
private constructor() {}
static getInstance(): EventManager {
if (!EventManager.instance) {
EventManager.instance = new EventManager();
}
return EventManager.instance;
}
/**
* 添加事件监听器
*/
addEventListener(eventType: string, listener: EventListener): void {
if (!this.eventListeners.has(eventType)) {
this.eventListeners.set(eventType, []);
}
const listeners = this.eventListeners.get(eventType)!;
listeners.push(listener);
// 添加到DOM
document.addEventListener(eventType, listener);
console.log(`[EventManager] 添加事件监听器: ${eventType}`);
}
/**
* 移除事件监听器
*/
removeEventListener(eventType: string, listener: EventListener): void {
const listeners = this.eventListeners.get(eventType);
if (listeners) {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
document.removeEventListener(eventType, listener);
console.log(`[EventManager] 移除事件监听器: ${eventType}`);
}
}
}
/**
* 移除特定类型的所有监听器
*/
removeAllListeners(eventType: string): void {
const listeners = this.eventListeners.get(eventType);
if (listeners) {
listeners.forEach(listener => {
document.removeEventListener(eventType, listener);
});
this.eventListeners.delete(eventType);
console.log(`[EventManager] 移除所有 ${eventType} 事件监听器`);
}
}
/**
* 清理所有事件监听器
*/
cleanup(): void {
this.eventListeners.forEach((listeners, eventType) => {
listeners.forEach(listener => {
document.removeEventListener(eventType, listener);
});
});
this.eventListeners.clear();
console.log('[EventManager] 清理所有事件监听器');
}
/**
* 发送消息到主进程
*/
static sendToMain(message: string, ...args: any[]): void {
try {
if (typeof (window as any).sendToMain === 'function') {
(window as any).sendToMain(message, ...args);
console.log(`[EventManager] 发送消息到主进程: ${message}`, args);
} else {
console.error('[EventManager] sendToMain 方法不可用');
}
} catch (error) {
console.error('[EventManager] 发送消息失败:', error);
}
}
/**
* 触发自定义事件
*/
static dispatch(eventType: string, detail?: any): void {
try {
const event = new CustomEvent(eventType, { detail });
document.dispatchEvent(event);
console.log(`[EventManager] 触发事件: ${eventType}`, detail);
} catch (error) {
console.error(`[EventManager] 触发事件失败: ${eventType}`, error);
}
}
}

View File

@@ -1,109 +0,0 @@
import { CanvasCoordinates } from '../types';
/**
* 获取相对于画布的坐标(考虑缩放和平移)
*/
export function getCanvasCoordinates(
event: MouseEvent,
canvasElement: HTMLElement | null,
panX: number,
panY: number,
zoomLevel: number
): CanvasCoordinates {
if (!canvasElement) {
return { x: 0, y: 0 };
}
try {
const rect = canvasElement.getBoundingClientRect();
const x = (event.clientX - rect.left - panX) / zoomLevel;
const y = (event.clientY - rect.top - panY) / zoomLevel;
return { x, y };
} catch (error) {
return { x: 0, y: 0 };
}
}
/**
* 计算网格样式
*/
export function getGridStyle(panX: number, panY: number, zoomLevel: number) {
const gridSize = 20 * zoomLevel;
return {
backgroundSize: `${gridSize}px ${gridSize}px`,
backgroundPosition: `${panX % gridSize}px ${panY % gridSize}px`
};
}
/**
* 计算视图居中的平移值
*/
export function calculateCenterView(
nodes: any[],
canvasWidth: number,
canvasHeight: number,
zoomLevel: number
): { panX: number; panY: number } {
if (nodes.length === 0) {
return { panX: 0, panY: 0 };
}
// 计算所有节点的边界
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
nodes.forEach(node => {
minX = Math.min(minX, node.x);
minY = Math.min(minY, node.y);
maxX = Math.max(maxX, node.x + 150);
maxY = Math.max(maxY, node.y + 100);
});
// 计算中心点
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
// 设置平移,使内容居中
const panX = canvasWidth / 2 - centerX * zoomLevel;
const panY = canvasHeight / 2 - centerY * zoomLevel;
return { panX, panY };
}
/**
* 约束缩放级别
*/
export function constrainZoom(zoom: number): number {
return Math.max(0.3, Math.min(zoom, 3));
}
/**
* 计算缩放后的坐标
*/
export function transformCoordinate(
x: number,
y: number,
panX: number,
panY: number,
zoomLevel: number
): { x: number; y: number } {
return {
x: x * zoomLevel + panX,
y: y * zoomLevel + panY
};
}
/**
* 计算逆向变换的坐标(从屏幕坐标到画布坐标)
*/
export function inverseTransformCoordinate(
screenX: number,
screenY: number,
panX: number,
panY: number,
zoomLevel: number
): { x: number; y: number } {
return {
x: (screenX - panX) / zoomLevel,
y: (screenY - panY) / zoomLevel
};
}

View File

@@ -1,95 +0,0 @@
import { InstallStatus } from '../types';
import * as fs from 'fs';
import * as path from 'path';
/**
* 检查行为树AI系统是否已安装
* 通过主进程检查项目中是否安装了@esengine/ai包
*/
export async function checkBehaviorTreeInstalled(projectPath: string): Promise<InstallStatus> {
try {
// 通过Editor.Message请求主进程检查安装状态
const isInstalled = await Editor.Message.request('cocos-ecs-extension', 'check-behavior-tree-installed');
if (isInstalled) {
// 如果已安装,读取版本信息
const packageJsonPath = path.join(projectPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
const aiPackage = dependencies['@esengine/ai'];
return {
installed: true,
version: aiPackage || null,
packageExists: true
};
}
}
return {
installed: false,
version: null,
packageExists: fs.existsSync(path.join(projectPath, 'package.json'))
};
} catch (error) {
return {
installed: false,
version: null,
packageExists: false
};
}
}
/**
* 格式化安装状态文本
*/
export function getInstallStatusText(
isChecking: boolean,
isInstalling: boolean,
isInstalled: boolean,
version: string | null
): string {
if (isChecking) return '检查状态中...';
if (isInstalling) return '正在安装AI系统...';
return isInstalled ? 'AI系统已安装' : 'AI系统未安装';
}
/**
* 获取安装状态CSS类
*/
export function getInstallStatusClass(
isInstalling: boolean,
isInstalled: boolean
): string {
if (isInstalling) return 'installing';
return isInstalled ? 'installed' : 'not-installed';
}
/**
* 安装行为树AI系统
* 通过发送消息到主进程来执行真实的npm安装命令
*/
export async function installBehaviorTreeAI(projectPath: string): Promise<boolean> {
try {
const result = await Editor.Message.request('cocos-ecs-extension', 'install-behavior-tree');
return Boolean(result);
} catch (error) {
console.error('请求安装AI系统失败:', error);
return false;
}
}
/**
* 更新行为树AI系统
* 通过发送消息到主进程来执行真实的npm更新命令
*/
export async function updateBehaviorTreeAI(projectPath: string): Promise<boolean> {
try {
const result = await Editor.Message.request('cocos-ecs-extension', 'update-behavior-tree');
return Boolean(result);
} catch (error) {
console.error('请求更新AI系统失败:', error);
return false;
}
}

View File

@@ -1,230 +0,0 @@
import { TreeNode, ValidationResult } from '../types';
import { NodeTemplate } from '../data/nodeTemplates';
/**
* 生成唯一的节点ID
*/
export function generateNodeId(): string {
return 'node_' + Math.random().toString(36).substr(2, 9);
}
/**
* 根据模板创建节点
*/
export function createNodeFromTemplate(template: NodeTemplate, x: number = 100, y: number = 100): TreeNode {
const nodeId = generateNodeId();
// 深拷贝 properties 以避免引用共享
let properties: any = {};
if (template.properties) {
for (const [key, prop] of Object.entries(template.properties)) {
properties[key] = {
name: prop.name,
type: prop.type,
value: prop.value,
description: prop.description,
options: prop.options ? [...prop.options] : undefined,
required: prop.required
};
}
}
const node: TreeNode = {
id: nodeId,
type: template.type,
name: template.name,
icon: template.icon,
description: template.description,
x: x,
y: y,
children: [],
properties: properties,
canHaveChildren: template.canHaveChildren,
canHaveParent: template.canHaveParent,
maxChildren: template.maxChildren,
minChildren: template.minChildren,
hasError: false
};
return node;
}
/**
* 根据ID查找节点
*/
export function getNodeById(nodes: TreeNode[], id: string): TreeNode | undefined {
return nodes.find(node => node.id === id);
}
/**
* 获取根节点
*/
export function getRootNode(nodes: TreeNode[]): TreeNode | undefined {
return nodes.find(node => !node.parent);
}
/**
* 递归删除节点及其子节点
*/
export function deleteNodeRecursive(
nodes: TreeNode[],
nodeId: string,
connections: any[],
onConnectionsUpdate: (connections: any[]) => void
): TreeNode[] {
const deleteRecursive = (id: string) => {
const node = getNodeById(nodes, id);
if (!node) return;
// 递归删除子节点
node.children.forEach(childId => deleteRecursive(childId));
// 从父节点的children中移除
if (node.parent) {
const parent = getNodeById(nodes, node.parent);
if (parent) {
const index = parent.children.indexOf(id);
if (index > -1) {
parent.children.splice(index, 1);
}
}
}
// 移除连接
const updatedConnections = connections.filter(conn =>
conn.sourceId !== id && conn.targetId !== id
);
onConnectionsUpdate(updatedConnections);
// 从树中移除节点
const nodeIndex = nodes.findIndex(n => n.id === id);
if (nodeIndex > -1) {
nodes.splice(nodeIndex, 1);
}
};
deleteRecursive(nodeId);
return [...nodes]; // 返回新数组以触发响应式更新
}
/**
* 验证行为树结构
*/
export function validateTree(nodes: TreeNode[]): ValidationResult {
if (nodes.length === 0) {
return { isValid: false, message: '行为树为空' };
}
// 检查根节点
const rootNodes = nodes.filter(node => !node.parent);
if (rootNodes.length === 0) {
return { isValid: false, message: '缺少根节点' };
}
if (rootNodes.length > 1) {
return { isValid: false, message: `发现多个根节点: ${rootNodes.map(n => n.name).join(', ')}` };
}
// 验证每个节点的子节点数量限制
for (const node of nodes) {
const childrenCount = node.children.length;
// 检查最小子节点数量
if (node.minChildren !== undefined && childrenCount < node.minChildren) {
return {
isValid: false,
message: `节点 "${node.name}" 需要至少 ${node.minChildren} 个子节点,当前只有 ${childrenCount}`
};
}
// 检查最大子节点数量
if (node.maxChildren !== undefined && childrenCount > node.maxChildren) {
return {
isValid: false,
message: `节点 "${node.name}" 最多只能有 ${node.maxChildren} 个子节点,当前有 ${childrenCount}`
};
}
// 检查装饰器节点的特殊限制
if (node.type.includes('decorator') || node.type.includes('Decorator')) {
if (childrenCount !== 1) {
return {
isValid: false,
message: `装饰器节点 "${node.name}" 必须有且只能有一个子节点,当前有 ${childrenCount}`
};
}
}
// 检查叶子节点不能有子节点
if (!node.canHaveChildren && childrenCount > 0) {
return {
isValid: false,
message: `叶子节点 "${node.name}" 不能有子节点,但当前有 ${childrenCount}`
};
}
}
// 检查循环引用
const visited = new Set<string>();
const recursionStack = new Set<string>();
function hasCycle(nodeId: string): boolean {
if (recursionStack.has(nodeId)) return true;
if (visited.has(nodeId)) return false;
visited.add(nodeId);
recursionStack.add(nodeId);
const node = getNodeById(nodes, nodeId);
if (node) {
for (const childId of node.children) {
if (hasCycle(childId)) return true;
}
}
recursionStack.delete(nodeId);
return false;
}
for (const node of nodes) {
if (hasCycle(node.id)) {
return { isValid: false, message: '检测到循环引用' };
}
}
return { isValid: true, message: '行为树结构有效' };
}
/**
* 更新节点属性
*/
export function updateNodeProperty(node: TreeNode, path: string, value: any): void {
if (!node.properties) return;
const keys = path.split('.');
let target: any = node.properties;
for (let i = 0; i < keys.length - 1; i++) {
target = target[keys[i]];
}
target[keys[keys.length - 1]] = value;
}
/**
* 计算节点的边界框
*/
export function getNodesBounds(nodes: TreeNode[]): { minX: number; minY: number; maxX: number; maxY: number } {
if (nodes.length === 0) {
return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
}
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
nodes.forEach(node => {
minX = Math.min(minX, node.x);
minY = Math.min(minY, node.y);
maxX = Math.max(maxX, node.x + 150); // 节点宽度
maxY = Math.max(maxY, node.y + 100); // 节点高度
});
return { minX, minY, maxX, maxY };
}

View File

@@ -1,717 +0,0 @@
import { readFileSync } from 'fs-extra';
import { join } from 'path';
import { createApp, App, defineComponent, ref, reactive, onMounted, onUnmounted } from 'vue';
import { WebSocketServer, WebSocket } from 'ws';
import { IncomingMessage } from 'http';
const panelDataMap = new WeakMap<any, App>();
/**
* 游戏实例信息
*/
interface GameInstance {
id: string;
name: string;
connectTime: number;
lastUpdateTime: number;
isActive: boolean;
debugData?: any;
ws?: WebSocket; // WebSocket连接
}
/**
* 详细的调试信息接口
*/
interface DetailedDebugInfo {
// 基础信息
instanceId: string;
instanceName: string;
isRunning: boolean;
frameworkLoaded: boolean;
currentScene: string;
uptime: number;
// 性能信息
performance: {
frameTime: number;
fps: number;
averageFrameTime: number;
minFrameTime: number;
maxFrameTime: number;
frameTimeHistory: number[];
engineFrameTime: number;
ecsPercentage: number;
};
// 内存信息
memory: {
totalMemory: number;
usedMemory: number;
freeMemory: number;
entityMemory: number;
componentMemory: number;
systemMemory: number;
pooledMemory: number;
gcCollections: number;
};
// 实体信息
entities: {
total: number;
active: number;
inactive: number;
pendingAdd: number;
pendingRemove: number;
entitiesPerArchetype: Array<{
signature: string;
count: number;
memory: number;
}>;
topEntitiesByComponents: Array<{
id: string;
name: string;
componentCount: number;
memory: number;
}>;
};
// 组件信息
components: {
totalTypes: number;
totalInstances: number;
componentStats: Array<{
typeName: string;
instanceCount: number;
memoryPerInstance: number;
totalMemory: number;
poolSize: number;
poolUtilization: number;
}>;
};
// 系统信息
systems: {
total: number;
systemStats: Array<{
name: string;
type: string;
entityCount: number;
averageExecutionTime: number;
minExecutionTime: number;
maxExecutionTime: number;
executionTimeHistory: number[];
memoryUsage: number;
updateOrder: number;
enabled: boolean;
percentage: number;
}>;
};
// 场景信息
scenes: {
currentScene: string;
sceneMemory: number;
sceneEntityCount: number;
sceneSystemCount: number;
sceneUptime: number;
};
}
/**
* ECS调试服务器
* 作为服务端,接收多个游戏实例的连接
*/
class ECSDebugServer {
private wss?: WebSocketServer;
private port: number = 8080;
private gameInstances = new Map<string, GameInstance>();
private isRunning: boolean = false;
constructor(port: number = 8080) {
this.port = port;
}
async start(): Promise<boolean> {
if (this.isRunning) return true;
try {
this.wss = new WebSocketServer({ port: this.port });
this.wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
const instanceId = this.generateInstanceId();
const instance: GameInstance = {
id: instanceId,
name: `游戏实例-${instanceId.substring(0, 8)}`,
connectTime: Date.now(),
lastUpdateTime: Date.now(),
isActive: true,
debugData: null,
ws: ws
};
this.gameInstances.set(instanceId, instance);
console.log(`[ECS Debug Server] New instance connected: ${instance.name}`);
ws.on('message', (data: Buffer) => {
try {
const message = JSON.parse(data.toString());
this.handleMessage(instanceId, message);
} catch (error) {
console.error('[ECS Debug Server] Failed to parse message:', error);
}
});
ws.on('close', () => {
const instance = this.gameInstances.get(instanceId);
if (instance) {
instance.isActive = false;
console.log(`[ECS Debug Server] Instance disconnected: ${instance.name}`);
}
});
ws.on('error', (error: Error) => {
console.error(`[ECS Debug Server] WebSocket error for ${instanceId}:`, error);
});
// 发送连接确认
this.sendToInstance(instanceId, {
type: 'connection_confirmed',
instanceId: instanceId,
serverTime: Date.now()
});
});
this.isRunning = true;
console.log(`[ECS Debug Server] Started on port ${this.port}`);
return true;
} catch (error) {
console.error('[ECS Debug Server] Failed to start:', error);
return false;
}
}
stop(): void {
if (this.wss) {
this.wss.close();
this.wss = undefined;
}
this.gameInstances.clear();
this.isRunning = false;
console.log('[ECS Debug Server] Stopped');
}
private generateInstanceId(): string {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
}
private handleMessage(instanceId: string, message: any): void {
const instance = this.gameInstances.get(instanceId);
if (!instance) return;
switch (message.type) {
case 'debug_data':
instance.debugData = message.data;
instance.lastUpdateTime = Date.now();
break;
case 'instance_info':
if (message.name) {
instance.name = message.name;
}
break;
case 'ping':
this.sendToInstance(instanceId, { type: 'pong', timestamp: Date.now() });
break;
}
}
private sendToInstance(instanceId: string, message: any): void {
const instance = this.gameInstances.get(instanceId);
if (instance && instance.ws && instance.ws.readyState === 1) {
instance.ws.send(JSON.stringify(message));
}
}
get running(): boolean {
return this.isRunning;
}
get instances(): GameInstance[] {
return Array.from(this.gameInstances.values());
}
getInstance(instanceId: string): GameInstance | undefined {
return this.gameInstances.get(instanceId);
}
getInstanceDebugData(instanceId: string): DetailedDebugInfo | null {
const instance = this.gameInstances.get(instanceId);
if (!instance || !instance.debugData) {
return null;
}
return this.transformToDetailedDebugInfo(instance, instance.debugData);
}
private transformToDetailedDebugInfo(instance: GameInstance, rawData: any): DetailedDebugInfo {
const uptime = (Date.now() - instance.connectTime) / 1000;
// 计算系统性能数据包括ECS占比
const systemBreakdown = rawData.performance?.systemBreakdown || [];
const systemPerformance = rawData.performance?.systemPerformance || [];
// 创建系统名称到占比的映射
const systemPercentageMap = new Map<string, number>();
systemBreakdown.forEach((sys: any) => {
systemPercentageMap.set(sys.systemName, sys.percentage || 0);
});
return {
instanceId: instance.id,
instanceName: instance.name,
isRunning: rawData.isRunning || false,
frameworkLoaded: rawData.frameworkLoaded || false,
currentScene: rawData.currentScene || '未知',
uptime: uptime,
performance: {
frameTime: rawData.performance?.frameTime || 0,
fps: rawData.performance?.fps || 0,
averageFrameTime: rawData.performance?.averageFrameTime || rawData.performance?.frameTime || 0,
minFrameTime: rawData.performance?.minFrameTime || rawData.performance?.frameTime || 0,
maxFrameTime: rawData.performance?.maxFrameTime || rawData.performance?.frameTime || 0,
frameTimeHistory: rawData.performance?.frameTimeHistory || [],
engineFrameTime: rawData.performance?.engineFrameTime || 0,
ecsPercentage: rawData.performance?.ecsPercentage || 0
},
memory: {
totalMemory: rawData.performance?.memoryDetails?.totalMemory || rawData.memory?.totalMemory || 512 * 1024 * 1024,
usedMemory: rawData.performance?.memoryDetails?.usedMemory || (rawData.performance?.memoryUsage ? rawData.performance.memoryUsage * 1024 * 1024 : 0),
freeMemory: rawData.performance?.memoryDetails?.freeMemory || 0,
entityMemory: rawData.performance?.memoryDetails?.entities || rawData.memory?.entityMemory || 0,
componentMemory: rawData.performance?.memoryDetails?.components || rawData.memory?.componentMemory || 0,
systemMemory: rawData.performance?.memoryDetails?.systems || rawData.memory?.systemMemory || 0,
pooledMemory: rawData.performance?.memoryDetails?.pooled || rawData.memory?.pooledMemory || 0,
gcCollections: rawData.performance?.memoryDetails?.gcCollections || rawData.memory?.gcCollections || 0
},
entities: {
total: rawData.entities?.totalEntities || 0,
active: rawData.entities?.activeEntities || 0,
inactive: (rawData.entities?.totalEntities || 0) - (rawData.entities?.activeEntities || 0),
pendingAdd: rawData.entities?.pendingAdd || 0,
pendingRemove: rawData.entities?.pendingRemove || 0,
entitiesPerArchetype: rawData.entities?.entitiesPerArchetype || [],
topEntitiesByComponents: rawData.entities?.topEntitiesByComponents || []
},
components: {
totalTypes: rawData.components?.componentTypes || 0,
totalInstances: rawData.components?.componentInstances || 0,
componentStats: (rawData.components?.componentStats || []).map((comp: any) => ({
typeName: comp.typeName,
instanceCount: comp.instanceCount || 0,
memoryPerInstance: comp.memoryPerInstance || 0,
totalMemory: comp.totalMemory || (comp.instanceCount || 0) * (comp.memoryPerInstance || 0),
poolSize: comp.poolSize || 0,
poolUtilization: comp.poolSize > 0 ? (comp.instanceCount / comp.poolSize * 100) : 0
}))
},
systems: {
total: rawData.systems?.totalSystems || 0,
systemStats: (rawData.systems?.systemsInfo || []).map((sys: any) => {
const systemName = sys.name;
const percentage = systemPercentageMap.get(systemName) || 0;
return {
name: systemName,
type: sys.type || 'Unknown',
entityCount: sys.entityCount || 0,
averageExecutionTime: sys.executionTime || 0,
minExecutionTime: sys.minExecutionTime || sys.executionTime || 0,
maxExecutionTime: sys.maxExecutionTime || sys.executionTime || 0,
executionTimeHistory: sys.executionTimeHistory || [],
memoryUsage: sys.memoryUsage || 0,
updateOrder: sys.updateOrder || 0,
enabled: sys.enabled !== false,
percentage: percentage
};
})
},
scenes: {
currentScene: rawData.currentScene || '未知',
sceneMemory: rawData.scenes?.sceneMemory || 0,
sceneEntityCount: rawData.entities?.totalEntities || 0,
sceneSystemCount: rawData.systems?.totalSystems || 0,
sceneUptime: rawData.scenes?.sceneUptime || uptime
}
};
}
}
/**
* 默认调试信息
*/
const defaultDebugInfo: DetailedDebugInfo = {
instanceId: '',
instanceName: '未选择实例',
isRunning: false,
frameworkLoaded: false,
currentScene: '未知',
uptime: 0,
performance: {
frameTime: 0,
fps: 0,
averageFrameTime: 0,
minFrameTime: 0,
maxFrameTime: 0,
frameTimeHistory: [],
engineFrameTime: 0,
ecsPercentage: 0
},
memory: {
totalMemory: 0,
usedMemory: 0,
freeMemory: 0,
entityMemory: 0,
componentMemory: 0,
systemMemory: 0,
pooledMemory: 0,
gcCollections: 0
},
entities: {
total: 0,
active: 0,
inactive: 0,
pendingAdd: 0,
pendingRemove: 0,
entitiesPerArchetype: [],
topEntitiesByComponents: []
},
components: {
totalTypes: 0,
totalInstances: 0,
componentStats: []
},
systems: {
total: 0,
systemStats: []
},
scenes: {
currentScene: '未知',
sceneMemory: 0,
sceneEntityCount: 0,
sceneSystemCount: 0,
sceneUptime: 0
}
};
// 全局调试服务器实例
let globalDebugServer: ECSDebugServer | null = null;
/**
* 启动调试服务器
*/
async function ensureDebugServer(): Promise<ECSDebugServer> {
if (!globalDebugServer) {
globalDebugServer = new ECSDebugServer(8080);
}
if (!globalDebugServer.running) {
await globalDebugServer.start();
}
return globalDebugServer;
}
module.exports = Editor.Panel.define({
listeners: {
show() { },
hide() { },
},
template: `<div id="app"></div>`,
style: readFileSync(join(__dirname, '../../../static/style/debug/index.css'), 'utf-8'),
$: {
app: '#app',
},
ready() {
if (this.$.app) {
const app = createApp(defineComponent({
setup() {
const debugInfo = reactive<DetailedDebugInfo>({ ...defaultDebugInfo });
const gameInstances = ref<GameInstance[]>([]);
const selectedInstanceId = ref<string>('');
const isAutoRefresh = ref(true);
const refreshInterval = ref(100);
const lastUpdateTime = ref('');
const showComponentPoolHelp = ref(false);
let intervalId: NodeJS.Timeout | null = null;
let debugServer: ECSDebugServer | null = null;
// 初始化调试服务器
const initializeServer = async () => {
try {
debugServer = await ensureDebugServer();
} catch (error) {
console.error('Failed to start debug server:', error);
}
};
// 更新游戏实例列表和调试信息
const updateDebugInfo = async () => {
if (!debugServer) return;
try {
// 更新实例列表
gameInstances.value = debugServer.instances;
// 如果有选中的实例,更新其调试信息
if (selectedInstanceId.value) {
const detailedInfo = debugServer.getInstanceDebugData(selectedInstanceId.value);
if (detailedInfo) {
Object.assign(debugInfo, detailedInfo);
} else {
// 实例已断开,重置选择
selectedInstanceId.value = '';
Object.assign(debugInfo, defaultDebugInfo);
}
}
lastUpdateTime.value = new Date().toLocaleTimeString();
} catch (error) {
console.error('Failed to update debug info:', error);
}
};
// 开始自动刷新
const startAutoRefresh = () => {
if (intervalId) clearInterval(intervalId);
if (isAutoRefresh.value) {
intervalId = setInterval(updateDebugInfo, refreshInterval.value);
}
};
// 停止自动刷新
const stopAutoRefresh = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
};
// 手动刷新
const manualRefresh = () => {
updateDebugInfo();
};
// 切换自动刷新
const toggleAutoRefresh = () => {
if (isAutoRefresh.value) {
startAutoRefresh();
} else {
stopAutoRefresh();
}
};
// 更改刷新间隔
const changeRefreshInterval = () => {
if (isAutoRefresh.value) {
startAutoRefresh();
}
};
// 实例选择改变
const onInstanceChanged = () => {
if (selectedInstanceId.value) {
updateDebugInfo();
} else {
Object.assign(debugInfo, defaultDebugInfo);
}
};
// 格式化运行时间
const formatUptime = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// 格式化内存大小
const formatMemory = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
};
// 获取FPS颜色
const getFpsColor = (fps: number): string => {
if (fps >= 55) return 'good';
if (fps >= 30) return 'warning';
return 'critical';
};
// 获取内存颜色
const getMemoryColor = (percentage: number): string => {
if (percentage < 70) return 'good';
if (percentage < 85) return 'warning';
return 'critical';
};
// 获取ECS时间占比颜色
const getECSTimeColor = (percentage: number): string => {
if (!percentage) return 'good';
if (percentage <= 10) return 'good'; // ECS占用<=10%为绿色
if (percentage <= 30) return 'warning'; // ECS占用<=30%为黄色
return 'critical'; // ECS占用>30%为红色
};
// 获取执行时间颜色
const getExecutionTimeColor = (time: number): string => {
if (time < 1) return 'good'; // <1ms为绿色
if (time < 5) return 'warning'; // <5ms为黄色
return 'critical';
};
// 打开文档链接
const openDocumentation = (section: string): void => {
const urls: Record<string, string> = {
'component-pool': 'https://github.com/esengine/ecs-framework/tree/master/docs/component-design-guide.md#1-对象池优化',
'performance-optimization': 'https://github.com/esengine/ecs-framework/tree/master/docs/performance-optimization.md'
};
const url = urls[section];
if (!url) return;
try {
// 在Cocos Creator扩展环境中直接使用Electron的shell模块
const { shell } = require('electron');
shell.openExternal(url);
} catch (error) {
console.error('无法打开链接:', error);
// 如果失败,复制到剪贴板
copyUrlToClipboard(url);
}
};
// 复制链接到剪贴板的辅助函数
const copyUrlToClipboard = (url: string): void => {
try {
// 尝试使用现代的剪贴板API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(() => {
console.log(`文档链接已复制到剪贴板: ${url}`);
// 如果可能的话,显示用户友好的提示
if (typeof alert !== 'undefined') {
alert(`文档链接已复制到剪贴板,请在浏览器中粘贴访问:\n${url}`);
}
}).catch(() => {
fallbackCopyText(url);
});
} else {
fallbackCopyText(url);
}
} catch (error) {
fallbackCopyText(url);
}
};
// 备用的复制文本方法
const fallbackCopyText = (text: string): void => {
try {
// 创建临时的文本区域
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
console.log(`文档链接已复制到剪贴板: ${text}`);
if (typeof alert !== 'undefined') {
alert(`文档链接已复制到剪贴板,请在浏览器中粘贴访问:\n${text}`);
}
} else {
console.log(`请手动复制文档链接: ${text}`);
if (typeof alert !== 'undefined') {
alert(`请手动复制文档链接:\n${text}`);
}
}
} catch (error) {
console.log(`请手动访问文档: ${text}`);
if (typeof alert !== 'undefined') {
alert(`请手动访问文档:\n${text}`);
}
}
};
// 组件挂载时初始化
onMounted(async () => {
await initializeServer();
updateDebugInfo();
startAutoRefresh();
});
// 组件卸载时清理
onUnmounted(() => {
stopAutoRefresh();
});
return {
debugInfo,
gameInstances,
selectedInstanceId,
isAutoRefresh,
refreshInterval,
lastUpdateTime,
showComponentPoolHelp,
manualRefresh,
toggleAutoRefresh,
changeRefreshInterval,
onInstanceChanged,
formatUptime,
formatMemory,
getFpsColor,
getMemoryColor,
getECSTimeColor,
getExecutionTimeColor,
openDocumentation
};
},
template: readFileSync(join(__dirname, '../../../static/template/debug/index.html'), 'utf-8'),
}));
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
app.mount(this.$.app);
panelDataMap.set(this, app);
}
},
beforeClose() { },
close() {
const app = panelDataMap.get(this);
if (app) {
app.unmount();
panelDataMap.delete(this);
}
// 关闭调试服务器
if (globalDebugServer) {
globalDebugServer.stop();
globalDebugServer = null;
}
},
});

View File

@@ -1,528 +0,0 @@
/* eslint-disable vue/one-component-per-file */
import { readFileSync } from 'fs-extra';
import { join } from 'path';
import { createApp, App, defineComponent, ref, onMounted } from 'vue';
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
const panelDataMap = new WeakMap<any, App>();
/**
* 检测ECS框架安装状态的工具函数
*/
async function checkEcsFrameworkStatus(projectPath: string) {
const packageJsonPath = path.join(projectPath, 'package.json');
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
try {
// 检查package.json是否存在
const packageJsonExists = fs.existsSync(packageJsonPath);
if (!packageJsonExists) {
return {
packageJsonExists: false,
ecsInstalled: false,
ecsVersion: null
};
}
// 读取package.json
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
// 检查依赖中是否包含ECS框架
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
const ecsInDeps = dependencies['@esengine/ecs-framework'];
// 检查node_modules中是否实际安装了ECS框架
const ecsActuallyInstalled = fs.existsSync(nodeModulesPath);
let ecsVersion = null;
if (ecsActuallyInstalled) {
try {
const ecsPackageJson = JSON.parse(
fs.readFileSync(path.join(nodeModulesPath, 'package.json'), 'utf-8')
);
ecsVersion = ecsPackageJson.version;
} catch (e) {
console.warn('Unable to read ECS framework version:', e);
}
}
return {
packageJsonExists: true,
ecsInstalled: ecsActuallyInstalled && !!ecsInDeps,
ecsVersion,
declaredVersion: ecsInDeps
};
} catch (error) {
console.error('Error checking ECS framework status:', error);
return {
packageJsonExists: false,
ecsInstalled: false,
ecsVersion: null
};
}
}
/**
* 检测ECS模板状态
*/
function checkEcsTemplateStatus(projectPath: string) {
const ecsDir = path.join(projectPath, 'assets', 'scripts', 'ecs');
try {
if (!fs.existsSync(ecsDir)) {
return {
templateExists: false,
existingFiles: []
};
}
// 扫描ECS目录中的文件
const existingFiles: string[] = [];
function scanDirectory(dirPath: string, relativePath: string = '') {
if (!fs.existsSync(dirPath)) return;
const items = fs.readdirSync(dirPath);
for (const item of items) {
const fullPath = path.join(dirPath, item);
const relativeFilePath = relativePath ? `${relativePath}/${item}` : item;
if (fs.statSync(fullPath).isDirectory()) {
scanDirectory(fullPath, relativeFilePath);
} else {
existingFiles.push(relativeFilePath);
}
}
}
scanDirectory(ecsDir);
return {
templateExists: existingFiles.length > 0,
existingFiles
};
} catch (error) {
console.error('Error checking ECS template status:', error);
return {
templateExists: false,
existingFiles: []
};
}
}
/**
* 获取ECS框架的最新版本
*/
function getLatestEcsVersion(): Promise<string | null> {
return new Promise((resolve) => {
exec('npm view @esengine/ecs-framework version', (error, stdout) => {
if (error) {
console.warn('Failed to get latest version:', error);
resolve(null);
} else {
resolve(stdout.trim());
}
});
});
}
/**
* 获取Node.js版本
*/
function getNodeVersion(): Promise<string> {
return new Promise((resolve) => {
exec('node --version', (error, stdout) => {
if (error) {
resolve('未知');
} else {
resolve(stdout.trim().replace('v', ''));
}
});
});
}
/**
* 比较版本号
*/
function compareVersions(current: string, latest: string): boolean {
try {
const currentParts = current.split('.').map(Number);
const latestParts = latest.split('.').map(Number);
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
const currentPart = currentParts[i] || 0;
const latestPart = latestParts[i] || 0;
if (latestPart > currentPart) {
return true; // 有更新
} else if (latestPart < currentPart) {
return false; // 当前版本更新
}
}
return false; // 版本相同
} catch (error) {
console.warn('Version comparison failed:', error);
return false;
}
}
/**
* @zh 如果希望兼容 3.3 之前的版本可以使用下方的代码
* @en You can add the code below if you want compatibility with versions prior to 3.3
*/
// Editor.Panel.define = Editor.Panel.define || function(options: any) { return options }
module.exports = Editor.Panel.define({
listeners: {
show() { console.log('ECS Welcome Panel Show'); },
hide() { console.log('ECS Welcome Panel Hide'); },
},
template: readFileSync(join(__dirname, '../../../static/template/default/index.html'), 'utf-8'),
style: readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8'),
$: {
app: '#app',
},
methods: {
/**
* 向主进程发送消息的方法
*/
sendToMain(message: string, ...args: any[]) {
Editor.Message.send('cocos-ecs-extension', message, ...args);
}
},
ready() {
if (this.$.app) {
const app = createApp({});
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
// ECS欢迎组件
app.component('EcsWelcome', defineComponent({
setup() {
// 响应式状态
const checkingStatus = ref(true);
const ecsInstalled = ref(false);
const ecsVersion = ref<string | null>(null);
const latestVersion = ref<string | null>(null);
const hasUpdate = ref(false);
const packageJsonExists = ref(false);
const nodeVersion = ref('检测中...');
const pluginVersion = ref('1.0.0');
const lastCheckTime = ref<string | null>(null);
// ECS模板状态
const templateExists = ref(false);
const existingFiles = ref<string[]>([]);
// 操作状态
const installing = ref(false);
const updating = ref(false);
const uninstalling = ref(false);
// 操作状态显示
const showOperationStatus = ref(false);
const operationStatusType = ref('loading');
const operationStatusMessage = ref('');
const operationStatusDetails = ref('');
// 显示操作状态
const setOperationStatus = (type: string, message: string, details?: string) => {
showOperationStatus.value = true;
operationStatusType.value = type;
operationStatusMessage.value = message;
operationStatusDetails.value = details || '';
// 自动隐藏成功和警告消息
if (type === 'success' || type === 'warning') {
setTimeout(() => {
showOperationStatus.value = false;
}, 5000);
}
};
// 获取状态图标
const getStatusIcon = (type: string) => {
switch (type) {
case 'loading': return '⏳';
case 'success': return '✅';
case 'error':
case 'failed': return '❌';
case 'warning': return '⚠️';
default: return '';
}
};
// 监听来自主进程的消息 - 暂时注释掉,使用定时刷新
const setupMessageListeners = () => {
// TODO: 使用正确的消息监听API
console.log('Message listeners setup - using polling instead');
};
// 定时检查状态(用于检测操作完成)
let statusCheckInterval: any = null;
const startStatusPolling = () => {
if (statusCheckInterval) clearInterval(statusCheckInterval);
statusCheckInterval = setInterval(() => {
if (installing.value || updating.value || uninstalling.value) {
checkStatus();
}
}, 3000); // 每3秒检查一次
};
const stopStatusPolling = () => {
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
statusCheckInterval = null;
}
};
// 检测状态的方法
const checkStatus = async () => {
checkingStatus.value = true;
try {
// 获取当前项目路径
const projectPath = Editor.Project.path;
// 检测ECS框架状态
const status = await checkEcsFrameworkStatus(projectPath);
const prevInstalled = ecsInstalled.value;
const prevVersion = ecsVersion.value;
packageJsonExists.value = status.packageJsonExists;
ecsInstalled.value = status.ecsInstalled;
ecsVersion.value = status.ecsVersion;
// 检测ECS模板状态
const templateStatus = checkEcsTemplateStatus(projectPath);
templateExists.value = templateStatus.templateExists;
existingFiles.value = templateStatus.existingFiles;
// 检测操作完成
if (installing.value) {
if (status.ecsInstalled && !prevInstalled) {
installing.value = false;
setOperationStatus('success', 'ECS Framework 安装成功!');
stopStatusPolling();
} else if (!status.ecsInstalled) {
// 可能还在安装中,继续等待
}
}
if (updating.value) {
if (status.ecsVersion && status.ecsVersion !== prevVersion) {
updating.value = false;
setOperationStatus('success', `ECS Framework 更新成功到 v${status.ecsVersion}`);
stopStatusPolling();
}
}
if (uninstalling.value) {
if (!status.ecsInstalled && prevInstalled) {
uninstalling.value = false;
setOperationStatus('success', 'ECS Framework 卸载成功!');
stopStatusPolling();
}
}
// 获取Node.js版本
nodeVersion.value = await getNodeVersion();
// 检查更新
if (ecsInstalled.value && ecsVersion.value) {
await checkForUpdates();
}
// 更新检查时间
lastCheckTime.value = new Date().toLocaleString();
} catch (error) {
console.error('Status check failed:', error);
// 如果检查失败,停止操作状态
if (installing.value || updating.value || uninstalling.value) {
installing.value = false;
updating.value = false;
uninstalling.value = false;
setOperationStatus('error', '状态检查失败,请手动验证操作结果');
stopStatusPolling();
}
} finally {
checkingStatus.value = false;
}
};
// 检查更新
const checkForUpdates = async () => {
if (!ecsInstalled.value || !ecsVersion.value) {
setOperationStatus('warning', '请先安装 ECS Framework');
return;
}
try {
setOperationStatus('loading', '正在检查更新...');
const latest = await getLatestEcsVersion();
if (latest) {
latestVersion.value = latest;
const needsUpdate = compareVersions(ecsVersion.value, latest);
hasUpdate.value = needsUpdate;
if (needsUpdate) {
setOperationStatus('success', `发现新版本v${latest}当前v${ecsVersion.value}`);
} else {
setOperationStatus('success', `已是最新版本v${ecsVersion.value}`);
}
} else {
setOperationStatus('warning', '无法获取最新版本信息,请检查网络连接');
}
// 更新检查时间
lastCheckTime.value = new Date().toLocaleString();
} catch (error) {
console.warn('Failed to check updates:', error);
setOperationStatus('error', '检查更新失败,请检查网络连接');
}
};
// 操作方法
const installEcsFramework = () => {
if (!packageJsonExists.value || installing.value) return;
Editor.Dialog.info('安装 ECS Framework', {
detail: '即将安装@esengine/ecs-framework到当前项目...',
buttons: ['确定', '取消'],
default: 0,
}).then((result) => {
if (result.response === 0) {
installing.value = true;
setOperationStatus('loading', '正在安装 ECS Framework...');
startStatusPolling();
// 发送安装命令到主进程
Editor.Message.send('cocos-ecs-extension', 'install-ecs-framework');
}
});
};
const updateEcsFramework = () => {
if (!hasUpdate.value || updating.value) return;
Editor.Dialog.info('更新 ECS Framework', {
detail: `即将更新ECS框架从 v${ecsVersion.value} 到 v${latestVersion.value}`,
buttons: ['确定', '取消'],
default: 0,
}).then((result) => {
if (result.response === 0) {
updating.value = true;
setOperationStatus('loading', `正在更新 ECS Framework 到 v${latestVersion.value}...`);
startStatusPolling();
Editor.Message.send('cocos-ecs-extension', 'update-ecs-framework', latestVersion.value);
}
});
};
const uninstallEcsFramework = () => {
if (uninstalling.value) return;
Editor.Dialog.warn('卸载 ECS Framework', {
detail: '确定要卸载ECS框架吗这将删除项目中的ECS框架依赖。',
buttons: ['确定卸载', '取消'],
default: 1,
}).then((result) => {
if (result.response === 0) {
uninstalling.value = true;
setOperationStatus('loading', '正在卸载 ECS Framework...');
startStatusPolling();
Editor.Message.send('cocos-ecs-extension', 'uninstall-ecs-framework');
}
});
};
const openDocumentation = () => {
if (!ecsInstalled.value) return;
Editor.Message.send('cocos-ecs-extension', 'open-documentation');
};
const createEcsTemplate = () => {
if (!ecsInstalled.value || templateExists.value) return;
Editor.Dialog.info('创建 ECS 模板', {
detail: '即将创建基础的ECS项目结构和启动代码...',
buttons: ['确定', '取消'],
default: 0,
}).then((result) => {
if (result.response === 0) {
Editor.Message.send('cocos-ecs-extension', 'create-ecs-template');
}
});
};
const openGithub = () => {
Editor.Message.send('cocos-ecs-extension', 'open-github');
};
const joinQQGroup = () => {
Editor.Message.send('cocos-ecs-extension', 'open-qq-group');
};
const openGenerator = () => {
Editor.Message.send('cocos-ecs-extension', 'open-generator');
};
// 组件挂载后检测状态
onMounted(() => {
setupMessageListeners();
checkStatus();
});
return {
checkingStatus,
ecsInstalled,
ecsVersion,
latestVersion,
hasUpdate,
packageJsonExists,
nodeVersion,
pluginVersion,
lastCheckTime,
templateExists,
existingFiles,
installing,
updating,
uninstalling,
showOperationStatus,
operationStatusType,
operationStatusMessage,
operationStatusDetails,
getStatusIcon,
installEcsFramework,
updateEcsFramework,
uninstallEcsFramework,
checkForUpdates,
openDocumentation,
createEcsTemplate,
openGithub,
joinQQGroup,
openGenerator
};
},
template: readFileSync(join(__dirname, '../../../static/template/vue/welcome.html'), 'utf-8'),
}));
app.mount(this.$.app);
panelDataMap.set(this, app);
}
},
beforeClose() { },
close() {
const app = panelDataMap.get(this);
if (app) {
app.unmount();
}
},
});

View File

@@ -1,240 +0,0 @@
import { readFileSync } from 'fs-extra';
import { join } from 'path';
import * as path from 'path';
import { createApp, App, defineComponent, ref, reactive } from 'vue';
import { CodeGenerator } from '../../CodeGenerator';
const panelDataMap = new WeakMap<any, App>();
module.exports = Editor.Panel.define({
listeners: {
show() { },
hide() { },
},
template: `<div id="app"></div>`,
style: readFileSync(join(__dirname, '../../../static/style/generator/index.css'), 'utf-8'),
$: {
app: '#app',
},
ready() {
if (this.$.app) {
const app = createApp(defineComponent({
setup() {
const featureName = ref('');
const options = reactive({
generateComponent: true,
generateSystem: false
});
// 组件选项
const componentOptions = reactive({
includeComments: true,
addProperties: []
});
// 系统选项
const systemOptions = reactive({
systemType: 'EntitySystem' as 'EntitySystem' | 'ProcessingSystem' | 'IntervalSystem' | 'PassiveSystem',
includeComments: true,
requiredComponents: [],
filterByComponent: true
});
// 系统类型定义
const systemTypes = [
{
value: 'EntitySystem',
name: 'EntitySystem',
icon: '🔄',
description: '批量处理实体,适合需要遍历多个实体的逻辑',
usage: '适用场景:移动系统、渲染系统、物理碰撞系统'
},
{
value: 'ProcessingSystem',
name: 'ProcessingSystem',
icon: '⚡',
description: '执行全局逻辑,不依赖特定实体',
usage: '适用场景:输入处理、音效管理、场景切换'
},
{
value: 'IntervalSystem',
name: 'IntervalSystem',
icon: '⏰',
description: '按时间间隔执行,可控制执行频率',
usage: '适用场景AI决策、状态保存、定时清理'
},
{
value: 'PassiveSystem',
name: 'PassiveSystem',
icon: '🎯',
description: '被动响应,需要手动调用或事件触发',
usage: '适用场景:技能释放、道具使用、特殊效果'
}
];
const isGenerating = ref(false);
const previewCode = ref('');
const showPreview = ref(false);
// 选择系统类型
const selectSystemType = (type: string) => {
systemOptions.systemType = type as any;
updatePreview();
};
// 生成代码
const generateCode = async () => {
if (!featureName.value.trim()) {
Editor.Dialog.warn('请输入功能名称', {
detail: '请先输入一个有效的功能名称例如Health、Movement、Combat等'
});
return;
}
if (!options.generateComponent && !options.generateSystem) {
Editor.Dialog.warn('请选择生成内容', {
detail: '请至少选择一种要生成的代码类型(组件或系统)'
});
return;
}
isGenerating.value = true;
try {
const projectPath = Editor.Project.path;
const ecsDir = path.join(projectPath, 'assets', 'scripts', 'ecs');
// 检查ECS目录是否存在
const fs = require('fs');
if (!fs.existsSync(ecsDir)) {
Editor.Dialog.warn('ECS目录不存在', {
detail: '请先创建ECS模板后再生成代码。\n\n您可以在欢迎面板中点击"创建ECS模板"来创建基础结构。',
});
return;
}
const codeGenerator = new CodeGenerator();
const generatedFiles: string[] = [];
const baseName = featureName.value.trim();
// 生成组件
if (options.generateComponent) {
const componentDir = path.join(ecsDir, 'components');
await codeGenerator.generateComponent(baseName, componentDir, componentOptions);
generatedFiles.push(`📦 组件: ${baseName}Component.ts`);
}
// 生成系统
if (options.generateSystem) {
const systemDir = path.join(ecsDir, 'systems');
// 如果选择了组件过滤且生成了组件,自动添加组件过滤
const requiredComponents = (systemOptions.filterByComponent && options.generateComponent) ?
[`${baseName}Component`] : [];
const systemOpts = {
...systemOptions,
requiredComponents
};
await codeGenerator.generateSystem(
baseName,
systemDir,
systemOpts
);
generatedFiles.push(`⚙️ 系统: ${baseName}System.ts`);
}
// 成功提示
Editor.Dialog.info('代码生成成功', {
detail: `${baseName} 功能代码已生成完成!\n\n生成的文件\n${generatedFiles.join('\n')}\n\n请刷新资源管理器查看新创建的文件。`
});
// 清空输入
featureName.value = '';
} catch (error) {
console.error('Failed to generate code:', error);
Editor.Dialog.error('代码生成失败', {
detail: `生成代码时发生错误:\n\n${error}`
});
} finally {
isGenerating.value = false;
}
};
// 预览代码
const previewGeneration = () => {
if (!featureName.value.trim()) {
showPreview.value = false;
return;
}
const baseName = featureName.value.trim();
let preview = `将要生成的文件:\n\n`;
if (options.generateComponent) {
preview += `📦 组件: ${baseName}Component.ts\n`;
preview += ` - 位置: assets/scripts/ecs/components/\n`;
preview += ` - 基础组件模板\n\n`;
}
if (options.generateSystem) {
const selectedType = systemTypes.find(t => t.value === systemOptions.systemType);
preview += `⚙️ 系统: ${baseName}System.ts\n`;
preview += ` - 位置: assets/scripts/ecs/systems/\n`;
preview += ` - 类型: ${selectedType?.name || systemOptions.systemType}\n`;
if (systemOptions.filterByComponent && options.generateComponent) {
preview += ` - 过滤组件: ${baseName}Component\n`;
} else if (systemOptions.filterByComponent) {
preview += ` - 组件过滤: 需要手动配置\n`;
} else {
preview += ` - 组件过滤: 无\n`;
}
preview += `\n`;
}
previewCode.value = preview;
showPreview.value = true;
};
// 监听功能名称变化
const updatePreview = () => {
if (featureName.value.trim()) {
previewGeneration();
} else {
showPreview.value = false;
}
};
return {
featureName,
options,
componentOptions,
systemOptions,
systemTypes,
isGenerating,
previewCode,
showPreview,
generateCode,
updatePreview,
selectSystemType
};
},
template: readFileSync(join(__dirname, '../../../static/template/generator/index.html'), 'utf-8')
}));
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
app.mount(this.$.app);
panelDataMap.set(this, app);
}
},
beforeClose() { },
close() {
const app = panelDataMap.get(this);
if (app) {
app.unmount();
panelDataMap.delete(this);
}
},
});

View File

@@ -1,187 +0,0 @@
import { join } from 'path';
// 添加编辑器内的模块搜索路径
module.paths.push(join(Editor.App.path, 'node_modules'));
export function load() {
console.log('ECS Debug Scene Script loaded');
}
export function unload() {
console.log('ECS Debug Scene Script unloaded');
}
export const methods = {
/**
* 获取预览状态
* @returns {object} 预览状态信息
*/
getPreviewState() {
try {
// 检查是否在游戏运行状态
const { director } = require('cc');
if (director && director.getScene && director.getScene()) {
return {
isRunning: true,
engineLoaded: true
};
}
return {
isRunning: false,
engineLoaded: false
};
} catch (error) {
console.warn('Failed to get preview state:', error);
return {
isRunning: false,
engineLoaded: false
};
}
},
/**
* 检查ECS框架是否已加载
* @returns {boolean} ECS框架加载状态
*/
isECSFrameworkLoaded() {
try {
// 检查是否有ECS框架的全局对象
return typeof window !== 'undefined' && !!(window as any).ECSFramework;
} catch (error) {
console.warn('Failed to check ECS framework status:', error);
return false;
}
},
/**
* 获取场景基本信息
* @returns {object} 场景信息
*/
getSceneBasicInfo() {
try {
const { director } = require('cc');
if (director && director.getScene) {
const scene = director.getScene();
return {
sceneName: scene ? (scene.name || '当前场景') : '未知场景',
nodeCount: scene ? this.countNodes(scene) : 0,
isValid: scene ? scene.isValid : false
};
}
return {
sceneName: '未知场景',
nodeCount: 0,
isValid: false
};
} catch (error) {
console.warn('Failed to get scene basic info:', error);
return {
sceneName: '获取失败',
nodeCount: 0,
isValid: false
};
}
},
/**
* 获取ECS框架的调试信息
* @returns {object|null} ECS调试数据或null如果框架未加载
*/
getECSDebugInfo() {
try {
// 检查是否有ECS框架的全局对象
if (typeof window !== 'undefined' && (window as any).ECSFramework) {
const ecs = (window as any).ECSFramework;
// 获取当前场景和实体管理器
if (ecs.Core && ecs.Core.getCurrentScene) {
const scene = ecs.Core.getCurrentScene();
if (scene && scene.entityManager) {
const entityManager = scene.entityManager;
const systemManager = scene.systemManager;
// 收集调试信息
const debugInfo = {
timestamp: new Date().toISOString(),
frameworkLoaded: true,
currentScene: scene.name || '当前场景',
totalEntities: entityManager.entityCount || 0,
activeEntities: entityManager.activeEntityCount || 0,
pendingAdd: 0, // 需要具体API
pendingRemove: 0, // 需要具体API
totalSystems: systemManager ? systemManager.getSystemCount() : 0,
systemsInfo: [],
frameTime: 0, // 需要性能监控
memoryUsage: 0, // 需要内存监控
componentTypes: 0, // 需要组件统计
componentInstances: 0 // 需要组件实例统计
};
// 获取系统信息
if (systemManager && systemManager.getSystems) {
const systems = systemManager.getSystems();
debugInfo.systemsInfo = systems.map((system: any, index: number) => ({
name: system.constructor.name || `System${index}`,
entityCount: system.entities ? system.entities.length : 0,
executionTime: system.lastExecutionTime || 0,
updateOrder: index + 1
}));
}
return debugInfo;
}
}
}
// 检查是否直接导入了ECS模块
try {
// 这里需要根据实际的ECS框架导入方式调整
const { Core } = require('ecs-framework');
if (Core) {
const scene = Core.getCurrentScene();
if (scene) {
return {
timestamp: new Date().toISOString(),
frameworkLoaded: true,
currentScene: scene.name || '当前场景',
totalEntities: scene.entityManager?.entityCount || 0,
activeEntities: scene.entityManager?.activeEntityCount || 0,
pendingAdd: 0,
pendingRemove: 0,
totalSystems: scene.systemManager?.getSystemCount() || 0,
systemsInfo: [],
frameTime: 0,
memoryUsage: 0,
componentTypes: 0,
componentInstances: 0
};
}
}
} catch (error) {
// ECS框架未导入或未初始化
}
return null;
} catch (error) {
console.warn('Failed to get ECS debug info:', error);
return null;
}
},
/**
* 递归计算节点数量
* @param {any} node
* @returns {number}
*/
countNodes(node: any): number {
if (!node) return 0;
let count = 1; // 当前节点
if (node.children) {
for (const child of node.children) {
count += this.countNodes(child);
}
}
return count;
}
};

View File

@@ -1,50 +0,0 @@
@echo off
chcp 65001 >nul
title Cocos ECS Extension - 热更新管理后台
echo.
echo ======================================
echo 🚀 Cocos ECS Extension 热更新管理后台
echo ======================================
echo.
:: 检查Node.js是否安装
node --version >nul 2>&1
if %errorlevel% neq 0 (
echo ❌ 错误: 未检测到Node.js请先安装Node.js
echo 下载地址: https://nodejs.org/
pause
exit /b 1
)
:: 获取Node.js版本
for /f "tokens=1" %%i in ('node --version') do set NODE_VERSION=%%i
echo ✅ Node.js版本: %NODE_VERSION%
:: 检查是否首次运行
if not exist "admin-backend\node_modules" (
echo.
echo 📦 首次运行,正在安装依赖...
cd admin-backend
call npm install
if %errorlevel% neq 0 (
echo ❌ 依赖安装失败
pause
exit /b 1
)
cd ..
echo ✅ 依赖安装完成
)
:: 启动服务
echo.
echo 🚀 启动热更新管理后台...
echo 📍 管理界面地址: http://localhost:3001
echo.
echo 💡 提示: 按 Ctrl+C 可停止服务
echo.
cd admin-backend
call npm run dev
pause

View File

@@ -1,261 +0,0 @@
/* 基础样式和布局 */
/* 全局重置和通用样式 */
* {
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;
position: relative;
}
/* 画布容器 */
.canvas-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: #2d3748;
}
/* 响应式设计 */
@media (max-width: 1400px) {
.properties-panel-container {
width: 280px;
}
.blackboard-sidebar {
width: 260px;
right: 280px;
}
.blackboard-sidebar.collapsed {
right: 232px;
}
}
@media (max-width: 1200px) {
.nodes-panel {
width: 240px;
}
.properties-panel-container {
width: 260px;
}
.blackboard-sidebar {
width: 240px;
right: 260px;
}
.blackboard-sidebar.collapsed {
right: 212px;
}
}
@media (max-width: 900px) {
.editor-container {
flex-direction: column;
}
.nodes-panel {
width: 100%;
height: 160px;
flex: none;
}
.properties-panel-container {
width: 100%;
height: 200px;
flex: none;
}
.blackboard-sidebar {
width: 250px;
max-height: 300px;
top: 380px;
right: 20px;
border-radius: 8px;
}
.blackboard-sidebar.collapsed {
right: 20px;
}
.canvas-container {
flex: 1;
min-height: 300px;
}
}
/* 编辑器容器禁用状态 */
.editor-container.disabled {
position: relative;
pointer-events: none;
opacity: 0.3;
filter: blur(1px);
}
/* 安装遮罩层 */
.install-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(26, 32, 44, 0.95);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
pointer-events: all;
}
.overlay-content {
text-align: center;
padding: 40px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
backdrop-filter: blur(20px);
max-width: 400px;
width: 90%;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.overlay-icon {
font-size: 60px;
margin-bottom: 20px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.overlay-content h3 {
font-size: 24px;
font-weight: 600;
margin-bottom: 12px;
color: #ffffff;
}
.overlay-content p {
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 30px;
line-height: 1.5;
}
.overlay-actions {
margin-bottom: 20px;
}
.overlay-install-btn {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 14px 28px;
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
border: none;
border-radius: 12px;
color: white;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
min-width: 200px;
}
.overlay-install-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(34, 197, 94, 0.4);
}
.overlay-install-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
}
.overlay-install-btn.installing {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
animation: installing-glow 2s infinite;
}
@keyframes installing-glow {
0%, 100% {
box-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
}
50% {
box-shadow: 0 0 20px rgba(59, 130, 246, 0.8);
}
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.overlay-note {
margin-top: 16px;
}
.overlay-note small {
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
}

View File

@@ -1,149 +0,0 @@
/* 画布区域样式 */
.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;
}

View File

@@ -1,359 +0,0 @@
/* 条件拖拽功能样式 */
/* 可拖拽条件节点 */
.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%);
}
}
/* 条件装饰器 - 基础状态 */
.conditional-decorator {
width: 200px;
min-width: 200px;
max-width: 350px; /* 增加最大宽度以容纳长的条件名称 */
min-height: 80px;
transition: all 0.3s ease;
word-wrap: break-word;
}
/* 条件装饰器 - 有附加条件状态 */
.conditional-decorator.has-attached-condition {
width: auto; /* 自动调整宽度 */
min-width: 280px; /* 进一步增加最小宽度,确保较长的条件名称能完整显示 */
max-width: 400px; /* 增加最大宽度 */
min-height: 110px; /* 增加基础高度 */
}
/* 条件装饰器 - 展开状态 */
.conditional-decorator.has-attached-condition .condition-properties {
min-height: 30px; /* 为展开内容预留空间 */
}
/* 条件装饰器接受状态 */
.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: 6px 8px;
border: 2px dashed #4a5568;
border-radius: 4px;
color: #a0aec0;
font-size: 10px;
transition: all 0.3s ease;
min-height: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.tree-node.can-accept-condition .condition-placeholder {
border-color: #ffd700;
color: #ffd700;
background: rgba(255, 215, 0, 0.05);
animation: pulse-placeholder 2s infinite;
}
@keyframes pulse-placeholder {
0%, 100% {
background: rgba(255, 215, 0, 0.05);
}
50% {
background: rgba(255, 215, 0, 0.15);
}
}
.placeholder-text {
font-size: 10px;
font-weight: 500;
}
/* 附加的条件 */
.attached-condition {
background: rgba(255, 215, 0, 0.1);
border: 1px solid #ffd700;
border-radius: 4px;
padding: 6px;
position: relative;
width: 100%;
box-sizing: border-box;
min-height: 36px;
}
/* 条件信息区域 */
.condition-info {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
cursor: pointer;
padding: 4px 6px 4px 6px;
padding-right: 40px; /* 为右侧两个按钮预留空间 */
border-radius: 3px;
transition: all 0.2s ease;
line-height: 1.3;
width: 100%;
box-sizing: border-box;
}
.condition-info:hover {
background: rgba(255, 215, 0, 0.2);
transform: scale(1.01);
}
.condition-info.condition-selected {
background: rgba(255, 215, 0, 0.3);
border: 1px solid #ffd700;
box-shadow: 0 0 0 1px rgba(255, 215, 0, 0.25);
}
.condition-icon {
font-size: 11px;
flex-shrink: 0;
}
.condition-text {
flex: 1;
color: #ffd700;
font-weight: 500;
font-size: 11px;
word-wrap: break-word;
word-break: break-word;
line-height: 1.3;
min-width: 0;
max-width: calc(100% - 50px); /* 为图标、编辑提示和按钮预留空间 */
white-space: nowrap; /* 尽量保持一行显示 */
overflow: hidden;
text-overflow: ellipsis; /* 如果实在太长则显示省略号 */
}
.edit-hint {
font-size: 9px;
color: #a0aec0;
margin-left: auto;
transition: color 0.2s ease;
flex-shrink: 0;
}
.condition-info:hover .edit-hint {
color: #ffd700;
}
/* 展开/收缩按钮 */
.toggle-condition-btn {
position: absolute;
top: 4px;
right: 20px;
width: 14px;
height: 14px;
border: none;
background: #4a5568;
color: #a0aec0;
cursor: pointer;
border-radius: 2px;
font-size: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
line-height: 1;
z-index: 2;
}
.toggle-condition-btn:hover {
background: #2d3748;
color: #ffd700;
transform: scale(1.1);
}
/* 移除条件按钮 */
.remove-condition-btn {
position: absolute;
top: -2px;
right: -2px;
background: #e53e3e;
border: none;
color: white;
cursor: pointer;
font-size: 10px;
width: 16px;
height: 16px;
border-radius: 50%;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
opacity: 0.8;
z-index: 3;
}
.remove-condition-btn:hover {
background: #c53030;
transform: scale(1.1);
opacity: 1;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* 条件属性展开区域 */
.condition-properties {
margin-top: 6px;
padding-top: 6px;
width: 100%;
box-sizing: border-box;
padding-right: 6px; /* 为按钮预留一点空间 */
}
/* 属性分隔线 */
.properties-divider {
width: calc(100% - 6px);
height: 1px;
background: linear-gradient(90deg, transparent 0%, #ffd700 20%, #ffd700 80%, transparent 100%);
margin-bottom: 6px;
opacity: 0.6;
}
/* 条件属性项 */
.condition-property-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 2px 4px;
margin-bottom: 2px;
font-size: 10px;
line-height: 1.2;
width: 100%;
box-sizing: border-box;
}
.condition-property-label {
color: #a0aec0;
font-weight: 500;
flex-shrink: 0;
margin-right: 8px;
white-space: nowrap;
}
.condition-property-value {
color: #ffd700;
font-weight: 400;
text-align: right;
word-wrap: break-word;
word-break: break-word;
min-width: 0;
flex: 1;
white-space: normal; /* 允许值换行 */
}
/* 画布状态 */
.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 {
/* 基础高度和宽度 */
min-height: 80px;
width: 200px; /* 增加基础宽度 */
transition: all 0.3s ease;
}
/* 附加了条件的装饰器节点需要更大的高度 */
.tree-node.node-conditional-decorator.has-attached-condition {
min-height: 130px; /* 增加高度 */
width: 220px; /* 进一步增加宽度以容纳更多内容 */
}
.tree-node.node-conditional-decorator .condition-attachment-area {
border: 1px solid #9f7aea;
background: rgba(159, 122, 234, 0.05);
margin-top: 4px;
min-height: 20px;
transition: all 0.3s ease;
}
/* 当有条件附加时,增加条件区域的高度 */
.tree-node.node-conditional-decorator.has-attached-condition .condition-attachment-area {
min-height: 45px;
padding: 10px 12px;
background: rgba(255, 215, 0, 0.08);
border-color: #ffd700;
}
.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;
}

View File

@@ -1,610 +0,0 @@
/* 模态框和状态栏样式 */
/* 状态栏 */
.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.75);
backdrop-filter: blur(4px);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
animation: modalFadeIn 0.2s ease-out;
}
@keyframes modalFadeIn {
from {
opacity: 0;
backdrop-filter: blur(0px);
}
to {
opacity: 1;
backdrop-filter: blur(4px);
}
}
.modal-content {
background: #2d3748;
border-radius: 12px;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6);
max-width: 95vw;
max-height: 90vh;
min-width: 800px;
width: auto;
display: flex;
flex-direction: column;
border: 1px solid #4a5568;
animation: modalSlideIn 0.3s ease-out;
overflow: hidden;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #4a5568;
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
position: relative;
}
.modal-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
}
.modal-header h3 {
margin: 0;
color: #ffffff;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 8px;
}
.modal-header h3::before {
content: '✨';
font-size: 14px;
}
.modal-header button {
background: rgba(255, 255, 255, 0.1);
border: none;
color: #cbd5e0;
cursor: pointer;
font-size: 16px;
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.modal-header button:hover {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
transform: scale(1.05);
}
.modal-body {
flex: 1;
overflow-y: auto;
padding: 24px;
background: #2d3748;
min-width: 750px;
}
/* 导出选项样式增强 */
.export-options {
margin-bottom: 16px;
padding: 16px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid #4a5568;
border-radius: 8px;
}
.export-options label {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 12px;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid #4a5568;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
font-weight: 500;
position: relative;
}
.export-options label:hover {
background: rgba(255, 255, 255, 0.05);
border-color: #667eea;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
}
.export-options label:has(input[type="radio"]:checked) {
background: rgba(102, 126, 234, 0.1);
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.export-options input[type="radio"] {
appearance: none;
width: 18px;
height: 18px;
border: 2px solid #4a5568;
border-radius: 50%;
background: #1a202c;
cursor: pointer;
position: relative;
transition: all 0.2s ease;
flex-shrink: 0;
margin-top: 2px;
}
.export-options input[type="radio"]:checked {
border-color: #667eea;
background: #667eea;
}
.export-options input[type="radio"]:checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
background: white;
border-radius: 50%;
}
.export-options label span {
color: #e2e8f0;
font-weight: 600;
flex: 1;
}
.export-options label small {
display: block;
margin-top: 4px;
color: #a0aec0;
font-size: 11px;
font-weight: 400;
line-height: 1.3;
}
/* 禁用选项样式 */
.export-options label.disabled-option {
opacity: 0.5;
cursor: not-allowed;
background: rgba(255, 255, 255, 0.01);
border-color: #374151;
}
.export-options label.disabled-option:hover {
background: rgba(255, 255, 255, 0.01);
border-color: #374151;
transform: none;
box-shadow: none;
}
.export-options label.disabled-option span {
color: #6b7280;
}
.export-options label.disabled-option input[type="radio"] {
border-color: #374151;
cursor: not-allowed;
}
.export-options label.disabled-option input[type="radio"]:disabled {
background: #1f2937;
}
.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: 20px 24px;
border-top: 1px solid #4a5568;
justify-content: flex-end;
background: #374151;
}
.modal-footer button {
padding: 12px 20px;
border: none;
border-radius: 8px;
color: white;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: all 0.2s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
position: relative;
overflow: hidden;
}
.modal-footer button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s ease;
}
.modal-footer button:hover::before {
left: 100%;
}
.modal-footer button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
}
.modal-footer button:active {
transform: translateY(0);
}
.modal-footer .copy-btn {
background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%);
border: 1px solid #3182ce;
}
.modal-footer .copy-btn:hover {
background: linear-gradient(135deg, #2c5282 0%, #2a4365 100%);
border-color: #2c5282;
}
.modal-footer .save-file-btn {
background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
border: 1px solid #38a169;
}
.modal-footer .save-file-btn:hover {
background: linear-gradient(135deg, #2f855a 0%, #276749 100%);
border-color: #2f855a;
}
.modal-footer .close-btn {
background: linear-gradient(135deg, #718096 0%, #4a5568 100%);
border: 1px solid #718096;
}
.modal-footer .close-btn:hover {
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
border-color: #4a5568;
}
/* Blackboard模态框特定样式 */
.blackboard-modal {
width: 520px;
max-width: 90vw;
}
.form-group {
margin-bottom: 20px;
position: relative;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #e2e8f0;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 6px;
}
.form-group label::before {
content: '';
width: 3px;
height: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: 12px 16px;
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
border: 2px solid #4a5568;
border-radius: 8px;
color: #e2e8f0;
font-size: 13px;
font-family: inherit;
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,
.form-select:focus,
.form-textarea:focus {
border-color: #667eea;
outline: none;
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: #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,
.form-textarea::placeholder {
color: #718096;
font-style: italic;
}
.constraint-inputs {
display: flex;
flex-direction: column;
gap: 12px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid #4a5568;
border-radius: 8px;
padding: 16px;
margin-top: 8px;
}
.constraint-row {
display: flex;
align-items: center;
gap: 12px;
}
.constraint-row label {
min-width: 60px;
font-size: 12px;
color: #a0aec0;
margin: 0;
text-transform: none;
font-weight: 500;
}
.constraint-row label::before {
display: none;
}
/* 自定义复选框样式 */
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
margin: 16px 0;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.02);
border-radius: 8px;
border: 1px solid #4a5568;
cursor: pointer;
transition: all 0.2s ease;
}
.checkbox-group:hover {
background: rgba(255, 255, 255, 0.05);
border-color: #667eea;
}
.checkbox-group input[type="checkbox"] {
appearance: none;
width: 18px;
height: 18px;
border: 2px solid #4a5568;
border-radius: 4px;
background: #1a202c;
cursor: pointer;
position: relative;
transition: all 0.2s ease;
}
.checkbox-group input[type="checkbox"]:checked {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
}
.checkbox-group input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 12px;
font-weight: bold;
}
.checkbox-group label {
margin: 0;
color: #e2e8f0;
font-size: 13px;
font-weight: 500;
cursor: pointer;
text-transform: none;
display: block;
}
.checkbox-group label::before {
display: none;
}
.constraint-row label {
min-width: 60px;
margin-bottom: 0;
font-size: 11px;
}
.constraint-row input {
flex: 1;
}
.allowed-values textarea {
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 {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: auto;
}
.export-code {
background: #1a202c;
border: 1px solid #4a5568;
border-radius: 4px;
padding: 12px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
color: #e2e8f0;
resize: none;
min-height: 400px;
width: 100%;
box-sizing: border-box;
line-height: 1.4;
overflow-y: auto;
white-space: pre;
word-wrap: break-word;
}

View File

@@ -1,393 +0,0 @@
/* 节点样式 */
.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-body需要更大的宽度 */
.tree-node.node-conditional-decorator .node-body {
max-width: 176px; /* 基础状态 */
}
.tree-node.node-conditional-decorator.has-attached-condition .node-body {
max-width: 196px; /* 附加条件后更宽 */
}
.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);
}
}
/* 节点类型提示 */
.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;
}

View File

@@ -1,299 +0,0 @@
/* 头部工具栏样式 */
.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: 16px;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
letter-spacing: 0.5px;
}
.current-file {
color: #a0aec0;
font-weight: 400;
font-size: 13px;
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:not(:disabled) {
background: rgba(255,255,255,0.2);
transform: translateY(-1px);
}
.tool-btn.active {
background: rgba(102, 126, 234, 0.3);
border-color: #667eea;
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 {
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);
}
}
/* 安装状态容器 */
.install-status-container {
position: relative;
}
.install-status {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 12px;
min-width: 280px;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.install-status:hover {
background: rgba(255,255,255,0.15);
border-color: rgba(255,255,255,0.3);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
/* 状态信息 */
.status-info {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.status-icon {
font-size: 18px;
line-height: 1;
}
.status-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.main-text {
font-size: 13px;
font-weight: 500;
color: white;
}
.version-text {
font-size: 11px;
color: rgba(255,255,255,0.7);
}
/* 安装按钮 */
.install-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
border: none;
border-radius: 8px;
color: white;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.install-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4);
}
.install-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
}
.install-btn.installing {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
}
.btn-icon {
font-size: 14px;
line-height: 1;
}
.btn-text {
font-size: 12px;
}
/* 加载点动画 */
.loading-dots {
display: flex;
gap: 2px;
margin-left: 4px;
}
.loading-dots span {
width: 3px;
height: 3px;
background: white;
border-radius: 50%;
animation: loading-dots 1.4s infinite ease-in-out;
}
.loading-dots span:nth-child(1) { animation-delay: -0.32s; }
.loading-dots span:nth-child(2) { animation-delay: -0.16s; }
@keyframes loading-dots {
0%, 80%, 100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
/* 刷新按钮 */
.refresh-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
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;
}
.refresh-btn:hover {
background: rgba(255,255,255,0.2);
transform: rotate(180deg);
}
/* 状态颜色 */
.install-status.installed {
border-color: rgba(34, 197, 94, 0.5);
background: rgba(34, 197, 94, 0.1);
}
.install-status.not-installed {
border-color: rgba(239, 68, 68, 0.5);
background: rgba(239, 68, 68, 0.1);
}
.install-status.installing {
border-color: rgba(59, 130, 246, 0.5);
background: rgba(59, 130, 246, 0.1);
animation: installing-pulse 2s infinite;
}
@keyframes installing-pulse {
0%, 100% {
box-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
}
50% {
box-shadow: 0 0 15px rgba(59, 130, 246, 0.6);
}
}

View File

@@ -1,483 +0,0 @@
#text {
color: var(--color-normal-contrast-weakest);
margin: auto;
width: 180px;
}
.counter {
text-align: center;
}
.ecs-welcome-panel {
height: 100%;
background: var(--color-panel-bg);
color: var(--color-text-normal);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
.welcome-container {
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
max-height: 100vh;
box-sizing: border-box;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
text-align: center;
flex-shrink: 0;
}
.header h1 {
margin: 0 0 6px 0;
font-size: 20px;
font-weight: 600;
}
.header p {
margin: 0;
opacity: 0.9;
font-size: 13px;
}
.status-section {
background: var(--color-area-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 14px;
margin-bottom: 14px;
flex-shrink: 0;
}
.status-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 10px;
color: var(--color-text-normal);
}
.status-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid var(--color-border-soft);
flex-wrap: wrap;
gap: 8px;
}
.status-item:last-child {
border-bottom: none;
}
.status-label {
color: var(--color-text-contrast-weakest);
flex: 1;
min-width: 200px;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.status-badge.installed {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-badge.not-installed {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status-badge.checking {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.version-info {
font-family: 'Courier New', monospace;
font-size: 11px;
color: var(--color-text-contrast-weak);
}
.update-hint {
color: #ff6b35;
font-size: 10px;
font-weight: 600;
margin-left: 8px;
display: block;
margin-top: 2px;
}
.management-buttons {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}
.management-buttons ui-button {
width: 100%;
min-height: 32px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 8px 12px;
}
.management-buttons ui-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.management-buttons ui-button.green {
background-color: #28a745;
color: white;
border: 1px solid #28a745;
}
.management-buttons ui-button.red {
background-color: #dc3545;
color: white;
border: 1px solid #dc3545;
}
.management-buttons ui-button.blue {
background-color: #007bff;
color: white;
border: 1px solid #007bff;
}
.operation-status {
padding: 12px;
border-radius: 6px;
margin-top: 8px;
}
.operation-status.loading {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
.operation-status.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.operation-status.error,
.operation-status.failed {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.operation-status.warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
.status-content {
display: flex;
align-items: center;
gap: 8px;
}
.status-icon {
font-size: 16px;
}
.status-text {
flex: 1;
font-weight: 500;
}
.status-details {
margin-top: 8px;
font-size: 11px;
opacity: 0.8;
white-space: pre-line;
}
.actions-section {
background: var(--color-area-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 14px;
margin-bottom: 14px;
flex-shrink: 0;
}
.action-card {
background: var(--color-neutral-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 12px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.2s ease;
}
.action-card:last-child {
margin-bottom: 0;
}
.action-card h3 {
margin: 0 0 6px 0;
font-size: 14px;
font-weight: 600;
color: var(--color-text-normal);
display: flex;
align-items: center;
gap: 8px;
}
.action-card p {
margin: 0;
font-size: 12px;
color: var(--color-text-contrast-weakest);
line-height: 1.4;
}
.footer {
background: var(--color-area-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 12px;
margin-top: 8px;
text-align: center;
font-size: 11px;
color: var(--color-text-contrast-weak);
flex-shrink: 0;
}
.footer a {
color: var(--color-primary);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.last-check {
margin-top: 4px;
font-size: 10px;
opacity: 0.7;
}
.loading-spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid var(--color-border);
border-top: 2px solid var(--color-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 6px;
vertical-align: middle;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.icon {
margin-right: 8px;
font-size: 16px;
}
/* 优化滚动条样式 */
.welcome-container::-webkit-scrollbar {
width: 8px;
}
.welcome-container::-webkit-scrollbar-track {
background: var(--color-area-bg);
border-radius: 4px;
}
.welcome-container::-webkit-scrollbar-thumb {
background: var(--color-border-emphasis);
border-radius: 4px;
opacity: 0.5;
}
.welcome-container::-webkit-scrollbar-thumb:hover {
background: var(--color-border-emphasis-strong);
opacity: 0.8;
}
/* 状态切换动画 */
.status-badge {
transition: all 0.3s ease;
}
.action-card {
transform: translateY(0);
transition: all 0.2s ease;
}
.action-card:hover:not(.disabled) {
background: var(--color-area-bg-hover);
border-color: var(--color-primary-highlight);
transform: translateY(-1px);
}
.action-card.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-card.disabled:hover {
background: var(--color-neutral-bg);
border-color: var(--color-border);
transform: none;
}
/* action-card样式 */
.action-card.special {
background: linear-gradient(135deg, var(--color-primary-fill) 0%, var(--color-focus-fill) 100%);
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.action-card.special h3 {
color: var(--color-primary-contrast);
font-weight: 700;
}
.action-card.special p {
color: var(--color-primary-contrast);
opacity: 0.9;
}
.action-card.special:hover {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-focus-border) 100%);
border-color: var(--color-focus-border);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.action-card.special .icon {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
/* 响应式布局 */
@media (max-width: 600px) {
.welcome-container {
padding: 16px;
}
.header h1 {
font-size: 20px;
}
.status-item {
flex-direction: column;
align-items: flex-start;
}
.status-label {
min-width: auto;
margin-bottom: 4px;
}
}
.management-buttons ui-button {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 现有文件信息样式 */
.existing-files-info {
margin-top: 12px;
padding: 10px;
background: var(--color-area-bg);
border: 1px solid var(--color-border);
border-radius: 4px;
font-size: 11px;
}
.files-summary {
font-weight: 600;
color: var(--color-text-normal);
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid var(--color-border);
}
.files-list,
.files-overflow {
max-height: 120px;
overflow-y: auto;
}
.file-item {
padding: 2px 0;
color: var(--color-text-contrast-weak);
font-family: var(--font-mono);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.more-files {
padding: 4px 0;
color: var(--color-text-contrast-weakest);
font-style: italic;
border-top: 1px solid var(--color-border);
margin-top: 4px;
padding-top: 6px;
}
/* 文件列表滚动条样式 */
.files-list::-webkit-scrollbar,
.files-overflow::-webkit-scrollbar {
width: 6px;
}
.files-list::-webkit-scrollbar-track,
.files-overflow::-webkit-scrollbar-track {
background: var(--color-area-bg);
border-radius: 3px;
}
.files-list::-webkit-scrollbar-thumb,
.files-overflow::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
}
.files-list::-webkit-scrollbar-thumb:hover,
.files-overflow::-webkit-scrollbar-thumb:hover {
background: var(--color-border-emphasis);
}

View File

@@ -1,378 +0,0 @@
/* ECS代码生成器样式 */
.ecs-generator {
height: 100%;
background: var(--color-panel-bg);
color: var(--color-text-normal);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
.generator-container {
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
max-height: 100vh;
box-sizing: border-box;
}
/* 头部样式 */
.generator-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
text-align: center;
flex-shrink: 0;
}
.generator-header h1 {
margin: 0 0 6px 0;
font-size: 20px;
font-weight: 600;
}
.generator-header p {
margin: 0;
opacity: 0.9;
font-size: 13px;
}
/* 表单区域 */
.generator-form {
background: var(--color-area-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
flex-shrink: 0;
}
.form-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 16px;
color: var(--color-text-normal);
}
/* 输入字段 */
.input-group {
margin-bottom: 20px;
}
.input-label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--color-text-normal);
margin-bottom: 8px;
}
.feature-input {
width: 100%;
padding: 8px 12px;
font-size: 14px;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-panel-bg);
color: var(--color-text-normal);
box-sizing: border-box;
transition: border-color 0.2s ease;
}
.feature-input:focus {
outline: none;
border-color: #667eea;
}
.input-hint {
margin-top: 6px;
font-size: 12px;
color: var(--color-text-contrast-weakest);
font-style: italic;
}
/* 选项组 */
.options-group {
margin-bottom: 20px;
}
.option-item {
display: flex;
align-items: center;
padding: 8px 0;
margin-bottom: 8px;
}
.option-item:last-child {
margin-bottom: 0;
}
/* 复选框样式 */
.checkbox-wrapper {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.checkbox-wrapper input[type="checkbox"] {
margin-right: 8px;
width: 16px;
height: 16px;
cursor: pointer;
}
.option-title {
font-size: 14px;
font-weight: 500;
color: var(--color-text-normal);
}
.option-description {
margin-left: 24px;
margin-top: 4px;
font-size: 12px;
color: var(--color-text-contrast-weakest);
}
/* 系统配置区域 */
.system-config {
margin-top: 16px;
padding: 16px;
background: var(--color-panel-bg);
border: 1px solid var(--color-border-soft);
border-radius: 6px;
}
.config-section {
margin-bottom: 20px;
}
.config-section:last-child {
margin-bottom: 0;
}
.config-label {
display: block;
font-size: 14px;
font-weight: 600;
color: var(--color-text-normal);
margin-bottom: 12px;
}
.filter-option {
margin-bottom: 8px;
}
/* 系统类型卡片 */
.system-type-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
}
.system-card {
padding: 16px;
border: 2px solid var(--color-border);
border-radius: 8px;
background: var(--color-area-bg);
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.system-card:hover {
border-color: #667eea;
background: var(--color-panel-bg);
}
.system-card.active {
border-color: #667eea;
background: var(--color-panel-bg);
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
}
.system-card.active::after {
content: '✓';
position: absolute;
top: 8px;
right: 12px;
color: #667eea;
font-weight: bold;
font-size: 16px;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.card-icon {
font-size: 20px;
margin-right: 8px;
}
.card-title {
font-size: 15px;
font-weight: 600;
color: var(--color-text-normal);
}
.card-description {
font-size: 13px;
color: var(--color-text-contrast-weak);
margin-bottom: 6px;
line-height: 1.4;
}
.card-usage {
font-size: 12px;
color: var(--color-text-contrast-weakest);
font-style: italic;
line-height: 1.3;
}
/* 系统类型选择 */
.system-type-group {
margin-left: 24px;
margin-top: 8px;
}
.system-type-select {
padding: 6px 8px;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-panel-bg);
color: var(--color-text-normal);
font-size: 13px;
cursor: pointer;
}
/* 预览区域 */
.preview-section {
background: var(--color-area-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
flex: 1;
min-height: 200px;
display: flex;
flex-direction: column;
}
.preview-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 12px;
color: var(--color-text-normal);
}
.preview-content {
flex: 1;
background: var(--color-panel-bg);
border: 1px solid var(--color-border-soft);
border-radius: 4px;
padding: 12px;
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--color-text-contrast-weak);
overflow-y: auto;
white-space: pre-wrap;
}
/* 操作按钮区域 */
.action-section {
background: var(--color-area-bg);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 16px;
flex-shrink: 0;
}
.generate-btn {
width: 100%;
padding: 12px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.generate-btn:hover:not(:disabled) {
opacity: 0.9;
}
.generate-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-icon {
font-size: 16px;
}
.btn-icon.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 滚动条样式 */
.generator-container::-webkit-scrollbar,
.preview-content::-webkit-scrollbar {
width: 8px;
}
.generator-container::-webkit-scrollbar-track,
.preview-content::-webkit-scrollbar-track {
background: var(--color-area-bg);
}
.generator-container::-webkit-scrollbar-thumb,
.preview-content::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
}
.generator-container::-webkit-scrollbar-thumb:hover,
.preview-content::-webkit-scrollbar-thumb:hover {
background: var(--color-border-emphasis);
}
/* 响应式设计 */
@media (max-width: 600px) {
.generator-container {
padding: 12px;
}
.generator-header h1 {
font-size: 18px;
}
.feature-input {
font-size: 16px;
}
}

View File

@@ -1,903 +0,0 @@
<!-- 头部工具栏 -->
<div class="header-toolbar">
<div class="toolbar-left">
<h2>🌳 行为树可视化编辑器
<span v-if="currentFileName" class="current-file">- {{ currentFileName }}</span>
<span v-if="hasUnsavedChanges" class="unsaved-indicator"></span>
</h2>
<div class="toolbar-buttons">
<button class="tool-btn" @click="newBehaviorTree" title="新建行为树">
<span>📄</span> 新建
</button>
<button class="tool-btn" :class="{ 'has-changes': hasUnsavedChanges }" @click="saveBehaviorTree" :title="currentFilePath ? '保存到: ' + currentFilePath : (currentFileName ? '另存为 ' + currentFileName : '保存行为树')">
<span>💾</span> 保存{{ hasUnsavedChanges ? ' *' : '' }}
</button>
<button class="tool-btn" @click="saveAsBehaviorTree" title="另存为新文件">
<span>💾</span> 另存为
</button>
<button class="tool-btn" @click="loadBehaviorTree" title="加载行为树">
<span>📂</span> 加载
</button>
<button class="tool-btn" @click="exportConfig" title="导出配置">
<span></span> 导出配置
</button>
</div>
</div>
<div class="toolbar-right">
<div class="install-status-container">
<div class="install-status" :class="installStatusClass()">
<div class="status-info">
<span class="status-icon">{{ isInstalled ? '✅' : (isInstalling ? '⏳' : '❌') }}</span>
<div class="status-text">
<span class="main-text">{{ installStatusText() }}</span>
<span v-if="version && isInstalled" class="version-text">版本 {{ version }}</span>
</div>
</div>
<button
v-if="!isInstalled"
@click="handleInstall"
:disabled="isInstalling"
class="install-btn"
:class="{ 'installing': isInstalling }"
>
<span class="btn-icon">{{ isInstalling ? '⏳' : '📦' }}</span>
<span class="btn-text">{{ isInstalling ? '安装中...' : '立即安装' }}</span>
<div v-if="isInstalling" class="loading-dots">
<span></span><span></span><span></span>
</div>
</button>
<button
v-if="isInstalled && !checkingStatus"
@click="checkInstallStatus"
class="refresh-btn"
title="检查更新"
>
🔄
</button>
</div>
</div>
</div>
</div>
<div class="editor-container" :class="{ 'disabled': !isInstalled }">
<!-- 未安装遮罩 -->
<div v-if="!isInstalled" class="install-overlay">
<div class="overlay-content">
<div class="overlay-icon">🤖</div>
<h3>需要安装AI系统</h3>
<p>行为树编辑器需要安装AI系统才能正常使用</p>
<div class="overlay-actions">
<button
@click="handleInstall"
:disabled="isInstalling"
class="overlay-install-btn"
:class="{ 'installing': isInstalling }"
>
<span>{{ isInstalling ? '⏳ 安装中...' : '📦 立即安装AI系统' }}</span>
<div v-if="isInstalling" class="loading-spinner"></div>
</button>
</div>
<div class="overlay-note">
<small>📝 安装完成后编辑器将自动可用</small>
</div>
</div>
</div>
<!-- 左侧节点面板 -->
<div class="nodes-panel">
<div class="panel-header">
<h3>📦 节点库</h3>
<input
type="text"
v-model="nodeSearchText"
placeholder="搜索节点..."
class="search-input"
>
</div>
<div class="node-categories">
<!-- 根节点 -->
<div class="category">
<h4 class="category-title">🌳 根节点</h4>
<div class="node-list">
<div
v-for="node in filteredRootNodes()"
:key="node.type"
class="node-item root"
:draggable="true"
@dragstart="onNodeDragStart($event, node)"
:title="node.description"
>
<span class="node-icon">{{ node.icon }}</span>
<span class="node-name">{{ node.name }}</span>
</div>
</div>
</div>
<!-- 复合节点 -->
<div class="category">
<h4 class="category-title">🔗 复合节点</h4>
<div class="node-list">
<div
v-for="node in filteredCompositeNodes()"
:key="node.type"
class="node-item composite"
:draggable="true"
@dragstart="onNodeDragStart($event, node)"
:title="node.description"
>
<span class="node-icon">{{ node.icon }}</span>
<span class="node-name">{{ node.name }}</span>
</div>
</div>
</div>
<!-- 装饰器节点 -->
<div class="category">
<h4 class="category-title">🎭 装饰器</h4>
<div class="node-list">
<div
v-for="node in filteredDecoratorNodes()"
:key="node.type"
class="node-item decorator"
:draggable="true"
@dragstart="onNodeDragStart($event, node)"
:title="node.description"
>
<span class="node-icon">{{ node.icon }}</span>
<span class="node-name">{{ node.name }}</span>
</div>
</div>
</div>
<!-- 动作节点 -->
<div class="category">
<h4 class="category-title">⚡ 动作节点</h4>
<div class="node-list">
<div
v-for="node in filteredActionNodes()"
:key="node.type"
class="node-item action"
:draggable="true"
@dragstart="onNodeDragStart($event, node)"
:title="node.description"
>
<span class="node-icon">{{ node.icon }}</span>
<span class="node-name">{{ node.name }}</span>
</div>
</div>
</div>
<!-- 条件节点 -->
<div class="category">
<h4 class="category-title">❓ 条件节点</h4>
<div class="node-list">
<div
v-for="node in filteredConditionNodes()"
:key="node.type"
class="node-item condition"
:class="{ 'draggable-condition': node.isDraggableCondition }"
:draggable="true"
@dragstart="handleConditionNodeDragStart($event, node)"
:title="node.description + (node.isDraggableCondition ? ' (可拖拽到条件装饰器)' : '')"
>
<span class="node-icon">{{ node.icon }}</span>
<span class="node-name">{{ node.name }}</span>
<span v-if="node.isDraggableCondition" class="drag-hint">🎯</span>
</div>
</div>
</div>
<!-- ECS节点 -->
<div class="category">
<h4 class="category-title">🎮 ECS节点</h4>
<div class="node-list">
<div
v-for="node in filteredECSNodes()"
:key="node.type"
class="node-item ecs"
:draggable="true"
@dragstart="onNodeDragStart($event, node)"
:title="node.description"
>
<span class="node-icon">{{ node.icon }}</span>
<span class="node-name">{{ node.name }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 中间画布区域 -->
<div class="canvas-container">
<div class="canvas-toolbar">
<div class="zoom-controls">
<button @click="zoomIn">🔍+</button>
<span>{{ Math.round(zoomLevel * 100) }}%</span>
<button @click="zoomOut">🔍-</button>
<button @click="resetZoom">重置</button>
</div>
<div class="canvas-actions">
<button @click="centerView">居中</button>
<button @click="autoLayout" title="紧凑子树布局 - 子节点紧贴父节点">自动布局</button>
<button @click="validateTree">验证</button>
<button @click="clearAllConnections" title="清除所有连接线" v-if="connections.length > 0">清除连线</button>
</div>
</div>
<div
ref="canvasAreaRef"
class="canvas-area"
:class="{
'connecting': dragState.isConnecting,
'condition-dragging': conditionDragState.isDraggingCondition
}"
@drop="handleCanvasDrop"
@dragover="onCanvasDragOver"
@wheel="onCanvasWheel"
@mousedown="onCanvasMouseDown"
@mousemove="onCanvasMouseMove"
@mouseup="onCanvasMouseUp"
>
<canvas
:width="canvasWidth"
:height="canvasHeight"
class="behavior-tree-canvas"
></canvas>
<!-- 连接线绘制层 -->
<svg
ref="svgRef"
class="connection-layer"
:width="canvasWidth"
:height="canvasHeight"
>
<g :transform="'translate(' + panX + ', ' + panY + ') scale(' + zoomLevel + ')'">
<path
v-for="connection in connections"
:key="connection.id"
:d="connection.path"
class="connection-line"
:class="{ 'connection-active': connection.active }"
@click="onConnectionClick($event, connection.id)"
:title="'点击删除连接线 (' + connection.sourceId + ' → ' + connection.targetId + ')'"
/>
<!-- 临时连接线 -->
<path
v-if="connectionState.tempPath"
:d="connectionState.tempPath"
class="connection-temp"
/>
</g>
</svg>
<!-- 节点渲染层 -->
<div
class="nodes-layer"
:style="{ transform: 'translate(' + panX + 'px, ' + panY + 'px) scale(' + zoomLevel + ')' }"
>
<div
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,
{
'node-selected': selectedNodeId === node.id,
'node-error': node.hasError,
'dragging': dragState.dragNode && dragState.dragNode.id === node.id,
'condition-hover': conditionDragState.hoveredDecoratorId === node.id,
'can-accept-condition': canAcceptCondition(node) && conditionDragState.isDraggingCondition,
'has-attached-condition': node.type === 'conditional-decorator' && node.attachedCondition
}
]"
:style="{
left: node.x + 'px',
top: node.y + 'px'
}"
@click="selectNode(node.id)"
@mousedown="startNodeDrag($event, node)"
@drop="handleNodeDrop($event, node)"
@dragover="handleNodeDragOver($event, node)"
@dragleave="handleNodeDragLeave($event, node)"
>
<div class="node-header">
<span class="node-icon">{{ node.icon }}</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">
<div v-if="node.description" class="node-description">{{ node.description }}</div>
<!-- 条件装饰器的条件显示 -->
<div v-if="node.type === 'conditional-decorator'" class="condition-attachment-area">
<div v-if="node.attachedCondition" class="attached-condition">
<div
class="condition-info"
:class="{ 'condition-selected': selectedConditionNodeId === node.id }"
@click.stop="selectConditionNode(node)"
title="点击编辑条件属性"
>
<span class="condition-icon">{{ node.attachedCondition.icon }}</span>
<span class="condition-text">{{ getConditionDisplayText(node) }}</span>
<span class="edit-hint">📝</span>
</div>
<!-- 展开时显示条件属性 -->
<div v-if="node.conditionExpanded" class="condition-properties">
<div class="properties-divider"></div>
<div
v-for="(value, key) in getConditionProperties(node)"
:key="key"
class="condition-property-item"
>
<span class="condition-property-label">{{ key }}:</span>
<span class="condition-property-value">{{ value }}</span>
</div>
</div>
<button
class="toggle-condition-btn"
@click.stop="toggleConditionExpanded(node)"
:title="node.conditionExpanded ? '收缩详情' : '展开详情'"
>{{ node.conditionExpanded ? '▲' : '▼' }}</button>
<button
class="remove-condition-btn"
@click.stop="removeConditionFromDecorator(node)"
title="移除条件"
>×</button>
</div>
<div v-else class="condition-placeholder">
<span class="placeholder-text">🎯 拖拽条件到此处</span>
</div>
</div>
<!-- 节点属性预览 -->
<div v-if="hasVisibleProperties(node)" class="node-properties-preview">
<div
v-for="(prop, key) in getVisibleProperties(node)"
:key="key"
class="property-preview-item"
:title="prop.name + ': ' + prop.description"
>
<span class="property-label">{{ prop.name }}:</span>
<span class="property-value" :class="'property-' + prop.type">
{{ formatPropertyValue(prop) }}
</span>
</div>
</div>
</div>
<!-- 输入端口 -->
<div
v-if="node.canHaveParent"
class="port port-input"
:class="{
'connecting': connectionState.isConnecting &&
connectionState.startPortType === 'output' &&
connectionState.startNodeId !== node.id,
'drag-target': isValidConnectionTarget(node.id, 'input')
}"
@mousedown.stop="startConnection($event, node.id, 'input')"
@mouseenter="onPortHover(node.id, 'input')"
@mouseleave="onPortLeave()"
title="执行流入口"
>
<div class="port-inner"></div>
</div>
<!-- 输出端口 -->
<div
v-if="node.canHaveChildren"
class="port port-output"
:class="{
'connecting': connectionState.isConnecting &&
connectionState.startPortType === 'input' &&
connectionState.startNodeId !== node.id,
'drag-target': isValidConnectionTarget(node.id, 'output')
}"
@mousedown.stop="startConnection($event, node.id, 'output')"
@mouseenter="onPortHover(node.id, 'output')"
@mouseleave="onPortLeave()"
title="执行流出口"
>
<div class="port-inner"></div>
</div>
<div v-if="node.children && node.children.length > 0" class="children-indicator">
{{ node.children.length }}
</div>
</div>
</div>
<div class="grid-background" :style="gridStyle()"></div>
</div>
</div>
<div class="properties-panel-container">
<div class="properties-panel">
<div class="panel-header">
<h3>⚙️ 属性面板</h3>
</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>
<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>
<textarea
:value="activeNode.description"
@input="updateNodeProperty('description', $event.target.value)"
:key="activeNode.id + '_description'"
:disabled="activeNode.isConditionNode"
:placeholder="getNodeTemplate(activeNode.type)?.description || '请输入节点描述...'"
></textarea>
</div>
</div>
<div class="property-section" v-if="activeNode.properties">
<h4>节点属性</h4>
<div
v-for="(prop, key) in activeNode.properties"
:key="key"
class="property-item"
:class="{ 'blackboard-droppable': isBlackboardDroppable(prop) }"
@drop="handleBlackboardDrop($event, key)"
@dragover="handleBlackboardDragOver"
@dragleave="handleBlackboardDragLeave"
>
<label>{{ prop.name }}:</label>
<div class="property-input-container">
<!-- Blackboard 拖拽目标区域 -->
<div
v-if="isBlackboardDroppable(prop)"
class="blackboard-drop-zone"
:class="{ 'has-reference': isBlackboardReference(prop.value) }"
@drop="handleBlackboardDrop($event, key)"
@dragover="handleBlackboardDragOver"
@dragleave="handleBlackboardDragLeave"
>
<div v-if="!isBlackboardReference(prop.value)" class="drop-placeholder">
<span class="drop-icon">📋</span>
<span class="drop-text">拖拽Blackboard变量到此处</span>
</div>
<div v-else class="blackboard-reference">
<span class="ref-icon">🔗</span>
<span class="ref-text">{{ prop.value }}</span>
<button class="clear-ref" @click="clearBlackboardReference(key)" title="清除引用">×</button>
</div>
</div>
<!-- 输入控件 -->
<input
v-if="prop.type === 'string'"
type="text"
:value="prop.value"
@input="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
:key="activeNode.id + '_' + key + '_string'"
:placeholder="isBlackboardDroppable(prop) ? '或直接输入值' : '请输入值'"
:class="{ 'with-blackboard': isBlackboardDroppable(prop) }"
>
<input
v-else-if="prop.type === 'number'"
type="number"
:value="prop.value"
@input="updateNodeProperty('properties.' + key + '.value', parseFloat($event.target.value) || 0)"
:key="activeNode.id + '_' + key + '_number'"
:placeholder="isBlackboardDroppable(prop) ? '或直接输入值' : '请输入值'"
:class="{ 'with-blackboard': isBlackboardDroppable(prop) }"
>
<input
v-else-if="prop.type === 'boolean'"
type="checkbox"
:checked="prop.value"
@change="updateNodeProperty('properties.' + key + '.value', $event.target.checked)"
:key="activeNode.id + '_' + key + '_boolean'"
>
<textarea
v-else-if="prop.type === 'code'"
:value="prop.value"
@input="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
rows="6"
class="code-input"
placeholder="请输入代码..."
:key="activeNode.id + '_' + key + '_code'"
></textarea>
<select
v-else-if="prop.type === 'select'"
:value="prop.value"
@change="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
:key="activeNode.id + '_' + key + '_select_' + prop.value"
>
<option
v-for="option in prop.options"
:key="option"
:value="option"
:selected="option === prop.value"
>
{{ option }}
</option>
</select>
</div>
<p v-if="prop.description" class="property-help">{{ prop.description }}</p>
</div>
</div>
<div class="property-section">
<h4>节点配置</h4>
<pre class="config-preview">{{ activeNode ? JSON.stringify(activeNode, null, 2) : '{}' }}</pre>
</div>
</div>
<div v-else class="no-selection">
<p>请选择一个节点查看属性</p>
</div>
</div>
</div>
</div>
<div class="tree-structure-panel" v-if="rootNode()">
<div class="panel-header">
<h3>🌲 树结构</h3>
</div>
<div class="tree-view">
<tree-node-item
:node="rootNode()"
:level="0"
:get-node-by-id-local="getNodeByIdLocal"
@node-select="selectNode"
></tree-node-item>
</div>
</div>
<div v-if="!validationResult().isValid" class="validation-error">
<span class="error-icon">⚠️</span>
<span>{{ validationResult().message }}</span>
</div>
<div v-if="showBlackboardModal" class="modal-overlay" @click="showBlackboardModal = false">
<div class="modal-content blackboard-modal" @click.stop>
<div class="modal-header">
<h3>{{ editingBlackboardVariable ? '编辑变量' : '添加变量' }}</h3>
<button @click="showBlackboardModal = false">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>变量名称:</label>
<input
type="text"
v-model="blackboardModalData.name"
placeholder="例如playerHealth、enemyCount..."
:disabled="editingBlackboardVariable"
class="form-input"
>
</div>
<div class="form-group">
<label>变量类型:</label>
<select v-model="blackboardModalData.type" class="form-select">
<option value="string">📝 字符串 (string)</option>
<option value="number">🔢 数字 (number)</option>
<option value="boolean">☑️ 布尔值 (boolean)</option>
<option value="vector2">📐 二维向量 (vector2)</option>
<option value="vector3">📏 三维向量 (vector3)</option>
<option value="object">📦 对象 (object)</option>
<option value="array">📋 数组 (array)</option>
</select>
</div>
<div class="form-group">
<label>默认值:</label>
<input
v-if="blackboardModalData.type === 'string'"
type="text"
v-model="blackboardModalData.defaultValue"
placeholder="输入默认字符串值..."
class="form-input"
>
<input
v-else-if="blackboardModalData.type === 'number'"
type="number"
v-model="blackboardModalData.defaultValue"
placeholder="输入默认数值..."
class="form-input"
>
<div v-else-if="blackboardModalData.type === 'boolean'" class="checkbox-group">
<input
type="checkbox"
v-model="blackboardModalData.defaultValue"
id="defaultBoolValue"
>
<label for="defaultBoolValue">默认为 True</label>
</div>
<textarea
v-else
v-model="blackboardModalData.defaultValue"
placeholder="请输入JSON格式的默认值例如{&quot;x&quot;: 0, &quot;y&quot;: 0}"
rows="3"
class="form-textarea"
></textarea>
</div>
<div class="form-group">
<label>描述:</label>
<textarea
v-model="blackboardModalData.description"
placeholder="描述这个变量的用途和作用..."
rows="2"
class="form-textarea"
></textarea>
</div>
<div class="form-group">
<label>分组:</label>
<input
type="text"
v-model="blackboardModalData.group"
placeholder="例如Player、AI、Environment、Game..."
class="form-input"
>
</div>
<div v-if="blackboardModalData.type === 'number'" class="form-group">
<label>数值约束:</label>
<div class="constraint-inputs">
<div class="constraint-row">
<label>最小值:</label>
<input type="number" v-model="blackboardModalData.constraints.min" placeholder="不限制" class="form-input">
</div>
<div class="constraint-row">
<label>最大值:</label>
<input type="number" v-model="blackboardModalData.constraints.max" placeholder="不限制" class="form-input">
</div>
<div class="constraint-row">
<label>步长:</label>
<input type="number" v-model="blackboardModalData.constraints.step" placeholder="1" class="form-input">
</div>
</div>
</div>
<div v-if="blackboardModalData.type === 'string'" class="form-group">
<div class="checkbox-group">
<input type="checkbox" v-model="blackboardModalData.useAllowedValues" id="useAllowedValues">
<label for="useAllowedValues">限制可选值</label>
</div>
<div v-if="blackboardModalData.useAllowedValues" class="allowed-values">
<textarea
v-model="blackboardModalData.allowedValuesText"
placeholder="每行输入一个可选值&#10;例如:&#10;idle&#10;running&#10;jumping&#10;attacking"
rows="4"
class="form-textarea"
></textarea>
</div>
</div>
<div class="checkbox-group">
<input type="checkbox" v-model="blackboardModalData.readOnly" id="readOnlyVar">
<label for="readOnlyVar">只读变量</label>
</div>
</div>
<div class="modal-footer">
<button @click="saveBlackboardVariable" class="save-btn">保存</button>
<button @click="showBlackboardModal = false" class="cancel-btn">取消</button>
</div>
</div>
</div>
<div class="blackboard-sidebar"
:class="{
'collapsed': blackboardCollapsed,
'transparent': blackboardTransparent
}"
@mouseenter="blackboardTransparent = false"
@mouseleave="blackboardTransparent = true">
<button class="blackboard-toggle"
@click="blackboardCollapsed = !blackboardCollapsed"
:title="blackboardCollapsed ? '展开 Blackboard' : '收缩 Blackboard'">
<span v-if="blackboardCollapsed">📋</span>
<span v-else></span>
</button>
<div class="blackboard-content" v-show="!blackboardCollapsed">
<div class="blackboard-header">
<h3>📋 Blackboard</h3>
</div>
<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>
<!-- 使用提示 -->
<div v-if="blackboardVariables.length > 0" class="blackboard-usage-hint">
<div class="hint-content">
<span class="hint-icon">💡</span>
<span class="hint-text">拖拽变量名使用</span>
</div>
</div>
<div class="blackboard-scroll-area">
<div v-if="blackboardVariables.length > 0" class="blackboard-groups">
<div
v-for="[groupName, variables] in groupedBlackboardVariables()"
:key="groupName"
class="variable-group"
>
<h4 class="group-title">{{ groupName }}</h4>
<div class="variable-list">
<div
v-for="variable in variables"
:key="variable.name"
class="variable-item"
:class="variable.type"
:title="variable.description"
>
<div class="variable-header">
<div
class="variable-drag-area"
:draggable="true"
@dragstart="onBlackboardDragStart($event, variable)"
@click.stop
>
<span class="variable-name">{{ variable.name }}</span>
<span class="variable-type">{{ getTypeDisplayName(variable.type) }}</span>
<span class="drag-hint">🎯</span>
</div>
<div class="variable-actions">
<button @click.stop="editBlackboardVariable(variable)" class="edit-btn">✏️</button>
<button @click.stop="removeBlackboardVariable(variable.name)" class="remove-btn">🗑️</button>
</div>
</div>
<div class="variable-value">
<select
v-if="variable.constraints && variable.constraints.allowedValues"
v-model="variable.value"
@change="onBlackboardValueChange(variable)"
@click.stop
:disabled="variable.readOnly"
>
<option
v-for="option in variable.constraints.allowedValues"
:key="option"
:value="option"
>
{{ option }}
</option>
</select>
<span v-else class="value-display">{{ getDisplayValue(variable) }}</span>
</div>
<div v-if="variable.constraints && hasVisibleConstraints(variable)" class="variable-constraints">
<small>{{ formatConstraints(variable.constraints) }}</small>
</div>
</div>
</div>
</div>
</div>
<div v-else class="empty-blackboard">
<div class="empty-icon">📋</div>
<p>还没有定义任何变量</p>
<button class="add-first-var" @click="showBlackboardModal = true">添加第一个变量</button>
</div>
</div>
</div>
</div>
<div v-if="showExportModal" class="modal-overlay" @click="showExportModal = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>⚡ 导出行为树配置</h3>
<button @click="showExportModal = false">×</button>
</div>
<div class="modal-body">
<div class="export-options">
<h4 style="margin: 0 0 12px 0; color: #e2e8f0; font-size: 14px;">📄 选择导出格式:</h4>
<label>
<input type="radio" v-model="exportFormat" value="json">
<span>📄 JSON配置文件</span>
<small style="display: block; margin-left: 24px; color: #a0aec0; font-size: 11px;">适用于运行时动态加载行为树</small>
</label>
<label class="disabled-option">
<input type="radio" v-model="exportFormat" value="typescript" disabled>
<span>📝 TypeScript代码 (暂不可用)</span>
<small style="display: block; margin-left: 24px; color: #6b7280; font-size: 11px;">此功能正在开发中,敬请期待</small>
</label>
</div>
<div class="preview-section">
<h4 style="margin: 0 0 8px 0; color: #e2e8f0; font-size: 13px;">📋 代码预览:</h4>
<textarea
class="export-code"
:value="exportedCode()"
readonly
></textarea>
</div>
<div class="usage-hint" style="margin-top: 16px; padding: 12px; background: rgba(102, 126, 234, 0.08); border: 1px solid rgba(102, 126, 234, 0.2); border-radius: 6px;">
<div style="color: #a0aec0; font-size: 12px; line-height: 1.4;">
<strong style="color: #e2e8f0;">💡 使用提示:</strong><br/>
<span v-if="exportFormat === 'json'">
• JSON配置可用于运行时动态加载行为树<br/>
• 使用 BehaviorTreeBuilder.fromConfig() 方法构建行为树<br/>
• 可以保存为 .json 文件在项目中使用
</span>
<span v-else>
• 当前仅支持JSON格式导出<br/>
• TypeScript代码生成功能正在开发中
</span>
</div>
</div>
</div>
<div class="modal-footer">
<button @click="copyToClipboard" class="copy-btn">📋 复制到剪贴板</button>
<button @click="saveToFile" class="save-file-btn">💾 保存到文件</button>
<button @click="showExportModal = false" class="close-btn">❌ 关闭</button>
</div>
</div>
</div>

View File

@@ -1,194 +0,0 @@
<div class="blackboard-panel">
<div class="panel-header">
<h3>🎯 黑板变量</h3>
<div class="header-actions">
<button class="btn-add-variable" @click="showAddVariableDialog = true" title="添加新变量">
添加变量
</button>
<button class="btn-import-export" @click="showImportExportDialog = true" title="导入/导出变量">
📤 导入/导出
</button>
</div>
</div>
<div class="variables-container">
<!-- 按分组显示变量 -->
<div v-for="group in groups" :key="group" class="variable-group">
<div class="group-header" @click="toggleGroup(group)">
<span class="group-icon">{{ expandedGroups.has(group) ? '📂' : '📁' }}</span>
<span class="group-name">{{ group }}</span>
<span class="group-count">({{ groupedBlackboardVariables()[group]?.length || 0 }})</span>
</div>
<div v-if="expandedGroups.has(group)" class="group-variables">
<div v-for="variable in groupedBlackboardVariables()[group]" :key="variable.name" class="variable-item"
:class="[variable.type, { 'readonly': variable.readonly }]" :draggable="true"
@dragstart="onBlackboardDragStart($event, variable)">
<div class="variable-header">
<div class="variable-info">
<span class="variable-name">{{ variable.name }}</span>
<span class="value-separator">:</span>
<span class="value-display">{{ getDisplayValue(variable) }}</span>
</div>
<span class="variable-type">{{ getTypeDisplayName(variable.type) }}</span>
</div>
<div class="variable-actions">
<button @click="editVariable(variable)" title="编辑" class="edit-btn"></button>
<button @click="removeBlackboardVariable(variable.name)" title="删除"
class="remove-btn">×</button>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="groups.length === 0" class="empty-state">
<div class="empty-icon">📋</div>
<div class="empty-text">还没有黑板变量</div>
<div class="empty-hint">点击"添加变量"来创建第一个变量</div>
</div>
</div>
<!-- 添加变量对话框 -->
<div v-if="showAddVariableDialog" class="dialog-overlay" @click="closeAddVariableDialog">
<div class="dialog" @click.stop>
<div class="dialog-header">
<h4>{{ editingVariable ? '编辑变量' : '添加变量' }}</h4>
<button @click="closeAddVariableDialog" class="dialog-close"></button>
</div>
<div class="dialog-content">
<div class="form-group">
<label>变量名</label>
<input v-model="newVariable.name" :disabled="editingVariable" type="text" placeholder="请输入变量名"
class="form-input">
</div>
<div class="form-group">
<label>变量类型</label>
<select v-model="newVariable.type" class="form-select" @change="onTypeChange">
<option value="string">字符串</option>
<option value="number">数字</option>
<option value="boolean">布尔</option>
<option value="vector2">Vector2</option>
<option value="vector3">Vector3</option>
<option value="object">对象</option>
<option value="array">数组</option>
</select>
</div>
<div class="form-group">
<label>默认值</label>
<!-- 根据类型显示不同的输入控件 -->
<input v-if="newVariable.type === 'string'" v-model="newVariable.defaultValue" type="text"
class="form-input">
<input v-else-if="newVariable.type === 'number'" v-model.number="newVariable.defaultValue" type="number"
class="form-input">
<label v-else-if="newVariable.type === 'boolean'" class="checkbox-label">
<input type="checkbox" v-model="newVariable.defaultValue">
<span>{{ newVariable.defaultValue ? '真' : '假' }}</span>
</label>
<div v-else-if="newVariable.type === 'vector2'" class="vector-input">
<input v-model.number="newVariable.defaultValue.x" type="number" placeholder="X">
<input v-model.number="newVariable.defaultValue.y" type="number" placeholder="Y">
</div>
<div v-else-if="newVariable.type === 'vector3'" class="vector-input">
<input v-model.number="newVariable.defaultValue.x" type="number" placeholder="X">
<input v-model.number="newVariable.defaultValue.y" type="number" placeholder="Y">
<input v-model.number="newVariable.defaultValue.z" type="number" placeholder="Z">
</div>
<textarea v-else-if="newVariable.type === 'object' || newVariable.type === 'array'"
v-model="newVariable.defaultValueText" class="form-textarea" rows="3"
placeholder="JSON格式"></textarea>
</div>
<div class="form-group">
<label>描述</label>
<input v-model="newVariable.description" type="text" placeholder="变量的用途描述" class="form-input">
</div>
<div class="form-group">
<label>分组</label>
<input v-model="newVariable.group" type="text" placeholder="变量分组Player、Enemy等" class="form-input">
</div>
<div class="form-row">
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" v-model="newVariable.readonly">
<span>只读</span>
</label>
</div>
</div>
<!-- 数字类型的约束 -->
<div v-if="newVariable.type === 'number'" class="form-row">
<div class="form-group">
<label>最小值</label>
<input v-model.number="newVariable.min" type="number" class="form-input">
</div>
<div class="form-group">
<label>最大值</label>
<input v-model.number="newVariable.max" type="number" class="form-input">
</div>
</div>
<!-- 可选值列表 -->
<div class="form-group">
<label>可选值列表(用逗号分隔)</label>
<input v-model="newVariable.optionsText" type="text" placeholder="例如: small,medium,large"
class="form-input">
</div>
</div>
<div class="dialog-actions">
<button @click="closeAddVariableDialog" class="btn-cancel">取消</button>
<button @click="saveVariable" class="btn-save" :disabled="!isValidVariable">
{{ editingVariable ? '保存' : '添加' }}
</button>
</div>
</div>
</div>
<!-- 导入/导出对话框 -->
<div v-if="showImportExportDialog" class="dialog-overlay" @click="closeImportExportDialog">
<div class="dialog large" @click.stop>
<div class="dialog-header">
<h4>导入/导出黑板变量</h4>
<button @click="closeImportExportDialog" class="dialog-close"></button>
</div>
<div class="dialog-content">
<div class="tab-container">
<div class="tabs">
<button :class="{ active: activeTab === 'export' }" @click="activeTab = 'export'">导出</button>
<button :class="{ active: activeTab === 'import' }" @click="activeTab = 'import'">导入</button>
</div>
<div v-if="activeTab === 'export'" class="tab-content">
<p>以下是当前黑板变量的JSON数据可以复制保存</p>
<textarea v-model="exportData" readonly class="export-textarea" rows="15"></textarea>
<button @click="copyExportData" class="btn-copy">📋 复制到剪贴板</button>
</div>
<div v-if="activeTab === 'import'" class="tab-content">
<p>粘贴黑板变量的JSON数据进行导入</p>
<textarea v-model="importData" class="import-textarea" rows="15"
placeholder="粘贴JSON数据..."></textarea>
<div class="import-options">
<label class="checkbox-label">
<input type="checkbox" v-model="clearBeforeImport">
<span>导入前清空现有变量</span>
</label>
</div>
<button @click="importVariables" class="btn-import" :disabled="!importData.trim()">
📥 导入变量
</button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,16 +0,0 @@
<div class="tree-node-item" :style="{ paddingLeft: (level * 16) + 'px' }">
<div class="tree-node-line" @click="$emit('node-select', node.id)">
<span class="tree-node-icon">{{ node.icon }}</span>
<span class="tree-node-name">{{ node.name }}</span>
<span class="tree-node-type">({{ node.type }})</span>
</div>
<tree-node-item
v-for="childId in (node.children || [])"
:key="childId"
:node="getNodeByIdLocal(childId)"
:level="level + 1"
:get-node-by-id-local="getNodeByIdLocal"
@node-select="$emit('node-select', $event)"
v-if="getNodeByIdLocal(childId)"
></tree-node-item>
</div>

View File

@@ -1,3 +0,0 @@
<div id="behavior-tree-app">
<behavior-tree-editor></behavior-tree-editor>
</div>

View File

@@ -1,382 +0,0 @@
<div class="debug-panel">
<!-- 顶部控制栏 -->
<div class="debug-toolbar">
<div class="toolbar-section">
<label>游戏实例:</label>
<select v-model="selectedInstanceId" @change="onInstanceChanged" class="instance-selector">
<option value="">-- 选择实例 --</option>
<option v-for="instance in gameInstances" :key="instance.id" :value="instance.id">
{{ instance.name }} ({{ instance.isActive ? '活跃' : '离线' }})
</option>
</select>
<span class="instance-count">{{ gameInstances.length }} 个实例</span>
</div>
<div class="toolbar-section">
<button @click="manualRefresh" class="btn-refresh" :disabled="!selectedInstanceId">
<ui-icon value="refresh"></ui-icon>
刷新
</button>
<label class="checkbox-label">
<input type="checkbox" v-model="isAutoRefresh" @change="toggleAutoRefresh">
自动刷新
</label>
<select v-model="refreshInterval" @change="changeRefreshInterval" :disabled="!isAutoRefresh">
<option :value="100">0.1秒</option>
<option :value="250">0.25秒</option>
<option :value="500">0.5秒</option>
<option :value="1000">1秒</option>
<option :value="2000">2秒</option>
</select>
</div>
<div class="toolbar-section">
<span class="last-update">{{ lastUpdateTime }}</span>
</div>
</div>
<!-- 连接状态和基础信息 -->
<div class="debug-section">
<div class="section-header">
<h2>📊 实例状态</h2>
</div>
<div class="status-grid" v-if="selectedInstanceId && debugInfo.instanceId">
<div class="status-item">
<span class="label">实例名称:</span>
<span class="value">{{ debugInfo.instanceName }}</span>
</div>
<div class="status-item">
<span class="label">运行状态:</span>
<span class="value" :class="{ online: debugInfo.isRunning, offline: !debugInfo.isRunning }">
{{ debugInfo.isRunning ? '运行中' : '已停止' }}
</span>
</div>
<div class="status-item">
<span class="label">当前场景:</span>
<span class="value">{{ debugInfo.currentScene }}</span>
</div>
<div class="status-item">
<span class="label">运行时间:</span>
<span class="value">{{ formatUptime(debugInfo.uptime) }}</span>
</div>
</div>
<div class="no-instance" v-else>
<ui-icon value="info" class="info-icon"></ui-icon>
<span>请选择一个游戏实例查看详细信息</span>
</div>
</div>
<!-- 性能监控 -->
<div class="debug-section" v-if="debugInfo.instanceId">
<div class="section-header">
<h2>⚡ 性能监控</h2>
</div>
<div class="performance-grid">
<div class="perf-card">
<div class="perf-title">帧率</div>
<div class="perf-value" :class="getFpsColor(debugInfo.performance.fps)">
{{ debugInfo.performance.fps }} FPS
</div>
<div class="perf-detail">
引擎帧时间: {{ debugInfo.performance.engineFrameTime?.toFixed(2) || '0.00' }}ms
</div>
</div>
<div class="perf-card">
<div class="perf-title">ECS执行时间</div>
<div class="perf-value" :class="getECSTimeColor(debugInfo.performance.ecsPercentage)">
{{ debugInfo.performance.frameTime.toFixed(3) }}ms
</div>
<div class="perf-detail">
占总帧时间: {{ debugInfo.performance.ecsPercentage?.toFixed(1) || '0.0' }}%
</div>
</div>
<div class="perf-card">
<div class="perf-title">总内存</div>
<div class="perf-value" :class="getMemoryColor(debugInfo.memory.usedMemory / debugInfo.memory.totalMemory * 100)">
{{ formatMemory(debugInfo.memory.usedMemory) }}
</div>
<div class="perf-detail">
/ {{ formatMemory(debugInfo.memory.totalMemory) }}
</div>
</div>
<div class="perf-card">
<div class="perf-title">GC回收</div>
<div class="perf-value">{{ debugInfo.memory.gcCollections }}</div>
<div class="perf-detail">次数</div>
</div>
</div>
</div>
<!-- 内存分析 -->
<div class="debug-section" v-if="debugInfo.instanceId">
<div class="section-header">
<h2>💾 内存分析</h2>
</div>
<div class="memory-breakdown">
<div class="memory-item">
<span class="memory-label">实体内存:</span>
<span class="memory-value">{{ formatMemory(debugInfo.memory.entityMemory) }}</span>
<div class="memory-bar">
<div class="memory-fill" :style="{ width: (debugInfo.memory.entityMemory / debugInfo.memory.usedMemory * 100) + '%' }"></div>
</div>
</div>
<div class="memory-item">
<span class="memory-label">组件内存:</span>
<span class="memory-value">{{ formatMemory(debugInfo.memory.componentMemory) }}</span>
<div class="memory-bar">
<div class="memory-fill" :style="{ width: (debugInfo.memory.componentMemory / debugInfo.memory.usedMemory * 100) + '%' }"></div>
</div>
</div>
<div class="memory-item">
<span class="memory-label">系统内存:</span>
<span class="memory-value">{{ formatMemory(debugInfo.memory.systemMemory) }}</span>
<div class="memory-bar">
<div class="memory-fill" :style="{ width: (debugInfo.memory.systemMemory / debugInfo.memory.usedMemory * 100) + '%' }"></div>
</div>
</div>
<div class="memory-item">
<span class="memory-label">对象池内存:</span>
<span class="memory-value">{{ formatMemory(debugInfo.memory.pooledMemory) }}</span>
<div class="memory-bar">
<div class="memory-fill" :style="{ width: (debugInfo.memory.pooledMemory / debugInfo.memory.usedMemory * 100) + '%' }"></div>
</div>
</div>
</div>
</div>
<!-- 实体统计 -->
<div class="debug-section" v-if="debugInfo.instanceId">
<div class="section-header">
<h2>👥 实体统计</h2>
</div>
<div class="entity-stats">
<div class="stat-card">
<div class="stat-number">{{ debugInfo.entities.total }}</div>
<div class="stat-label">总实体数</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ debugInfo.entities.active }}</div>
<div class="stat-label">活跃实体</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ debugInfo.entities.inactive }}</div>
<div class="stat-label">非活跃实体</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ debugInfo.entities.pendingAdd }}</div>
<div class="stat-label">待添加</div>
</div>
</div>
<!-- Archetype分布 -->
<div class="archetype-section" v-if="debugInfo.entities.entitiesPerArchetype.length > 0">
<h3>Archetype 分布</h3>
<div class="archetype-list">
<div v-for="archetype in debugInfo.entities.entitiesPerArchetype" :key="archetype.signature" class="archetype-item">
<div class="archetype-signature">{{ archetype.signature }}</div>
<div class="archetype-stats">
<span class="entity-count">{{ archetype.count }} 实体</span>
<span class="memory-usage">{{ formatMemory(archetype.memory) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 组件分析 -->
<div class="debug-section" v-if="debugInfo.instanceId">
<div class="section-header">
<h2>🧩 组件分析</h2>
<div class="section-help">
<button class="help-btn" @click="showComponentPoolHelp = !showComponentPoolHelp" title="组件对象池使用说明">
<ui-icon value="help"></ui-icon>
对象池使用说明
</button>
</div>
</div>
<!-- 对象池使用说明 -->
<div v-if="showComponentPoolHelp" class="help-panel">
<div class="help-content">
<h4>📖 组件对象池使用指南</h4>
<p>组件对象池用于复用组件实例,减少频繁创建/销毁组件带来的内存分配开销。当前利用率为0表示还未配置对象池。</p>
<div class="help-steps">
<div class="help-step">
<h5>1. 注册组件对象池</h5>
<pre class="code-example">import { ComponentPoolManager } from '@esengine/ecs-framework';
// 为Transform组件注册对象池
ComponentPoolManager.getInstance().registerPool(
'Transform',
() => new Transform(),
(comp) => comp.reset(), // 可选的重置函数
100 // 池大小
);</pre>
</div>
<div class="help-step">
<h5>2. 使用对象池获取组件</h5>
<pre class="code-example">// 从对象池获取组件实例
const poolManager = ComponentPoolManager.getInstance();
const transform = poolManager.acquireComponent('Transform');
// 使用完毕后释放回池中
poolManager.releaseComponent('Transform', transform);</pre>
</div>
<div class="help-step">
<h5>3. 查看性能改进</h5>
<p>正确配置后利用率栏将显示池的使用情况。利用率越高说明对象池被有效使用可以减少GC压力。</p>
</div>
</div>
<div class="help-links">
<a href="#" @click="openDocumentation('component-pool')" class="doc-link">
<ui-icon value="book"></ui-icon>
查看详细文档
</a>
<a href="#" @click="openDocumentation('performance-optimization')" class="doc-link">
<ui-icon value="speed"></ui-icon>
性能优化指南
</a>
</div>
<div class="doc-access-note">
<ui-icon value="info"></ui-icon>
<span>如果无法直接打开文档链接,链接将自动复制到剪贴板,请在浏览器中粘贴访问</span>
</div>
</div>
</div>
<div class="component-overview">
<div class="overview-item">
<span class="label">组件类型:</span>
<span class="value">{{ debugInfo.components.totalTypes }}</span>
</div>
<div class="overview-item">
<span class="label">组件实例:</span>
<span class="value">{{ debugInfo.components.totalInstances }}</span>
</div>
</div>
<div class="component-table" v-if="debugInfo.components.componentStats.length > 0">
<div class="table-header">
<div class="col-name">组件类型</div>
<div class="col-count">实例数</div>
<div class="col-memory">内存占用</div>
<div class="col-pool">对象池</div>
<div class="col-efficiency">
利用率
<span class="help-tooltip" title="对象池利用率:使用中的实例 / 池总大小。利用率为0表示未配置对象池">
<ui-icon value="help"></ui-icon>
</span>
</div>
</div>
<div v-for="comp in debugInfo.components.componentStats" :key="comp.typeName" class="table-row">
<div class="col-name">{{ comp.typeName }}</div>
<div class="col-count">{{ comp.instanceCount }}</div>
<div class="col-memory">{{ formatMemory(comp.totalMemory) }}</div>
<div class="col-pool">
{{ comp.poolSize || '未配置' }}
<span v-if="comp.poolSize === 0" class="not-configured" title="点击上方说明了解如何配置对象池">
<ui-icon value="warning"></ui-icon>
</span>
</div>
<div class="col-efficiency">
<div class="efficiency-bar">
<div class="efficiency-fill" :style="{ width: comp.poolUtilization + '%' }"></div>
</div>
{{ comp.poolUtilization.toFixed(1) }}%
<span v-if="comp.poolUtilization === 0 && comp.poolSize === 0" class="no-pool-hint">
(无对象池)
</span>
</div>
</div>
</div>
<!-- 无对象池组件提示 -->
<div v-if="debugInfo.components.componentStats.length > 0 && debugInfo.components.componentStats.every(c => c.poolSize === 0)" class="pool-suggestion">
<div class="suggestion-content">
<ui-icon value="lightbulb" class="suggestion-icon"></ui-icon>
<div class="suggestion-text">
<h4>💡 性能优化建议</h4>
<p>检测到所有组件都未配置对象池。为频繁创建/销毁的组件配置对象池可以显著提升性能,减少垃圾回收压力。</p>
<button @click="showComponentPoolHelp = true" class="suggestion-action">
了解如何配置对象池
</button>
</div>
</div>
</div>
</div>
<!-- 系统性能 -->
<div class="debug-section" v-if="debugInfo.instanceId">
<div class="section-header">
<h2>⚙️ 系统性能</h2>
</div>
<div class="systems-table" v-if="debugInfo.systems.systemStats.length > 0">
<div class="table-header">
<div class="col-name">系统名称</div>
<div class="col-entities">实体数</div>
<div class="col-time">执行时间</div>
<div class="col-percentage">ECS占比</div>
<div class="col-order">优先级</div>
<div class="col-status">状态</div>
</div>
<div v-for="system in debugInfo.systems.systemStats" :key="system.name" class="table-row">
<div class="col-name">{{ system.name }}</div>
<div class="col-entities">{{ system.entityCount }}</div>
<div class="col-time">
<span :class="getExecutionTimeColor(system.averageExecutionTime)">
{{ system.averageExecutionTime.toFixed(3) }}ms
</span>
<div class="time-range">
({{ system.minExecutionTime.toFixed(3) }}-{{ system.maxExecutionTime.toFixed(3) }}ms)
</div>
</div>
<div class="col-percentage">
<div class="percentage-bar">
<div class="percentage-fill" :style="{ width: (system.percentage || 0) + '%' }"></div>
</div>
{{ (system.percentage || 0).toFixed(1) }}%
</div>
<div class="col-order">{{ system.updateOrder }}</div>
<div class="col-status">
<span :class="{ enabled: system.enabled, disabled: !system.enabled }">
{{ system.enabled ? '启用' : '禁用' }}
</span>
</div>
</div>
</div>
</div>
<!-- 服务器状态 -->
<div class="debug-section" v-if="gameInstances.length === 0">
<div class="section-header">
<h2>🔌 调试服务器</h2>
</div>
<div class="server-status">
<div class="status-message">
<ui-icon value="wifi" class="server-icon"></ui-icon>
<div class="message-content">
<h3>等待游戏实例连接...</h3>
<p>调试服务器正在 <strong>ws://localhost:8080</strong> 监听连接</p>
<p>请确保游戏中的ECS框架已启用调试模式</p>
</div>
</div>
<div class="connection-help">
<h4>连接说明:</h4>
<ul>
<li>在ECSManager组件中启用调试模式</li>
<li>运行游戏,框架会自动连接到调试服务器</li>
<li>支持多个游戏实例同时连接</li>
<li>可以在上方选择不同的实例进行调试</li>
</ul>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +0,0 @@
<div class="ecs-welcome-panel">
<div id="app">
<ecs-welcome></ecs-welcome>
</div>
</div>

View File

@@ -1,111 +0,0 @@
<div class="ecs-generator">
<div class="generator-container">
<!-- 头部区域 -->
<div class="generator-header">
<h1>🛠️ ECS 代码生成器</h1>
<p>输入功能名称,快速生成组件和系统代码</p>
</div>
<!-- 表单区域 -->
<div class="generator-form">
<div class="form-title">📝 基础设置</div>
<!-- 功能名称输入 -->
<div class="input-group">
<label class="input-label">功能名称</label>
<input
v-model="featureName"
@input="updatePreview"
type="text"
class="feature-input"
placeholder="例如Health、Movement、Combat"
maxlength="50"
/>
<div class="input-hint">
💡 使用英文名称首字母大写例如Health、Movement、Combat
</div>
</div>
<!-- 生成选项 -->
<div class="options-group">
<label class="input-label">生成内容</label>
<div class="option-item">
<label class="checkbox-wrapper">
<input type="checkbox" v-model="options.generateComponent" @change="updatePreview">
<span class="option-title">📦 生成组件 (Component)</span>
</label>
</div>
<div class="option-description" v-if="options.generateComponent">
创建基础组件类包含reset()方法和基本结构
</div>
<div class="option-item">
<label class="checkbox-wrapper">
<input type="checkbox" v-model="options.generateSystem" @change="updatePreview">
<span class="option-title">⚙️ 生成系统 (System)</span>
</label>
</div>
<div class="option-description" v-if="options.generateSystem">
创建处理组件的系统类,包含完整的生命周期方法
</div>
<!-- 系统详细配置 -->
<div class="system-config" v-if="options.generateSystem">
<!-- 组件过滤选项 -->
<div class="config-section">
<label class="config-label">🔍 组件过滤</label>
<div class="filter-option">
<label class="checkbox-wrapper">
<input type="checkbox" v-model="systemOptions.filterByComponent" @change="updatePreview">
<span class="option-title">过滤包含组件的实体</span>
</label>
</div>
<div class="option-description" v-if="systemOptions.filterByComponent">
系统只处理包含指定组件的实体。如果勾选了"生成组件",会自动过滤{{featureName}}Component
</div>
</div>
<!-- 系统类型选择 -->
<div class="config-section">
<label class="config-label">🎯 系统类型</label>
<div class="system-type-cards">
<div
v-for="type in systemTypes"
:key="type.value"
class="system-card"
:class="{ active: systemOptions.systemType === type.value }"
@click="selectSystemType(type.value)">
<div class="card-header">
<span class="card-icon">{{ type.icon }}</span>
<span class="card-title">{{ type.name }}</span>
</div>
<div class="card-description">{{ type.description }}</div>
<div class="card-usage">{{ type.usage }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 预览区域 -->
<div class="preview-section" v-if="showPreview">
<div class="preview-title">👀 生成预览</div>
<div class="preview-content">{{ previewCode }}</div>
</div>
<!-- 生成按钮 -->
<div class="action-section">
<button
class="generate-btn"
:disabled="isGenerating || (!options.generateComponent && !options.generateSystem) || !featureName.trim()"
@click="generateCode">
<span class="btn-icon" :class="{ spinning: isGenerating }">
{{ isGenerating ? '⏳' : '🚀' }}
</span>
{{ isGenerating ? '生成中...' : '生成代码' }}
</button>
</div>
</div>
</div>

View File

@@ -1,6 +0,0 @@
<div class="counter">
<h2> {{counter}}</h2>
<ui-button class="blue"
@click="addition">+</ui-button>
<ui-button @click="subtraction">-</ui-button>
</div>

View File

@@ -1,162 +0,0 @@
<div class="welcome-container">
<!-- 头部欢迎区域 -->
<div class="header">
<h1>🎮 ECS Framework for Cocos Creator</h1>
<p>高性能实体组件系统框架</p>
</div>
<!-- 操作状态显示区域 -->
<div class="status-section" v-if="showOperationStatus">
<div class="status-title">📋 操作状态</div>
<div class="operation-status" :class="operationStatusType">
<div class="status-content">
<span v-if="operationStatusType === 'loading'" class="loading-spinner"></span>
<span class="status-icon">{{ getStatusIcon(operationStatusType) }}</span>
<span class="status-text">{{ operationStatusMessage }}</span>
</div>
<div v-if="operationStatusDetails" class="status-details">
{{ operationStatusDetails }}
</div>
</div>
</div>
<!-- 安装状态检测区域 -->
<div class="status-section">
<div class="status-title">📦 安装状态检测</div>
<div class="status-item">
<span class="status-label">ECS Framework (@esengine/ecs-framework)</span>
<div v-if="checkingStatus" class="status-badge checking">
<span class="loading-spinner"></span>检测中...
</div>
<div v-else-if="ecsInstalled" class="status-badge installed">
✅ 已安装 <span class="version-info">v{{ecsVersion}}</span>
<span v-if="hasUpdate" class="update-hint">有新版本v{{latestVersion}}</span>
</div>
<div v-else class="status-badge not-installed">
❌ 未安装
</div>
</div>
<div class="status-item">
<span class="status-label">项目package.json</span>
<div v-if="packageJsonExists" class="status-badge installed">
✅ 存在
</div>
<div v-else class="status-badge not-installed">
❌ 不存在
</div>
</div>
<div class="status-item">
<span class="status-label">Node.js环境</span>
<div class="status-badge installed">
✅ v{{nodeVersion}}
</div>
</div>
</div>
<!-- ECS框架管理区域 -->
<div class="status-section" v-if="ecsInstalled">
<div class="status-title">🔧 ECS Framework 管理</div>
<div class="management-buttons">
<ui-button
class="green"
:disabled="!hasUpdate || updating"
@click="updateEcsFramework">
<span v-if="updating" class="loading-spinner"></span>
{{ updating ? '更新中...' : (hasUpdate ? `更新到 v${latestVersion}` : '已是最新版本') }}
</ui-button>
<ui-button
class="red"
:disabled="uninstalling"
@click="uninstallEcsFramework">
<span v-if="uninstalling" class="loading-spinner"></span>
{{ uninstalling ? '卸载中...' : '卸载 ECS Framework' }}
</ui-button>
<ui-button
class="blue"
@click="checkForUpdates">
🔄 检查更新
</ui-button>
</div>
</div>
<!-- 快速操作区域 -->
<div class="actions-section">
<div class="status-title">🚀 快速开始</div>
<div class="action-card" :class="{ disabled: !packageJsonExists || installing }" @click="installEcsFramework">
<h3>
<span class="icon">📥</span>
{{ ecsInstalled ? '重新安装 ECS Framework' : '安装 ECS Framework' }}
</h3>
<p v-if="installing">正在安装中,请稍候...</p>
<p v-else-if="!packageJsonExists">请先确保项目有package.json文件</p>
<p v-else-if="ecsInstalled">重新安装当前版本的ECS框架</p>
<p v-else>安装@esengine/ecs-framework到当前项目</p>
</div>
<div class="action-card" @click="openDocumentation">
<h3>
<span class="icon">📚</span>
查看文档
</h3>
<p>打开ECS框架完整使用文档和教程</p>
</div>
<div class="action-card" :class="{ disabled: !ecsInstalled || templateExists }" @click="createEcsTemplate">
<h3>
<span class="icon">🛠️</span>
创建ECS模板
</h3>
<p v-if="!ecsInstalled">请先安装 ECS Framework</p>
<p v-else-if="templateExists">⚠️ ECS模板已存在避免覆盖现有代码</p>
<p v-else>生成基础的ECS项目结构和启动代码</p>
<div v-if="templateExists && existingFiles.length > 0" class="existing-files-info">
<div class="files-summary">
检测到 {{existingFiles.length}} 个已存在的文件
</div>
<div class="files-list" v-if="existingFiles.length <= 10">
<div v-for="file in existingFiles" :key="file" class="file-item">
📄 {{file}}
</div>
</div>
<div v-else class="files-overflow">
<div v-for="file in existingFiles.slice(0, 8)" :key="file" class="file-item">
📄 {{file}}
</div>
<div class="more-files">... 还有 {{existingFiles.length - 8}} 个文件</div>
</div>
</div>
</div>
</div>
<!-- 代码生成工具区域 -->
<div class="actions-section" v-if="ecsInstalled">
<div class="status-title">🛠️ 代码生成工具</div>
<div class="action-card special" @click="openGenerator">
<h3>
<span class="icon">🚀</span>
代码生成器
</h3>
<p>一站式代码生成工具,输入功能名称快速生成组件和系统代码</p>
</div>
</div>
<!-- 底部信息 -->
<div class="footer">
<p>ECS Framework Plugin v{{pluginVersion}} | 基于Vue 3.x构建</p>
<p>
更多信息请访问: <a href="#" @click="openGithub">GitHub Repository</a> |
<a href="#" @click="joinQQGroup">💬 加入QQ群交流</a>
</p>
<p v-if="lastCheckTime" class="last-check">最后检查时间: {{lastCheckTime}}</p>
</div>
</div>

View File

@@ -1,132 +0,0 @@
const fs = require('fs');
const path = require('path');
// 模拟项目路径(实际会是真实的项目路径)
const projectPath = process.cwd();
const settingsPath = path.join(projectPath, '.ecs-framework-settings.json');
console.log('🧪 测试ECS框架设置功能...');
console.log('设置文件路径:', settingsPath);
// 默认设置
const testSettings = {
codeGeneration: {
template: 'typescript',
useStrictMode: true,
generateComments: true,
generateImports: true,
componentSuffix: 'Component',
systemSuffix: 'System',
indentStyle: 'spaces',
indentSize: 4
},
performance: {
enableMonitoring: true,
warningThreshold: 16.67,
criticalThreshold: 33.33,
memoryWarningMB: 100,
memoryCriticalMB: 200,
maxRecentSamples: 60,
enableFpsMonitoring: true,
targetFps: 60
},
debugging: {
enableDebugMode: true,
showEntityCount: true,
showSystemExecutionTime: true,
enablePerformanceWarnings: true,
logLevel: 'info',
enableDetailedLogs: false
},
editor: {
autoRefreshAssets: true,
showWelcomePanelOnStartup: true,
enableAutoUpdates: false,
updateChannel: 'stable',
enableNotifications: true
},
template: {
defaultEntityName: 'TestEntity', // 修改这个值来测试
defaultComponentName: 'TestComponent',
defaultSystemName: 'TestSystem',
createExampleFiles: true,
includeDocumentation: true,
useFactoryPattern: true
},
events: {
enableEventSystem: true,
defaultEventPriority: 0,
enableAsyncEvents: true,
enableEventBatching: false,
batchSize: 10,
batchDelay: 16,
maxEventListeners: 100
}
};
// 测试保存功能
console.log('✅ 测试保存设置...');
try {
fs.writeFileSync(settingsPath, JSON.stringify(testSettings, null, 2), 'utf-8');
console.log('✅ 设置已成功保存到:', settingsPath);
} catch (error) {
console.error('❌ 保存设置失败:', error);
}
// 测试加载功能
console.log('✅ 测试加载设置...');
try {
if (fs.existsSync(settingsPath)) {
const loadedData = fs.readFileSync(settingsPath, 'utf-8');
const loadedSettings = JSON.parse(loadedData);
console.log('✅ 设置已成功加载');
console.log('默认实体名称:', loadedSettings.template.defaultEntityName);
console.log('调试模式:', loadedSettings.debugging.enableDebugMode);
console.log('目标FPS:', loadedSettings.performance.targetFps);
// 验证数据完整性
const expectedKeys = Object.keys(testSettings);
const loadedKeys = Object.keys(loadedSettings);
if (expectedKeys.every(key => loadedKeys.includes(key))) {
console.log('✅ 数据完整性检查通过');
} else {
console.log('❌ 数据完整性检查失败');
}
} else {
console.log('❌ 设置文件不存在');
}
} catch (error) {
console.error('❌ 加载设置失败:', error);
}
// 测试修改和重新保存
console.log('✅ 测试修改设置...');
try {
const modifiedSettings = { ...testSettings };
modifiedSettings.template.defaultEntityName = 'ModifiedEntity';
modifiedSettings.performance.targetFps = 120;
fs.writeFileSync(settingsPath, JSON.stringify(modifiedSettings, null, 2), 'utf-8');
// 重新加载验证
const reloadedData = fs.readFileSync(settingsPath, 'utf-8');
const reloadedSettings = JSON.parse(reloadedData);
if (reloadedSettings.template.defaultEntityName === 'ModifiedEntity' &&
reloadedSettings.performance.targetFps === 120) {
console.log('✅ 设置修改测试通过');
} else {
console.log('❌ 设置修改测试失败');
}
} catch (error) {
console.error('❌ 修改设置测试失败:', error);
}
console.log('🎉 测试完成!设置功能工作正常。');
console.log('📁 设置文件位置:', settingsPath);
// 清理测试文件(可选)
// fs.unlinkSync(settingsPath);
// console.log('🧹 已清理测试文件');

View File

@@ -1,15 +0,0 @@
{
"extends": "./base.tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./source",
"types": [
"node",
"@cocos/creator-types/editor",
]
},
"exclude": [
"admin-backend/**/*",
"admin-desktop/**/*"
]
}

1
thirdparty/admin-backend vendored Submodule