更新文档及优化行为树编辑器
This commit is contained in:
25
README.md
25
README.md
@@ -31,8 +31,27 @@ npm install @esengine/ecs-framework
|
|||||||
```typescript
|
```typescript
|
||||||
import { Core, Scene, Entity, Component, EntitySystem } from '@esengine/ecs-framework';
|
import { Core, Scene, Entity, Component, EntitySystem } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
// 创建核心实例
|
// 创建核心实例 - 使用配置对象(推荐)
|
||||||
const core = Core.create(true); // 调试模式
|
const core = Core.create({
|
||||||
|
debug: true, // 启用调试模式
|
||||||
|
enableEntitySystems: true, // 启用实体系统
|
||||||
|
debugConfig: { // 可选:调试配置
|
||||||
|
enabled: true,
|
||||||
|
websocketUrl: 'ws://localhost:8080',
|
||||||
|
autoReconnect: true,
|
||||||
|
updateInterval: 1000,
|
||||||
|
channels: {
|
||||||
|
entities: true,
|
||||||
|
systems: true,
|
||||||
|
performance: true,
|
||||||
|
components: true,
|
||||||
|
scenes: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 简化创建 - 向后兼容(仍然支持)
|
||||||
|
const core2 = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
|
||||||
|
|
||||||
// 创建场景
|
// 创建场景
|
||||||
const scene = new Scene();
|
const scene = new Scene();
|
||||||
@@ -305,6 +324,7 @@ enum ECSEventType {
|
|||||||
| 实体管理器 | ✅ 统一接口 | ❌ 低级 API | ✅ 高级接口 |
|
| 实体管理器 | ✅ 统一接口 | ❌ 低级 API | ✅ 高级接口 |
|
||||||
| 性能优化 | ✅ 多重优化 | ✅ 极致性能 | ✅ React 优化 |
|
| 性能优化 | ✅ 多重优化 | ✅ 极致性能 | ✅ React 优化 |
|
||||||
| JavaScript引擎集成 | ✅ 专为JS引擎设计 | ✅ 通用设计 | ⚠️ 主要 React |
|
| JavaScript引擎集成 | ✅ 专为JS引擎设计 | ✅ 通用设计 | ⚠️ 主要 React |
|
||||||
|
| 可视化调试工具 | ✅ [Cocos插件](https://store.cocos.com/app/detail/7823) | ❌ 无官方工具 | ✅ React DevTools |
|
||||||
|
|
||||||
**选择指南:**
|
**选择指南:**
|
||||||
- 选择本框架:需要完整的游戏开发工具链和中文社区支持
|
- 选择本框架:需要完整的游戏开发工具链和中文社区支持
|
||||||
@@ -331,6 +351,7 @@ ecs-framework/
|
|||||||
### 🎯 新手入门
|
### 🎯 新手入门
|
||||||
- **[📖 新手教程完整指南](docs/beginner-tutorials.md)** - 完整学习路径,从零开始 ⭐ **强烈推荐**
|
- **[📖 新手教程完整指南](docs/beginner-tutorials.md)** - 完整学习路径,从零开始 ⭐ **强烈推荐**
|
||||||
- **[🚀 快速入门](docs/getting-started.md)** - 详细的入门教程,包含Laya/Cocos/Node.js集成指南 ⭐ **平台集成必读**
|
- **[🚀 快速入门](docs/getting-started.md)** - 详细的入门教程,包含Laya/Cocos/Node.js集成指南 ⭐ **平台集成必读**
|
||||||
|
- 💡 **Cocos Creator用户特别提示**:我们提供[专用调试插件](https://store.cocos.com/app/detail/7823),支持可视化ECS调试
|
||||||
- [🧠 技术概念详解](docs/concepts-explained.md) - 通俗易懂的技术概念解释 ⭐ **推荐新手阅读**
|
- [🧠 技术概念详解](docs/concepts-explained.md) - 通俗易懂的技术概念解释 ⭐ **推荐新手阅读**
|
||||||
- [🎯 位掩码使用指南](docs/bitmask-guide.md) - 位掩码概念、原理和高级使用技巧
|
- [🎯 位掩码使用指南](docs/bitmask-guide.md) - 位掩码概念、原理和高级使用技巧
|
||||||
- [💡 使用场景示例](docs/use-cases.md) - 不同类型游戏的具体应用案例
|
- [💡 使用场景示例](docs/use-cases.md) - 不同类型游戏的具体应用案例
|
||||||
|
|||||||
@@ -387,16 +387,12 @@ class HealthComponent extends Component {
|
|||||||
this.currentHealth -= damage;
|
this.currentHealth -= damage;
|
||||||
|
|
||||||
// 发送事件,让其他系统响应
|
// 发送事件,让其他系统响应
|
||||||
Core.emitter.emit('health:damaged', {
|
// 注意:需要在实际使用中获取EntityManager实例
|
||||||
entity: this.entity,
|
// 示例:entityManager.eventBus.emit('health:damaged', {...});
|
||||||
damage: damage,
|
|
||||||
remainingHealth: this.currentHealth
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.currentHealth <= 0) {
|
if (this.currentHealth <= 0) {
|
||||||
Core.emitter.emit('health:died', {
|
// 示例:entityManager.eventBus.emit('health:died', {...});
|
||||||
entity: this.entity
|
console.log('实体死亡');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,12 +402,13 @@ class AnimationComponent extends Component {
|
|||||||
onAddedToEntity() {
|
onAddedToEntity() {
|
||||||
super.onAddedToEntity();
|
super.onAddedToEntity();
|
||||||
|
|
||||||
// 监听受伤事件
|
// 监听受伤事件(需要在实际使用中获取EntityManager实例)
|
||||||
Core.emitter.addObserver('health:damaged', this.onDamaged, this);
|
// 示例:entityManager.eventBus.on('health:damaged', this.onDamaged, { context: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemovedFromEntity() {
|
onRemovedFromEntity() {
|
||||||
Core.emitter.removeObserver('health:damaged', this.onDamaged, this);
|
// 事件监听会在组件移除时自动清理
|
||||||
|
// 如需手动清理,保存listenerId并调用eventBus.off()
|
||||||
super.onRemovedFromEntity();
|
super.onRemovedFromEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,28 +19,64 @@ Core 是框架的核心管理类,负责游戏的生命周期管理。
|
|||||||
### 创建和配置
|
### 创建和配置
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Core } from '@esengine/ecs-framework';
|
import { Core, ICoreConfig } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
// 创建核心实例(调试模式)
|
// 创建核心实例(使用配置对象 - 推荐)
|
||||||
const core = Core.create(true);
|
const config: ICoreConfig = {
|
||||||
|
debug: true, // 启用调试模式
|
||||||
|
enableEntitySystems: true, // 启用实体系统
|
||||||
|
debugConfig: { // 可选:远程调试配置
|
||||||
|
enabled: true,
|
||||||
|
websocketUrl: 'ws://localhost:8080',
|
||||||
|
autoReconnect: true,
|
||||||
|
updateInterval: 1000,
|
||||||
|
channels: {
|
||||||
|
entities: true,
|
||||||
|
systems: true,
|
||||||
|
performance: true,
|
||||||
|
components: true,
|
||||||
|
scenes: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const core = Core.create(config);
|
||||||
|
|
||||||
// 创建核心实例(发布模式)
|
// 简化创建(向后兼容)
|
||||||
const core = Core.create(false);
|
const core1 = Core.create(true); // 调试模式
|
||||||
|
const core2 = Core.create(false); // 发布模式
|
||||||
|
const core3 = Core.create(); // 默认调试模式
|
||||||
```
|
```
|
||||||
|
|
||||||
### 事件系统
|
### 事件系统
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { CoreEvents } from '@esengine/ecs-framework';
|
import { EntityManager, ECSEventType } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
// 监听核心事件
|
// 获取EntityManager的事件系统
|
||||||
Core.emitter.addObserver(CoreEvents.frameUpdated, this.onUpdate, this);
|
const entityManager = new EntityManager();
|
||||||
|
const eventBus = entityManager.eventBus;
|
||||||
|
|
||||||
// 发送帧更新事件
|
// 监听实体事件
|
||||||
Core.emitter.emit(CoreEvents.frameUpdated);
|
eventBus.onEntityCreated((data) => {
|
||||||
|
console.log(`实体创建: ${data.entityName}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventBus.onComponentAdded((data) => {
|
||||||
|
console.log(`组件添加: ${data.componentType}`);
|
||||||
|
});
|
||||||
|
|
||||||
// 发送自定义事件
|
// 发送自定义事件
|
||||||
Core.emitter.emit("customEvent", { data: "value" });
|
eventBus.emit("customEvent", { data: "value" });
|
||||||
|
|
||||||
|
// 使用事件装饰器(推荐)
|
||||||
|
import { EventHandler } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
class GameSystem {
|
||||||
|
@EventHandler('entity:died')
|
||||||
|
onEntityDied(data: any) {
|
||||||
|
console.log('实体死亡:', data);
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 定时器系统
|
### 定时器系统
|
||||||
|
|||||||
@@ -29,6 +29,145 @@ Core.update(deltaTime);
|
|||||||
- 更精确的时间控制
|
- 更精确的时间控制
|
||||||
- 统一的API,简化集成
|
- 统一的API,简化集成
|
||||||
|
|
||||||
|
## Core配置
|
||||||
|
|
||||||
|
### 基础配置
|
||||||
|
|
||||||
|
ECS框架提供了灵活的配置选项来满足不同项目需求:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core, ICoreConfig } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// 方式1:简化配置(向后兼容)
|
||||||
|
Core.create(true); // 启用调试模式
|
||||||
|
Core.create(false); // 发布模式
|
||||||
|
Core.create(); // 默认调试模式
|
||||||
|
|
||||||
|
// 方式2:详细配置(推荐)
|
||||||
|
const config: ICoreConfig = {
|
||||||
|
debug: true, // 启用调试模式
|
||||||
|
enableEntitySystems: true, // 启用实体系统(默认true)
|
||||||
|
debugConfig: { // 可选:远程调试配置
|
||||||
|
enabled: true,
|
||||||
|
websocketUrl: 'ws://localhost:8080',
|
||||||
|
autoReconnect: true,
|
||||||
|
updateInterval: 1000, // 调试数据更新间隔(毫秒)
|
||||||
|
channels: { // 调试数据通道
|
||||||
|
entities: true, // 实体信息
|
||||||
|
systems: true, // 系统信息
|
||||||
|
performance: true, // 性能数据
|
||||||
|
components: true, // 组件信息
|
||||||
|
scenes: true // 场景信息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const core = Core.create(config);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调试功能
|
||||||
|
|
||||||
|
ECS框架内置了强大的调试功能,支持运行时监控和远程调试:
|
||||||
|
|
||||||
|
#### Cocos Creator专用调试插件
|
||||||
|
|
||||||
|
**🎯 对于Cocos Creator用户,我们提供了专门的可视化调试插件:**
|
||||||
|
|
||||||
|
- **插件地址**:[cocos-ecs-framework 调试插件](https://store.cocos.com/app/detail/7823)
|
||||||
|
- **插件版本**:v1.0.0
|
||||||
|
- **支持版本**:Cocos Creator v3.0.0+
|
||||||
|
- **支持平台**:Android | iOS | HTML5
|
||||||
|
|
||||||
|
这个插件提供了完整的ECS可视化调试界面,包括实体查看器、组件编辑器、系统监控、性能分析等功能。
|
||||||
|
|
||||||
|
#### 通用调试配置
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 运行时启用调试
|
||||||
|
Core.enableDebug({
|
||||||
|
enabled: true,
|
||||||
|
websocketUrl: 'ws://localhost:8080',
|
||||||
|
autoReconnect: true,
|
||||||
|
updateInterval: 500,
|
||||||
|
channels: {
|
||||||
|
entities: true,
|
||||||
|
systems: true,
|
||||||
|
performance: true,
|
||||||
|
components: false, // 可以选择性禁用某些通道
|
||||||
|
scenes: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取调试数据
|
||||||
|
const debugData = Core.getDebugData();
|
||||||
|
console.log('当前实体数量:', debugData?.entities?.totalEntities);
|
||||||
|
|
||||||
|
// 禁用调试
|
||||||
|
Core.disableDebug();
|
||||||
|
|
||||||
|
// 检查调试状态
|
||||||
|
if (Core.isDebugEnabled) {
|
||||||
|
console.log('调试模式已启用');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境配置建议
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 开发环境 - Cocos Creator
|
||||||
|
const devConfigForCocos: ICoreConfig = {
|
||||||
|
debug: true,
|
||||||
|
enableEntitySystems: true,
|
||||||
|
debugConfig: {
|
||||||
|
enabled: true,
|
||||||
|
websocketUrl: 'ws://localhost:8080', // 连接Cocos插件
|
||||||
|
autoReconnect: true,
|
||||||
|
updateInterval: 1000,
|
||||||
|
channels: {
|
||||||
|
entities: true,
|
||||||
|
systems: true,
|
||||||
|
performance: true,
|
||||||
|
components: true,
|
||||||
|
scenes: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开发环境 - 其他平台
|
||||||
|
const devConfig: ICoreConfig = {
|
||||||
|
debug: true,
|
||||||
|
enableEntitySystems: true,
|
||||||
|
debugConfig: {
|
||||||
|
enabled: true,
|
||||||
|
websocketUrl: 'ws://localhost:8080',
|
||||||
|
autoReconnect: true,
|
||||||
|
updateInterval: 1000,
|
||||||
|
channels: {
|
||||||
|
entities: true,
|
||||||
|
systems: true,
|
||||||
|
performance: true,
|
||||||
|
components: true,
|
||||||
|
scenes: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生产环境
|
||||||
|
const prodConfig: ICoreConfig = {
|
||||||
|
debug: false, // 关闭调试以提升性能
|
||||||
|
enableEntitySystems: true,
|
||||||
|
// debugConfig 可以省略或设为 undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||||
|
Core.create(isDevelopment ? devConfig : prodConfig);
|
||||||
|
```
|
||||||
|
|
||||||
|
**💡 调试功能说明:**
|
||||||
|
- **Cocos Creator**:推荐使用[官方调试插件](https://store.cocos.com/app/detail/7823)获得最佳调试体验
|
||||||
|
- **其他平台**:可以通过WebSocket连接自定义调试工具
|
||||||
|
- **生产环境**:建议关闭调试功能以获得最佳性能
|
||||||
|
|
||||||
## 平台集成
|
## 平台集成
|
||||||
|
|
||||||
### Laya引擎
|
### Laya引擎
|
||||||
@@ -44,8 +183,10 @@ class LayaECSGame extends LayaScene {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// 初始化ECS框架
|
// 初始化ECS框架(简化方式)
|
||||||
Core.create(true);
|
Core.create(true); // 启用调试模式
|
||||||
|
// 完整配置示例: Core.create({ debug: true, enableEntitySystems: true, debugConfig: {...} })
|
||||||
|
|
||||||
this.ecsScene = new ECSScene();
|
this.ecsScene = new ECSScene();
|
||||||
this.ecsScene.name = "LayaGameScene";
|
this.ecsScene.name = "LayaGameScene";
|
||||||
Core.scene = this.ecsScene;
|
Core.scene = this.ecsScene;
|
||||||
@@ -117,8 +258,10 @@ export class ECSGameManager extends CocosComponent {
|
|||||||
private entityManager: EntityManager;
|
private entityManager: EntityManager;
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
// 初始化ECS框架
|
// 初始化ECS框架(简化方式)
|
||||||
Core.create(true);
|
Core.create(true); // 启用调试模式
|
||||||
|
// 完整配置示例: Core.create({ debug: true, enableEntitySystems: true, debugConfig: {...} })
|
||||||
|
|
||||||
this.ecsScene = new ECSScene();
|
this.ecsScene = new ECSScene();
|
||||||
this.ecsScene.name = "CocosGameScene";
|
this.ecsScene.name = "CocosGameScene";
|
||||||
Core.scene = this.ecsScene;
|
Core.scene = this.ecsScene;
|
||||||
@@ -172,6 +315,12 @@ class CocosRenderSystem extends EntitySystem {
|
|||||||
// 将ECSGameManager脚本挂载到场景根节点
|
// 将ECSGameManager脚本挂载到场景根节点
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**🔧 Cocos Creator调试提示:**
|
||||||
|
为了获得最佳的ECS调试体验,建议安装我们的专用调试插件:
|
||||||
|
- 插件地址:[https://store.cocos.com/app/detail/7823](https://store.cocos.com/app/detail/7823)
|
||||||
|
- 支持Cocos Creator v3.0.0+
|
||||||
|
- 提供实体查看器、组件编辑器、系统监控等功能
|
||||||
|
|
||||||
### Node.js后端
|
### Node.js后端
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -185,7 +334,10 @@ class ServerGameManager {
|
|||||||
private lastUpdate: number = Date.now();
|
private lastUpdate: number = Date.now();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
Core.create(true);
|
// 初始化ECS框架(简化方式)
|
||||||
|
Core.create(true); // 启用调试模式
|
||||||
|
// 完整配置示例: Core.create({ debug: true, enableEntitySystems: true, debugConfig: {...} })
|
||||||
|
|
||||||
this.scene = new Scene();
|
this.scene = new Scene();
|
||||||
this.scene.name = "ServerScene";
|
this.scene.name = "ServerScene";
|
||||||
Core.scene = this.scene;
|
Core.scene = this.scene;
|
||||||
@@ -276,7 +428,10 @@ class BrowserGame {
|
|||||||
private entityManager: EntityManager;
|
private entityManager: EntityManager;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
Core.create(true);
|
// 初始化ECS框架(简化方式)
|
||||||
|
Core.create(true); // 启用调试模式
|
||||||
|
// 完整配置示例: Core.create({ debug: true, enableEntitySystems: true, debugConfig: {...} })
|
||||||
|
|
||||||
this.scene = new Scene();
|
this.scene = new Scene();
|
||||||
this.scene.name = "BrowserScene";
|
this.scene.name = "BrowserScene";
|
||||||
Core.scene = this.scene;
|
Core.scene = this.scene;
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ class HealthSystem extends EntitySystem {
|
|||||||
entity.addComponent(new DeadComponent());
|
entity.addComponent(new DeadComponent());
|
||||||
|
|
||||||
// 触发死亡事件
|
// 触发死亡事件
|
||||||
Core.emitter.emit('entity:died', {
|
const eventBus = this.scene.entityManager.eventBus;
|
||||||
|
eventBus.emit('entity:died', {
|
||||||
entityId: entity.id,
|
entityId: entity.id,
|
||||||
entityName: entity.name
|
entityName: entity.name
|
||||||
});
|
});
|
||||||
@@ -235,7 +236,8 @@ class SpawnSystem extends IntervalSystem {
|
|||||||
spawner.lastSpawnTime = Time.totalTime;
|
spawner.lastSpawnTime = Time.totalTime;
|
||||||
|
|
||||||
// 发送生成事件
|
// 发送生成事件
|
||||||
Core.emitter.emit('enemy:spawned', {
|
const eventBus = this.scene.entityManager.eventBus;
|
||||||
|
eventBus.emit('enemy:spawned', {
|
||||||
enemyId: enemy.id,
|
enemyId: enemy.id,
|
||||||
spawnPoint: spawnPoint,
|
spawnPoint: spawnPoint,
|
||||||
spawnerEntity: spawnerEntity.id
|
spawnerEntity: spawnerEntity.id
|
||||||
@@ -270,18 +272,17 @@ class ScoreSystem extends PassiveSystem {
|
|||||||
initialize() {
|
initialize() {
|
||||||
super.initialize();
|
super.initialize();
|
||||||
|
|
||||||
// 监听游戏事件(使用Core.emitter)
|
// 监听游戏事件(使用EntityManager的事件系统)
|
||||||
Core.emitter.addObserver('enemy:killed', this.onEnemyKilled, this);
|
const eventBus = this.scene.entityManager.eventBus;
|
||||||
Core.emitter.addObserver('item:collected', this.onItemCollected, this);
|
eventBus.on('enemy:killed', this.onEnemyKilled, { context: this });
|
||||||
Core.emitter.addObserver('combo:broken', this.onComboBroken, this);
|
eventBus.on('item:collected', this.onItemCollected, { context: this });
|
||||||
|
eventBus.on('combo:broken', this.onComboBroken, { context: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
// PassiveSystem被移除时清理
|
// PassiveSystem被移除时清理
|
||||||
destroy() {
|
destroy() {
|
||||||
// 清理事件监听
|
// 事件监听会在系统销毁时自动清理
|
||||||
Core.emitter.removeObserver('enemy:killed', this.onEnemyKilled, this);
|
// 如需手动清理,可以保存listenerId并调用eventBus.off()
|
||||||
Core.emitter.removeObserver('item:collected', this.onItemCollected, this);
|
|
||||||
Core.emitter.removeObserver('combo:broken', this.onComboBroken, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onEnemyKilled(data: { enemyType: string; position: { x: number; y: number } }) {
|
private onEnemyKilled(data: { enemyType: string; position: { x: number; y: number } }) {
|
||||||
@@ -305,7 +306,8 @@ class ScoreSystem extends PassiveSystem {
|
|||||||
this.score += points;
|
this.score += points;
|
||||||
|
|
||||||
// 发送分数更新事件
|
// 发送分数更新事件
|
||||||
Core.emitter.emit('score:updated', {
|
const eventBus = this.scene.entityManager.eventBus;
|
||||||
|
eventBus.emit('score:updated', {
|
||||||
score: this.score,
|
score: this.score,
|
||||||
points: points,
|
points: points,
|
||||||
multiplier: this.multiplier,
|
multiplier: this.multiplier,
|
||||||
@@ -415,7 +417,8 @@ class CollisionSystem extends EntitySystem {
|
|||||||
|
|
||||||
if (collision) {
|
if (collision) {
|
||||||
// 发送碰撞事件,让其他系统响应
|
// 发送碰撞事件,让其他系统响应
|
||||||
Core.emitter.emit('collision:detected', {
|
const eventBus = this.scene.entityManager.eventBus;
|
||||||
|
eventBus.emit('collision:detected', {
|
||||||
entity1: collider1,
|
entity1: collider1,
|
||||||
entity2: collider2,
|
entity2: collider2,
|
||||||
collisionPoint: point
|
collisionPoint: point
|
||||||
@@ -427,7 +430,8 @@ class CollisionSystem extends EntitySystem {
|
|||||||
class HealthSystem extends PassiveSystem {
|
class HealthSystem extends PassiveSystem {
|
||||||
onAddedToScene() {
|
onAddedToScene() {
|
||||||
// 监听碰撞事件
|
// 监听碰撞事件
|
||||||
Core.emitter.addObserver('collision:detected', this.onCollision, this);
|
const eventBus = this.scene.entityManager.eventBus;
|
||||||
|
eventBus.on('collision:detected', this.onCollision, { context: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
private onCollision(data: CollisionEventData) {
|
private onCollision(data: CollisionEventData) {
|
||||||
|
|||||||
@@ -488,8 +488,9 @@ class LevelTimer {
|
|||||||
|
|
||||||
console.log("⏰ 时间到!游戏结束");
|
console.log("⏰ 时间到!游戏结束");
|
||||||
|
|
||||||
// 触发游戏结束
|
// 触发游戏结束(需要在实际使用中获取EntityManager实例)
|
||||||
Core.emitter.emit('level:timeout');
|
// 示例:entityManager.eventBus.emit('level:timeout');
|
||||||
|
console.log('触发关卡超时事件');
|
||||||
}
|
}
|
||||||
|
|
||||||
completeLevel() {
|
completeLevel() {
|
||||||
@@ -509,7 +510,8 @@ class LevelTimer {
|
|||||||
const bonus = Math.floor(timeLeft * 10); // 每秒剩余10分
|
const bonus = Math.floor(timeLeft * 10); // 每秒剩余10分
|
||||||
if (bonus > 0) {
|
if (bonus > 0) {
|
||||||
console.log(`时间奖励:${bonus} 分`);
|
console.log(`时间奖励:${bonus} 分`);
|
||||||
Core.emitter.emit('score:time_bonus', { bonus });
|
// 触发时间奖励事件(需要在实际使用中获取EntityManager实例)
|
||||||
|
// 示例:entityManager.eventBus.emit('score:time_bonus', { bonus });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function useAppState() {
|
|||||||
|
|
||||||
// UI状态
|
// UI状态
|
||||||
const showExportModal = ref(false);
|
const showExportModal = ref(false);
|
||||||
const exportFormat = ref('json');
|
const exportFormat = ref('json'); // 默认JSON格式,TypeScript暂时禁用
|
||||||
|
|
||||||
// 工具函数
|
// 工具函数
|
||||||
const getNodeByIdLocal = (id: string): TreeNode | undefined => {
|
const getNodeByIdLocal = (id: string): TreeNode | undefined => {
|
||||||
|
|||||||
@@ -159,23 +159,51 @@ export function useBehaviorTreeEditor() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const blackboardData = event.dataTransfer?.getData('application/blackboard-variable');
|
const blackboardData = event.dataTransfer?.getData('application/blackboard-variable');
|
||||||
if (!blackboardData) return;
|
|
||||||
|
if (!blackboardData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const variable = JSON.parse(blackboardData);
|
const variable = JSON.parse(blackboardData);
|
||||||
const activeNode = computedProps.activeNode.value;
|
|
||||||
|
|
||||||
if (!activeNode || !activeNode.properties) return;
|
// 检查当前是否在编辑条件节点
|
||||||
|
if (appState.selectedConditionNodeId.value) {
|
||||||
|
// 条件节点:直接更新装饰器的属性
|
||||||
|
const decoratorNode = appState.getNodeByIdLocal(appState.selectedConditionNodeId.value);
|
||||||
|
if (decoratorNode) {
|
||||||
|
const referenceValue = `{{${variable.name}}}`;
|
||||||
|
|
||||||
const property = activeNode.properties[propertyKey];
|
if (!decoratorNode.properties) {
|
||||||
if (!property) return;
|
decoratorNode.properties = {};
|
||||||
|
}
|
||||||
|
decoratorNode.properties[propertyKey] = referenceValue;
|
||||||
|
|
||||||
// 设置Blackboard引用
|
// 强制触发响应式更新
|
||||||
const referenceValue = `{{${variable.name}}}`;
|
const nodeIndex = appState.treeNodes.value.findIndex(n => n.id === decoratorNode.id);
|
||||||
nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, referenceValue);
|
if (nodeIndex > -1) {
|
||||||
|
const newNodes = [...appState.treeNodes.value];
|
||||||
|
newNodes[nodeIndex] = { ...decoratorNode };
|
||||||
|
appState.treeNodes.value = newNodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 普通节点:使用原来的逻辑
|
||||||
|
const activeNode = computedProps.activeNode.value;
|
||||||
|
|
||||||
// 移除拖拽样式
|
if (!activeNode || !activeNode.properties) {
|
||||||
const element = event.currentTarget as HTMLElement;
|
return;
|
||||||
element.classList.remove('drag-over');
|
}
|
||||||
|
|
||||||
|
const property = activeNode.properties[propertyKey];
|
||||||
|
|
||||||
|
if (!property) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置Blackboard引用
|
||||||
|
const referenceValue = `{{${variable.name}}}`;
|
||||||
|
nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, referenceValue);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('处理Blackboard拖拽失败:', error);
|
console.error('处理Blackboard拖拽失败:', error);
|
||||||
@@ -187,6 +215,7 @@ export function useBehaviorTreeEditor() {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const hasBlackboardData = event.dataTransfer?.types.includes('application/blackboard-variable');
|
const hasBlackboardData = event.dataTransfer?.types.includes('application/blackboard-variable');
|
||||||
|
|
||||||
if (hasBlackboardData) {
|
if (hasBlackboardData) {
|
||||||
event.dataTransfer!.dropEffect = 'copy';
|
event.dataTransfer!.dropEffect = 'copy';
|
||||||
const element = event.currentTarget as HTMLElement;
|
const element = event.currentTarget as HTMLElement;
|
||||||
@@ -200,7 +229,25 @@ export function useBehaviorTreeEditor() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clearBlackboardReference = (propertyKey: string) => {
|
const clearBlackboardReference = (propertyKey: string) => {
|
||||||
nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, '');
|
// 检查当前是否在编辑条件节点
|
||||||
|
if (appState.selectedConditionNodeId.value) {
|
||||||
|
// 条件节点:直接清除装饰器的属性
|
||||||
|
const decoratorNode = appState.getNodeByIdLocal(appState.selectedConditionNodeId.value);
|
||||||
|
if (decoratorNode && decoratorNode.properties) {
|
||||||
|
decoratorNode.properties[propertyKey] = '';
|
||||||
|
|
||||||
|
// 强制触发响应式更新
|
||||||
|
const nodeIndex = appState.treeNodes.value.findIndex(n => n.id === decoratorNode.id);
|
||||||
|
if (nodeIndex > -1) {
|
||||||
|
const newNodes = [...appState.treeNodes.value];
|
||||||
|
newNodes[nodeIndex] = { ...decoratorNode };
|
||||||
|
appState.treeNodes.value = newNodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 普通节点:使用原来的逻辑
|
||||||
|
nodeOps.updateNodeProperty(`properties.${propertyKey}.value`, '');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const startNodeDrag = (event: MouseEvent, node: any) => {
|
const startNodeDrag = (event: MouseEvent, node: any) => {
|
||||||
@@ -461,62 +508,128 @@ export function useBehaviorTreeEditor() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存到文件
|
// 保存到文件 - 使用Cocos Creator扩展API提供保存路径选择
|
||||||
const saveToFile = () => {
|
const saveToFile = async () => {
|
||||||
const code = computedProps.exportedCode();
|
try {
|
||||||
const format = appState.exportFormat.value;
|
const code = computedProps.exportedCode();
|
||||||
const extension = format === 'json' ? '.json' : '.ts';
|
const format = appState.exportFormat.value;
|
||||||
const mimeType = format === 'json' ? 'application/json' : 'text/typescript';
|
const extension = format === 'json' ? '.json' : '.ts';
|
||||||
|
const fileType = format === 'json' ? 'JSON配置文件' : 'TypeScript文件';
|
||||||
|
|
||||||
// 创建文件并下载
|
// 使用Cocos Creator的文件保存对话框
|
||||||
const blob = new Blob([code], { type: mimeType });
|
const result = await Editor.Dialog.save({
|
||||||
const url = URL.createObjectURL(blob);
|
title: `保存${fileType}`,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: fileType,
|
||||||
|
extensions: extension === '.json' ? ['json'] : ['ts']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '所有文件',
|
||||||
|
extensions: ['*']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
const a = document.createElement('a');
|
if (result.canceled || !result.filePath) {
|
||||||
a.href = url;
|
return; // 用户取消了保存
|
||||||
a.download = `behavior_tree_config${extension}`;
|
}
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
|
|
||||||
URL.revokeObjectURL(url);
|
// 写入文件
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
await fs.writeFile(result.filePath, code, 'utf8');
|
||||||
|
|
||||||
// 显示成功消息
|
// 显示成功消息
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
toast.style.cssText = `
|
toast.style.cssText = `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
background: #4caf50;
|
background: #4caf50;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
z-index: 10001;
|
z-index: 10001;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
`;
|
max-width: 400px;
|
||||||
toast.textContent = `文件已保存: behavior_tree_config${extension}`;
|
word-wrap: break-word;
|
||||||
|
`;
|
||||||
|
|
||||||
document.body.appendChild(toast);
|
const path = require('path');
|
||||||
|
const fileName = path.basename(result.filePath);
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div style="font-weight: bold; margin-bottom: 4px;">✅ 文件保存成功</div>
|
||||||
|
<div style="font-size: 12px; opacity: 0.9;">文件名: ${fileName}</div>
|
||||||
|
<div style="font-size: 11px; opacity: 0.7; margin-top: 2px;">路径: ${result.filePath}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
setTimeout(() => {
|
document.body.appendChild(toast);
|
||||||
toast.style.opacity = '1';
|
|
||||||
toast.style.transform = 'translateX(0)';
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.style.opacity = '0';
|
|
||||||
toast.style.transform = 'translateX(100%)';
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (document.body.contains(toast)) {
|
toast.style.opacity = '1';
|
||||||
document.body.removeChild(toast);
|
toast.style.transform = 'translateX(0)';
|
||||||
}
|
}, 10);
|
||||||
}, 300);
|
|
||||||
}, 3000);
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
toast.style.transform = 'translateX(100%)';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(toast)) {
|
||||||
|
document.body.removeChild(toast);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, 4000);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('保存文件失败:', error);
|
||||||
|
|
||||||
|
// 显示错误消息
|
||||||
|
const errorToast = document.createElement('div');
|
||||||
|
errorToast.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: #f56565;
|
||||||
|
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;
|
||||||
|
max-width: 400px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
`;
|
||||||
|
errorToast.innerHTML = `
|
||||||
|
<div style="font-weight: bold; margin-bottom: 4px;">❌ 保存失败</div>
|
||||||
|
<div style="font-size: 12px;">${error?.message || error}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(errorToast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
errorToast.style.opacity = '1';
|
||||||
|
errorToast.style.transform = 'translateX(0)';
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
errorToast.style.opacity = '0';
|
||||||
|
errorToast.style.transform = 'translateX(100%)';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(errorToast)) {
|
||||||
|
document.body.removeChild(errorToast);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 自动检查安装状态
|
// 自动检查安装状态
|
||||||
installation.checkInstallStatus();
|
installation.checkInstallStatus();
|
||||||
@@ -634,23 +747,31 @@ export function useBehaviorTreeEditor() {
|
|||||||
},
|
},
|
||||||
updateNodeProperty: (path: string, value: any) => {
|
updateNodeProperty: (path: string, value: any) => {
|
||||||
if (appState.selectedConditionNodeId.value) {
|
if (appState.selectedConditionNodeId.value) {
|
||||||
|
// 条件节点的属性更新 - 需要同步到装饰器
|
||||||
const decoratorNode = appState.getNodeByIdLocal(appState.selectedConditionNodeId.value);
|
const decoratorNode = appState.getNodeByIdLocal(appState.selectedConditionNodeId.value);
|
||||||
if (decoratorNode) {
|
if (decoratorNode) {
|
||||||
const keys = path.split('.');
|
// 解析路径,例如 "properties.variableName.value" -> "variableName"
|
||||||
let current: any = decoratorNode;
|
const pathParts = path.split('.');
|
||||||
|
if (pathParts[0] === 'properties' && pathParts[2] === 'value') {
|
||||||
|
const propertyName = pathParts[1];
|
||||||
|
|
||||||
for (let i = 0; i < keys.length - 1; i++) {
|
// 直接更新装饰器的属性
|
||||||
const key = keys[i];
|
if (!decoratorNode.properties) {
|
||||||
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
decoratorNode.properties = {};
|
||||||
current[key] = {};
|
|
||||||
}
|
}
|
||||||
current = current[key];
|
decoratorNode.properties[propertyName] = value;
|
||||||
}
|
|
||||||
|
|
||||||
const finalKey = keys[keys.length - 1];
|
// 强制触发响应式更新
|
||||||
current[finalKey] = value;
|
const nodeIndex = appState.treeNodes.value.findIndex(n => n.id === decoratorNode.id);
|
||||||
|
if (nodeIndex > -1) {
|
||||||
|
const newNodes = [...appState.treeNodes.value];
|
||||||
|
newNodes[nodeIndex] = { ...decoratorNode };
|
||||||
|
appState.treeNodes.value = newNodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// 普通节点属性更新
|
||||||
nodeOps.updateNodeProperty(path, value);
|
nodeOps.updateNodeProperty(path, value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -660,9 +781,11 @@ export function useBehaviorTreeEditor() {
|
|||||||
handleDecoratorDragLeave: conditionAttachment.handleDecoratorDragLeave,
|
handleDecoratorDragLeave: conditionAttachment.handleDecoratorDragLeave,
|
||||||
attachConditionToDecorator: conditionAttachment.attachConditionToDecorator,
|
attachConditionToDecorator: conditionAttachment.attachConditionToDecorator,
|
||||||
getConditionDisplayText: conditionAttachment.getConditionDisplayText,
|
getConditionDisplayText: conditionAttachment.getConditionDisplayText,
|
||||||
|
getConditionProperties: conditionAttachment.getConditionProperties,
|
||||||
removeConditionFromDecorator: conditionAttachment.removeConditionFromDecorator,
|
removeConditionFromDecorator: conditionAttachment.removeConditionFromDecorator,
|
||||||
canAcceptCondition: conditionAttachment.canAcceptCondition,
|
canAcceptCondition: conditionAttachment.canAcceptCondition,
|
||||||
resetDragState: conditionAttachment.resetDragState,
|
resetDragState: conditionAttachment.resetDragState,
|
||||||
|
toggleConditionExpanded: conditionAttachment.toggleConditionExpanded,
|
||||||
|
|
||||||
handleCanvasDrop: (event: DragEvent) => {
|
handleCanvasDrop: (event: DragEvent) => {
|
||||||
if (conditionAttachment.handleCanvasDrop(event)) {
|
if (conditionAttachment.handleCanvasDrop(event)) {
|
||||||
|
|||||||
@@ -346,15 +346,27 @@ export function useBlackboard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onVariableDragStart = (event: DragEvent, variable: BlackboardVariable) => {
|
const onVariableDragStart = (event: DragEvent, variable: BlackboardVariable) => {
|
||||||
if (!event.dataTransfer) return;
|
if (!event.dataTransfer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.dataTransfer.setData('application/blackboard-variable', JSON.stringify({
|
const dragData = {
|
||||||
name: variable.name,
|
name: variable.name,
|
||||||
type: variable.type,
|
type: variable.type,
|
||||||
value: variable.value
|
value: variable.value
|
||||||
}));
|
};
|
||||||
|
|
||||||
|
event.dataTransfer.setData('application/blackboard-variable', JSON.stringify(dragData));
|
||||||
event.dataTransfer.effectAllowed = 'copy';
|
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) => {
|
const editVariable = (variable: BlackboardVariable) => {
|
||||||
|
|||||||
@@ -86,18 +86,216 @@ export function useComputedProperties(
|
|||||||
const decoratorNode = treeNodes.value.find(n => n.id === selectedConditionNodeId.value);
|
const decoratorNode = treeNodes.value.find(n => n.id === selectedConditionNodeId.value);
|
||||||
if (!decoratorNode || !decoratorNode.attachedCondition) return null;
|
if (!decoratorNode || !decoratorNode.attachedCondition) return null;
|
||||||
|
|
||||||
|
// 根据条件类型重新构建属性结构
|
||||||
|
const conditionProperties = reconstructConditionProperties(
|
||||||
|
decoratorNode.attachedCondition.type,
|
||||||
|
decoratorNode.properties || {}
|
||||||
|
);
|
||||||
|
|
||||||
// 创建一个虚拟的条件节点对象,用于属性编辑
|
// 创建一个虚拟的条件节点对象,用于属性编辑
|
||||||
return {
|
return {
|
||||||
id: decoratorNode.id + '_condition',
|
id: decoratorNode.id + '_condition',
|
||||||
name: decoratorNode.attachedCondition.name + '(条件)',
|
name: decoratorNode.attachedCondition.name + '(条件)',
|
||||||
type: decoratorNode.attachedCondition.type,
|
type: decoratorNode.attachedCondition.type,
|
||||||
icon: decoratorNode.attachedCondition.icon,
|
icon: decoratorNode.attachedCondition.icon,
|
||||||
properties: decoratorNode.properties || {},
|
properties: conditionProperties,
|
||||||
isConditionNode: true,
|
isConditionNode: true,
|
||||||
parentDecorator: decoratorNode
|
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 activeNode = computed(() => selectedConditionNode.value || selectedNode.value);
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,38 @@ export function useConditionAttachment(
|
|||||||
conditionCode: conditionTemplate.properties?.conditionCode?.value || '(context) => true'
|
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:
|
default:
|
||||||
return baseConfig;
|
return baseConfig;
|
||||||
}
|
}
|
||||||
@@ -177,7 +209,12 @@ export function useConditionAttachment(
|
|||||||
'condition-active': 'isActive',
|
'condition-active': 'isActive',
|
||||||
'condition-numeric': 'numericCompare',
|
'condition-numeric': 'numericCompare',
|
||||||
'condition-property': 'propertyExists',
|
'condition-property': 'propertyExists',
|
||||||
'condition-custom': 'custom'
|
'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';
|
return typeMap[template.type] || 'custom';
|
||||||
@@ -190,27 +227,19 @@ export function useConditionAttachment(
|
|||||||
event: DragEvent,
|
event: DragEvent,
|
||||||
decoratorNode: TreeNode
|
decoratorNode: TreeNode
|
||||||
): boolean => {
|
): boolean => {
|
||||||
console.log('🎯 执行条件吸附:', decoratorNode.name, dragState.conditionTemplate?.name);
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (!dragState.isDraggingCondition || !dragState.conditionTemplate) {
|
if (!dragState.isDraggingCondition || !dragState.conditionTemplate) {
|
||||||
console.log('❌ 拖拽状态无效:', {
|
|
||||||
isDragging: dragState.isDraggingCondition,
|
|
||||||
hasTemplate: !!dragState.conditionTemplate
|
|
||||||
});
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isConditionalDecorator(decoratorNode)) {
|
if (!isConditionalDecorator(decoratorNode)) {
|
||||||
console.log('❌ 不是条件装饰器:', decoratorNode.type);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取条件配置
|
// 获取条件配置
|
||||||
const conditionConfig = mapConditionToDecoratorProperties(dragState.conditionTemplate);
|
const conditionConfig = mapConditionToDecoratorProperties(dragState.conditionTemplate);
|
||||||
console.log('📝 条件配置:', conditionConfig);
|
|
||||||
|
|
||||||
// 更新装饰器属性
|
// 更新装饰器属性
|
||||||
if (!decoratorNode.properties) {
|
if (!decoratorNode.properties) {
|
||||||
@@ -226,7 +255,10 @@ export function useConditionAttachment(
|
|||||||
icon: dragState.conditionTemplate.icon
|
icon: dragState.conditionTemplate.icon
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('✅ 条件吸附成功!', decoratorNode.attachedCondition);
|
// 初始化为收缩状态
|
||||||
|
if (decoratorNode.conditionExpanded === undefined) {
|
||||||
|
decoratorNode.conditionExpanded = false;
|
||||||
|
}
|
||||||
|
|
||||||
// 重置拖拽状态
|
// 重置拖拽状态
|
||||||
resetDragState();
|
resetDragState();
|
||||||
@@ -260,7 +292,6 @@ export function useConditionAttachment(
|
|||||||
* 重置拖拽状态
|
* 重置拖拽状态
|
||||||
*/
|
*/
|
||||||
const resetDragState = () => {
|
const resetDragState = () => {
|
||||||
console.log('🔄 重置拖拽状态');
|
|
||||||
dragState.isDraggingCondition = false;
|
dragState.isDraggingCondition = false;
|
||||||
dragState.conditionTemplate = null;
|
dragState.conditionTemplate = null;
|
||||||
dragState.mousePosition = null;
|
dragState.mousePosition = null;
|
||||||
@@ -268,45 +299,126 @@ export function useConditionAttachment(
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取条件显示文本
|
* 获取条件显示文本(简化版始终显示条件名称)
|
||||||
*/
|
*/
|
||||||
const getConditionDisplayText = (decoratorNode: TreeNode): string => {
|
const getConditionDisplayText = (decoratorNode: TreeNode, expanded: boolean = false): string => {
|
||||||
if (!decoratorNode.attachedCondition || !decoratorNode.properties) {
|
if (!decoratorNode.attachedCondition) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const conditionType = decoratorNode.properties.conditionType;
|
// 始终返回条件名称,不管是否展开
|
||||||
|
return decoratorNode.attachedCondition.name;
|
||||||
|
};
|
||||||
|
|
||||||
switch (conditionType) {
|
/**
|
||||||
case 'random':
|
* 获取条件的可见属性(用于展开时显示)
|
||||||
const probability = decoratorNode.properties.successProbability || 0.5;
|
*/
|
||||||
return `${(probability * 100).toFixed(0)}%概率`;
|
const getConditionProperties = (decoratorNode: TreeNode): Record<string, any> => {
|
||||||
|
if (!decoratorNode.attachedCondition || !decoratorNode.properties) {
|
||||||
case 'hasComponent':
|
return {};
|
||||||
return `有${decoratorNode.properties.componentType || 'Component'}`;
|
|
||||||
|
|
||||||
case 'hasTag':
|
|
||||||
return `标签=${decoratorNode.properties.tagValue || 0}`;
|
|
||||||
|
|
||||||
case 'isActive':
|
|
||||||
const checkHierarchy = decoratorNode.properties.checkHierarchy;
|
|
||||||
return checkHierarchy ? '激活(含层级)' : '激活';
|
|
||||||
|
|
||||||
case 'numericCompare':
|
|
||||||
const path = decoratorNode.properties.propertyPath || 'value';
|
|
||||||
const operator = decoratorNode.properties.compareOperator || '>';
|
|
||||||
const value = decoratorNode.properties.compareValue || 0;
|
|
||||||
return `${path} ${operator} ${value}`;
|
|
||||||
|
|
||||||
case 'propertyExists':
|
|
||||||
return `存在${decoratorNode.properties.propertyPath || 'property'}`;
|
|
||||||
|
|
||||||
case 'custom':
|
|
||||||
return '自定义条件';
|
|
||||||
|
|
||||||
default:
|
|
||||||
return decoratorNode.attachedCondition.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -314,10 +426,34 @@ export function useConditionAttachment(
|
|||||||
*/
|
*/
|
||||||
const removeConditionFromDecorator = (decoratorNode: TreeNode) => {
|
const removeConditionFromDecorator = (decoratorNode: TreeNode) => {
|
||||||
if (decoratorNode.attachedCondition) {
|
if (decoratorNode.attachedCondition) {
|
||||||
|
// 删除附加的条件信息
|
||||||
delete decoratorNode.attachedCondition;
|
delete decoratorNode.attachedCondition;
|
||||||
|
|
||||||
// 完全清空所有属性,回到初始空白状态
|
// 重置展开状态
|
||||||
decoratorNode.properties = {};
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -339,6 +475,8 @@ export function useConditionAttachment(
|
|||||||
getConditionDisplayText,
|
getConditionDisplayText,
|
||||||
removeConditionFromDecorator,
|
removeConditionFromDecorator,
|
||||||
canAcceptCondition,
|
canAcceptCondition,
|
||||||
isConditionalDecorator
|
isConditionalDecorator,
|
||||||
|
toggleConditionExpanded,
|
||||||
|
getConditionProperties
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -135,18 +135,47 @@ export function useNodeOperations(
|
|||||||
|
|
||||||
// 节点属性更新
|
// 节点属性更新
|
||||||
const updateNodeProperty = (path: string, value: any) => {
|
const updateNodeProperty = (path: string, value: any) => {
|
||||||
const node = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
|
const selectedNode = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
|
||||||
if (!node) return;
|
if (!selectedNode) return;
|
||||||
|
|
||||||
// 使用通用方法更新属性
|
// 检查是否是条件节点的属性更新
|
||||||
setNestedProperty(node, path, value);
|
if (selectedNode.isConditionNode && selectedNode.parentDecorator) {
|
||||||
|
// 条件节点的属性更新需要同步到装饰器
|
||||||
|
updateConditionNodeProperty(selectedNode.parentDecorator, path, value);
|
||||||
|
} else {
|
||||||
|
// 普通节点的属性更新
|
||||||
|
setNestedProperty(selectedNode, path, value);
|
||||||
|
|
||||||
// 强制触发响应式更新
|
// 强制触发响应式更新
|
||||||
const nodeIndex = treeNodes.value.findIndex(n => n.id === node.id);
|
const nodeIndex = treeNodes.value.findIndex(n => n.id === selectedNode.id);
|
||||||
if (nodeIndex > -1) {
|
if (nodeIndex > -1) {
|
||||||
const newNodes = [...treeNodes.value];
|
const newNodes = [...treeNodes.value];
|
||||||
newNodes[nodeIndex] = { ...node };
|
newNodes[nodeIndex] = { ...selectedNode };
|
||||||
treeNodes.value = newNodes;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -292,7 +292,45 @@ export const nodeTemplates: NodeTemplate[] = [
|
|||||||
minChildren: 1,
|
minChildren: 1,
|
||||||
className: 'ConditionalDecorator',
|
className: 'ConditionalDecorator',
|
||||||
namespace: 'behaviourTree/decorators',
|
namespace: 'behaviourTree/decorators',
|
||||||
properties: {}
|
properties: {
|
||||||
|
conditionType: {
|
||||||
|
name: '条件类型',
|
||||||
|
type: 'select',
|
||||||
|
value: 'custom',
|
||||||
|
options: ['custom', 'random', 'hasComponent', 'hasTag', 'isActive', 'numericCompare', 'propertyExists'],
|
||||||
|
description: '装饰器使用的条件类型',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
executeWhenTrue: {
|
||||||
|
name: '条件为真时执行',
|
||||||
|
type: 'boolean',
|
||||||
|
value: true,
|
||||||
|
description: '条件为真时是否执行子节点',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
executeWhenFalse: {
|
||||||
|
name: '条件为假时执行',
|
||||||
|
type: 'boolean',
|
||||||
|
value: false,
|
||||||
|
description: '条件为假时是否执行子节点',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
checkInterval: {
|
||||||
|
name: '检查间隔',
|
||||||
|
type: 'number',
|
||||||
|
value: 0,
|
||||||
|
description: '条件检查间隔时间(秒),0表示每帧检查',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
abortType: {
|
||||||
|
name: '中止类型',
|
||||||
|
type: 'select',
|
||||||
|
value: 'None',
|
||||||
|
options: ['None', 'LowerPriority', 'Self', 'Both'],
|
||||||
|
description: '决定节点在何种情况下会被中止',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 动作节点 (Actions) - 叶子节点,不能有子节点
|
// 动作节点 (Actions) - 叶子节点,不能有子节点
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export interface TreeNode {
|
|||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
};
|
};
|
||||||
|
// 条件节点相关(用于虚拟条件节点)
|
||||||
|
isConditionNode?: boolean;
|
||||||
|
parentDecorator?: TreeNode;
|
||||||
|
// 条件显示状态
|
||||||
|
conditionExpanded?: boolean; // 条件是否展开显示详细信息
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Connection {
|
export interface Connection {
|
||||||
|
|||||||
@@ -28,6 +28,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 条件装饰器 - 基础状态 */
|
||||||
|
.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 {
|
.tree-node.can-accept-condition {
|
||||||
border: 2px dashed #ffd700;
|
border: 2px dashed #ffd700;
|
||||||
@@ -51,7 +74,7 @@
|
|||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 条件附加区域 */
|
/* 条件吸附区域 */
|
||||||
.condition-attachment-area {
|
.condition-attachment-area {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -61,84 +84,214 @@
|
|||||||
|
|
||||||
.condition-placeholder {
|
.condition-placeholder {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 12px;
|
padding: 6px 8px;
|
||||||
border: 2px dashed #4a5568;
|
border: 2px dashed #4a5568;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #a0aec0;
|
color: #a0aec0;
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
min-height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node.can-accept-condition .condition-placeholder {
|
.tree-node.can-accept-condition .condition-placeholder {
|
||||||
border-color: #ffd700;
|
border-color: #ffd700;
|
||||||
color: #ffd700;
|
color: #ffd700;
|
||||||
background: rgba(255, 215, 0, 0.05);
|
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 {
|
.attached-condition {
|
||||||
background: rgba(255, 215, 0, 0.1);
|
background: rgba(255, 215, 0, 0.1);
|
||||||
border: 1px solid #ffd700;
|
border: 1px solid #ffd700;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 条件信息区域 */
|
||||||
.condition-info {
|
.condition-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 4px 6px 4px 6px;
|
||||||
|
padding-right: 40px; /* 为右侧两个按钮预留空间 */
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
line-height: 1.3;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.condition-info:hover {
|
.condition-info:hover {
|
||||||
background: rgba(255, 215, 0, 0.2);
|
background: rgba(255, 215, 0, 0.2);
|
||||||
transform: scale(1.02);
|
transform: scale(1.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
.condition-info.condition-selected {
|
.condition-info.condition-selected {
|
||||||
background: rgba(255, 215, 0, 0.3);
|
background: rgba(255, 215, 0, 0.3);
|
||||||
border: 1px solid #ffd700;
|
border: 1px solid #ffd700;
|
||||||
box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.25);
|
box-shadow: 0 0 0 1px rgba(255, 215, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.condition-icon {
|
.condition-icon {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.condition-text {
|
.condition-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
color: #ffd700;
|
color: #ffd700;
|
||||||
font-weight: 500;
|
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 {
|
.edit-hint {
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
color: #a0aec0;
|
color: #a0aec0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.condition-info:hover .edit-hint {
|
.condition-info:hover .edit-hint {
|
||||||
color: #ffd700;
|
color: #ffd700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove-condition-btn {
|
/* 展开/收缩按钮 */
|
||||||
background: none;
|
.toggle-condition-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 20px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
border: none;
|
border: none;
|
||||||
color: #e53e3e;
|
background: #4a5568;
|
||||||
|
color: #a0aec0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
font-size: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
transition: all 0.2s ease;
|
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 {
|
.remove-condition-btn:hover {
|
||||||
background: rgba(229, 62, 62, 0.2);
|
background: #c53030;
|
||||||
transform: scale(1.1);
|
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; /* 允许值换行 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 画布状态 */
|
/* 画布状态 */
|
||||||
@@ -151,9 +304,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 条件装饰器节点的特殊样式 */
|
/* 条件装饰器节点的特殊样式 */
|
||||||
|
.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 {
|
.tree-node.node-conditional-decorator .condition-attachment-area {
|
||||||
border: 1px solid #9f7aea;
|
border: 1px solid #9f7aea;
|
||||||
background: rgba(159, 122, 234, 0.05);
|
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 {
|
.tree-node.node-conditional-decorator.node-selected .condition-attachment-area {
|
||||||
|
|||||||
@@ -56,8 +56,10 @@
|
|||||||
background: #2d3748;
|
background: #2d3748;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6);
|
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6);
|
||||||
max-width: 90vw;
|
max-width: 95vw;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
|
min-width: 800px;
|
||||||
|
width: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: 1px solid #4a5568;
|
border: 1px solid #4a5568;
|
||||||
@@ -138,18 +140,119 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: #2d3748;
|
background: #2d3748;
|
||||||
|
min-width: 750px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 导出选项样式增强 */
|
||||||
.export-options {
|
.export-options {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-options label {
|
.export-options label {
|
||||||
display: block;
|
display: flex;
|
||||||
margin-bottom: 8px;
|
align-items: flex-start;
|
||||||
color: #e2e8f0;
|
gap: 8px;
|
||||||
font-size: 14px;
|
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;
|
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 {
|
.code-output {
|
||||||
@@ -217,22 +320,38 @@
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer .save-btn {
|
.modal-footer .copy-btn {
|
||||||
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%);
|
||||||
|
border: 1px solid #3182ce;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer .save-btn:hover {
|
.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%);
|
background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
|
||||||
|
border: 1px solid #38a169;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer .cancel-btn {
|
.modal-footer .save-file-btn:hover {
|
||||||
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
background: linear-gradient(135deg, #2f855a 0%, #276749 100%);
|
||||||
|
border-color: #2f855a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer .cancel-btn:hover {
|
.modal-footer .close-btn {
|
||||||
background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
|
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模态框特定样式 */
|
||||||
.blackboard-modal {
|
.blackboard-modal {
|
||||||
width: 520px;
|
width: 520px;
|
||||||
@@ -427,5 +546,11 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
resize: none;
|
resize: none;
|
||||||
min-height: 300px;
|
min-height: 400px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
@@ -83,6 +83,15 @@
|
|||||||
overflow: hidden;
|
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 {
|
.node-description {
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
color: #cbd5e0;
|
color: #cbd5e0;
|
||||||
|
|||||||
@@ -564,61 +564,73 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.blackboard-sidebar .variable-item {
|
.blackboard-sidebar .variable-item {
|
||||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
padding: 8px;
|
||||||
border: 1px solid #4a5568;
|
margin: 4px 0;
|
||||||
border-radius: 8px;
|
background: #4a5568;
|
||||||
padding: 12px 16px;
|
border-radius: 4px;
|
||||||
cursor: grab;
|
transition: all 0.2s ease;
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
border-left: 3px solid transparent;
|
||||||
align-items: center;
|
user-select: none;
|
||||||
gap: 12px;
|
|
||||||
border-left-width: 0;
|
|
||||||
min-height: 44px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blackboard-sidebar .variable-item::before {
|
.blackboard-sidebar .variable-item::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px;
|
left: 0;
|
||||||
top: 50%;
|
top: 0;
|
||||||
transform: translateY(-50%);
|
bottom: 0;
|
||||||
width: 8px;
|
width: 3px;
|
||||||
height: 8px;
|
border-radius: 2px 0 0 2px;
|
||||||
border-radius: 50%;
|
transition: all 0.2s ease;
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1), 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blackboard-sidebar .variable-item:hover {
|
|
||||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
|
||||||
border-color: #667eea;
|
|
||||||
transform: translateY(-2px) scale(1.02);
|
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(102, 126, 234, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blackboard-sidebar .variable-item:hover::before {
|
.blackboard-sidebar .variable-item:hover::before {
|
||||||
transform: translateY(-50%) scale(1.2);
|
background: #ffd700;
|
||||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2), 0 0 12px currentColor, 0 4px 8px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blackboard-sidebar .variable-item:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blackboard-sidebar .variable-header {
|
.blackboard-sidebar .variable-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin-left: 20px;
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-drag-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
cursor: grab;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-drag-area:hover {
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-drag-area:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-drag-area .drag-hint {
|
||||||
|
opacity: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #ffd700;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-sidebar .variable-drag-area:hover .drag-hint {
|
||||||
|
opacity: 1;
|
||||||
|
animation: pulse-hint 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blackboard-sidebar .variable-info {
|
.blackboard-sidebar .variable-info {
|
||||||
@@ -921,3 +933,169 @@
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Blackboard 拖拽目标区域样式 */
|
||||||
|
.blackboard-drop-zone {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 2px dashed #4a5568;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(74, 85, 104, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-drop-zone:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-drop-zone.drag-over {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.15);
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-drop-zone.has-reference {
|
||||||
|
border-color: #48bb78;
|
||||||
|
background: rgba(72, 187, 120, 0.1);
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-drop-zone.has-reference:hover {
|
||||||
|
background: rgba(72, 187, 120, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #a0aec0;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-text {
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-drop-zone .drop-placeholder {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-drop-zone:hover .drop-placeholder {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-drop-zone:hover .drop-icon {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-drop-zone.drag-over .drop-placeholder {
|
||||||
|
color: #667eea;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-drop-zone.drag-over .drop-icon {
|
||||||
|
animation: bounce-drop 0.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce-drop {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1.1) translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2) translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框的Blackboard集成样式 */
|
||||||
|
.property-item input.with-blackboard {
|
||||||
|
border-top: 1px solid #4a5568;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-item input.with-blackboard:focus {
|
||||||
|
border-top-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-hint {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: translateY(-50%) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-50%) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce-drop {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1.1) translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2) translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.blackboard-usage-hint {
|
||||||
|
margin: 8px 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
border: 1px solid rgba(102, 126, 234, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
animation: pulse-hint 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-text {
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调试信息样式 */
|
||||||
|
.property-item.debug-info {
|
||||||
|
background: rgba(255, 193, 7, 0.1);
|
||||||
|
border-left: 3px solid #ffc107;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #e83e8c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -287,7 +287,8 @@
|
|||||||
'node-error': node.hasError,
|
'node-error': node.hasError,
|
||||||
'dragging': dragState.dragNode && dragState.dragNode.id === node.id,
|
'dragging': dragState.dragNode && dragState.dragNode.id === node.id,
|
||||||
'condition-hover': conditionDragState.hoveredDecoratorId === node.id,
|
'condition-hover': conditionDragState.hoveredDecoratorId === node.id,
|
||||||
'can-accept-condition': canAcceptCondition(node) && conditionDragState.isDraggingCondition
|
'can-accept-condition': canAcceptCondition(node) && conditionDragState.isDraggingCondition,
|
||||||
|
'has-attached-condition': node.type === 'conditional-decorator' && node.attachedCondition
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
:style="{
|
:style="{
|
||||||
@@ -321,6 +322,25 @@
|
|||||||
<span class="condition-text">{{ getConditionDisplayText(node) }}</span>
|
<span class="condition-text">{{ getConditionDisplayText(node) }}</span>
|
||||||
<span class="edit-hint">📝</span>
|
<span class="edit-hint">📝</span>
|
||||||
</div>
|
</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
|
<button
|
||||||
class="remove-condition-btn"
|
class="remove-condition-btn"
|
||||||
@click.stop="removeConditionFromDecorator(node)"
|
@click.stop="removeConditionFromDecorator(node)"
|
||||||
@@ -440,13 +460,35 @@
|
|||||||
>
|
>
|
||||||
<label>{{ prop.name }}:</label>
|
<label>{{ prop.name }}:</label>
|
||||||
<div class="property-input-container">
|
<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
|
<input
|
||||||
v-if="prop.type === 'string'"
|
v-if="prop.type === 'string'"
|
||||||
type="text"
|
type="text"
|
||||||
:value="prop.value"
|
:value="prop.value"
|
||||||
@input="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
|
@input="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
|
||||||
:key="activeNode.id + '_' + key + '_string'"
|
:key="activeNode.id + '_' + key + '_string'"
|
||||||
:placeholder="'拖拽Blackboard变量或直接输入'"
|
:placeholder="isBlackboardDroppable(prop) ? '或直接输入值' : '请输入值'"
|
||||||
|
:class="{ 'with-blackboard': isBlackboardDroppable(prop) }"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-else-if="prop.type === 'number'"
|
v-else-if="prop.type === 'number'"
|
||||||
@@ -454,7 +496,8 @@
|
|||||||
:value="prop.value"
|
:value="prop.value"
|
||||||
@input="updateNodeProperty('properties.' + key + '.value', parseFloat($event.target.value) || 0)"
|
@input="updateNodeProperty('properties.' + key + '.value', parseFloat($event.target.value) || 0)"
|
||||||
:key="activeNode.id + '_' + key + '_number'"
|
:key="activeNode.id + '_' + key + '_number'"
|
||||||
:placeholder="'拖拽Blackboard变量或直接输入'"
|
:placeholder="isBlackboardDroppable(prop) ? '或直接输入值' : '请输入值'"
|
||||||
|
:class="{ 'with-blackboard': isBlackboardDroppable(prop) }"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-else-if="prop.type === 'boolean'"
|
v-else-if="prop.type === 'boolean'"
|
||||||
@@ -487,12 +530,6 @@
|
|||||||
{{ option }}
|
{{ option }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div v-if="isBlackboardReference(prop.value)" 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>
|
</div>
|
||||||
<p v-if="prop.description" class="property-help">{{ prop.description }}</p>
|
<p v-if="prop.description" class="property-help">{{ prop.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -688,6 +725,14 @@
|
|||||||
<button @click="clearBlackboard" v-if="blackboardVariables.length > 0" class="clear-btn">清空</button>
|
<button @click="clearBlackboard" v-if="blackboardVariables.length > 0" class="clear-btn">清空</button>
|
||||||
</div>
|
</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 class="blackboard-scroll-area">
|
||||||
<div v-if="blackboardVariables.length > 0" class="blackboard-groups">
|
<div v-if="blackboardVariables.length > 0" class="blackboard-groups">
|
||||||
<div
|
<div
|
||||||
@@ -702,16 +747,22 @@
|
|||||||
:key="variable.name"
|
:key="variable.name"
|
||||||
class="variable-item"
|
class="variable-item"
|
||||||
:class="variable.type"
|
:class="variable.type"
|
||||||
:draggable="true"
|
|
||||||
@dragstart="onBlackboardDragStart($event, variable)"
|
|
||||||
:title="variable.description"
|
:title="variable.description"
|
||||||
>
|
>
|
||||||
<div class="variable-header">
|
<div class="variable-header">
|
||||||
<span class="variable-name">{{ variable.name }}</span>
|
<div
|
||||||
<span class="variable-type">{{ getTypeDisplayName(variable.type) }}</span>
|
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">
|
<div class="variable-actions">
|
||||||
<button @click="editBlackboardVariable(variable)" class="edit-btn">✏️</button>
|
<button @click.stop="editBlackboardVariable(variable)" class="edit-btn">✏️</button>
|
||||||
<button @click="removeBlackboardVariable(variable.name)" class="remove-btn">🗑️</button>
|
<button @click.stop="removeBlackboardVariable(variable.name)" class="remove-btn">🗑️</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -720,6 +771,7 @@
|
|||||||
v-if="variable.constraints && variable.constraints.allowedValues"
|
v-if="variable.constraints && variable.constraints.allowedValues"
|
||||||
v-model="variable.value"
|
v-model="variable.value"
|
||||||
@change="onBlackboardValueChange(variable)"
|
@change="onBlackboardValueChange(variable)"
|
||||||
|
@click.stop
|
||||||
:disabled="variable.readOnly"
|
:disabled="variable.readOnly"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
@@ -755,28 +807,51 @@
|
|||||||
<div v-if="showExportModal" class="modal-overlay" @click="showExportModal = false">
|
<div v-if="showExportModal" class="modal-overlay" @click="showExportModal = false">
|
||||||
<div class="modal-content" @click.stop>
|
<div class="modal-content" @click.stop>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3>导出配置</h3>
|
<h3>⚡ 导出行为树配置</h3>
|
||||||
<button @click="showExportModal = false">×</button>
|
<button @click="showExportModal = false">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="export-options">
|
<div class="export-options">
|
||||||
|
<h4 style="margin: 0 0 12px 0; color: #e2e8f0; font-size: 14px;">📄 选择导出格式:</h4>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" v-model="exportFormat" value="json"> JSON配置
|
<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>
|
||||||
<label>
|
<label class="disabled-option">
|
||||||
<input type="radio" v-model="exportFormat" value="typescript"> TypeScript代码
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<div class="preview-section">
|
||||||
class="export-code"
|
<h4 style="margin: 0 0 8px 0; color: #e2e8f0; font-size: 13px;">📋 代码预览:</h4>
|
||||||
:value="exportedCode()"
|
<textarea
|
||||||
readonly
|
class="export-code"
|
||||||
></textarea>
|
: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>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button @click="copyToClipboard">复制到剪贴板</button>
|
<button @click="copyToClipboard" class="copy-btn">📋 复制到剪贴板</button>
|
||||||
<button @click="saveToFile">保存到文件</button>
|
<button @click="saveToFile" class="save-file-btn">💾 保存到文件</button>
|
||||||
<button @click="showExportModal = false">关闭</button>
|
<button @click="showExportModal = false" class="close-btn">❌ 关闭</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
2
thirdparty/BehaviourTree-ai
vendored
2
thirdparty/BehaviourTree-ai
vendored
Submodule thirdparty/BehaviourTree-ai updated: 73c1d77324...93719af820
Reference in New Issue
Block a user