refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216)
* refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 * feat(editor): 添加插件市场功能 * feat(editor): 重构插件市场以支持版本管理和ZIP打包 * feat(editor): 重构插件发布流程并修复React渲染警告 * fix(plugin): 修复插件发布和市场的路径不一致问题 * feat: 重构插件发布流程并添加插件删除功能 * fix(editor): 完善插件删除功能并修复多个关键问题 * fix(auth): 修复自动登录与手动登录的竞态条件问题 * feat(editor): 重构插件管理流程 * feat(editor): 支持 ZIP 文件直接发布插件 - 新增 PluginSourceParser 解析插件源 - 重构发布流程支持文件夹和 ZIP 两种方式 - 优化发布向导 UI * feat(editor): 插件市场支持多版本安装 - 插件解压到项目 plugins 目录 - 新增 Tauri 后端安装/卸载命令 - 支持选择任意版本安装 - 修复打包逻辑,保留完整 dist 目录结构 * feat(editor): 个人中心支持多版本管理 - 合并同一插件的不同版本 - 添加版本历史展开/折叠功能 - 禁止有待审核 PR 时更新插件 * fix(editor): 修复 InspectorRegistry 服务注册 - InspectorRegistry 实现 IService 接口 - 注册到 Core.services 供插件使用 * feat(behavior-tree-editor): 完善插件注册和文件操作 - 添加文件创建模板和操作处理器 - 实现右键菜单创建行为树功能 - 修复文件读取权限问题(使用 Tauri 命令) - 添加 BehaviorTreeEditorPanel 组件 - 修复 rollup 配置支持动态导入 * feat(plugin): 完善插件构建和发布流程 * fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成 * fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能 * fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式 * refactor(behavior-tree-editor): 移除调试面板功能简化代码结构 * refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑 * feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性 * fix(lint): 修复ESLint错误确保CI通过 * refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能 * refactor(behavior-tree-editor): 清理技术债务,优化代码质量 * fix(editor-app): 修复字符串替换安全问题
This commit is contained in:
@@ -0,0 +1,501 @@
|
||||
import { GlobalBlackboardConfig, BlackboardValueType } from '@esengine/behavior-tree';
|
||||
|
||||
/**
|
||||
* 类型生成配置选项
|
||||
*/
|
||||
export interface TypeGenerationOptions {
|
||||
/** 常量名称大小写风格 */
|
||||
constantCase?: 'UPPER_SNAKE' | 'camelCase' | 'PascalCase';
|
||||
|
||||
/** 常量对象名称 */
|
||||
constantsName?: string;
|
||||
|
||||
/** 接口名称 */
|
||||
interfaceName?: string;
|
||||
|
||||
/** 类型别名名称 */
|
||||
typeAliasName?: string;
|
||||
|
||||
/** 包装类名称 */
|
||||
wrapperClassName?: string;
|
||||
|
||||
/** 默认值对象名称 */
|
||||
defaultsName?: string;
|
||||
|
||||
/** 导入路径 */
|
||||
importPath?: string;
|
||||
|
||||
/** 是否生成常量对象 */
|
||||
includeConstants?: boolean;
|
||||
|
||||
/** 是否生成接口 */
|
||||
includeInterface?: boolean;
|
||||
|
||||
/** 是否生成类型别名 */
|
||||
includeTypeAlias?: boolean;
|
||||
|
||||
/** 是否生成包装类 */
|
||||
includeWrapperClass?: boolean;
|
||||
|
||||
/** 是否生成默认值 */
|
||||
includeDefaults?: boolean;
|
||||
|
||||
/** 自定义头部注释 */
|
||||
customHeader?: string;
|
||||
|
||||
/** 使用单引号还是双引号 */
|
||||
quoteStyle?: 'single' | 'double';
|
||||
|
||||
/** 是否在文件末尾添加换行 */
|
||||
trailingNewline?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局黑板 TypeScript 类型生成器
|
||||
*
|
||||
* 将全局黑板配置导出为 TypeScript 类型定义,提供:
|
||||
* - 编译时类型检查
|
||||
* - IDE 自动补全
|
||||
* - 避免拼写错误
|
||||
* - 重构友好
|
||||
*/
|
||||
export class GlobalBlackboardTypeGenerator {
|
||||
/**
|
||||
* 默认生成选项
|
||||
*/
|
||||
static readonly DEFAULT_OPTIONS: Required<TypeGenerationOptions> = {
|
||||
constantCase: 'UPPER_SNAKE',
|
||||
constantsName: 'GlobalVars',
|
||||
interfaceName: 'GlobalBlackboardTypes',
|
||||
typeAliasName: 'GlobalVariableName',
|
||||
wrapperClassName: 'TypedGlobalBlackboard',
|
||||
defaultsName: 'GlobalBlackboardDefaults',
|
||||
importPath: '@esengine/behavior-tree',
|
||||
includeConstants: true,
|
||||
includeInterface: true,
|
||||
includeTypeAlias: true,
|
||||
includeWrapperClass: true,
|
||||
includeDefaults: true,
|
||||
customHeader: '',
|
||||
quoteStyle: 'single',
|
||||
trailingNewline: true
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成 TypeScript 类型定义代码
|
||||
*
|
||||
* @param config 全局黑板配置
|
||||
* @param options 生成选项
|
||||
* @returns TypeScript 代码字符串
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 使用默认选项
|
||||
* const code = GlobalBlackboardTypeGenerator.generate(config);
|
||||
*
|
||||
* // 自定义命名
|
||||
* const code = GlobalBlackboardTypeGenerator.generate(config, {
|
||||
* constantsName: 'GameVars',
|
||||
* wrapperClassName: 'GameBlackboard'
|
||||
* });
|
||||
*
|
||||
* // 只生成接口和类型别名,不生成包装类
|
||||
* const code = GlobalBlackboardTypeGenerator.generate(config, {
|
||||
* includeWrapperClass: false,
|
||||
* includeDefaults: false
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
static generate(config: GlobalBlackboardConfig, options?: TypeGenerationOptions): string {
|
||||
const opts = { ...this.DEFAULT_OPTIONS, ...options };
|
||||
const now = new Date().toLocaleString('zh-CN', { hour12: false });
|
||||
const variables = config.variables || [];
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
// 生成文件头部注释
|
||||
parts.push(this.generateHeader(now, opts));
|
||||
|
||||
// 根据配置生成各个部分
|
||||
if (opts.includeConstants) {
|
||||
parts.push(this.generateConstants(variables, opts));
|
||||
}
|
||||
|
||||
if (opts.includeInterface) {
|
||||
parts.push(this.generateInterface(variables, opts));
|
||||
}
|
||||
|
||||
if (opts.includeTypeAlias) {
|
||||
parts.push(this.generateTypeAliases(opts));
|
||||
}
|
||||
|
||||
if (opts.includeWrapperClass) {
|
||||
parts.push(this.generateTypedClass(opts));
|
||||
}
|
||||
|
||||
if (opts.includeDefaults) {
|
||||
parts.push(this.generateDefaults(variables, opts));
|
||||
}
|
||||
|
||||
// 组合所有部分
|
||||
let code = parts.join('\n\n');
|
||||
|
||||
// 添加文件末尾换行
|
||||
if (opts.trailingNewline && !code.endsWith('\n')) {
|
||||
code += '\n';
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件头部注释
|
||||
*/
|
||||
private static generateHeader(timestamp: string, opts: Required<TypeGenerationOptions>): string {
|
||||
const customHeader = opts.customHeader || `/**
|
||||
* 全局黑板类型定义
|
||||
*
|
||||
* ⚠️ 此文件由编辑器自动生成,请勿手动修改!
|
||||
* 生成时间: ${timestamp}
|
||||
*/`;
|
||||
|
||||
return `${customHeader}
|
||||
|
||||
import { GlobalBlackboardService } from '${opts.importPath}';`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成常量对象
|
||||
*/
|
||||
private static generateConstants(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
||||
const quote = opts.quoteStyle === 'single' ? "'" : '"';
|
||||
|
||||
if (variables.length === 0) {
|
||||
return `/**
|
||||
* 全局变量名称常量
|
||||
*/
|
||||
export const ${opts.constantsName} = {} as const;`;
|
||||
}
|
||||
|
||||
// 按命名空间分组
|
||||
const grouped = this.groupVariablesByNamespace(variables);
|
||||
|
||||
if (Object.keys(grouped).length === 1 && grouped[''] !== undefined) {
|
||||
// 无命名空间,扁平结构
|
||||
const entries = variables
|
||||
.map((v) => ` ${this.transformName(v.name, opts.constantCase)}: ${quote}${v.name}${quote}`)
|
||||
.join(',\n');
|
||||
|
||||
return `/**
|
||||
* 全局变量名称常量
|
||||
* 使用常量避免拼写错误
|
||||
*/
|
||||
export const ${opts.constantsName} = {
|
||||
${entries}
|
||||
} as const;`;
|
||||
} else {
|
||||
// 有命名空间,分组结构
|
||||
const namespaces = Object.entries(grouped)
|
||||
.map(([namespace, vars]) => {
|
||||
if (namespace === '') {
|
||||
// 根级别变量
|
||||
return vars
|
||||
.map((v) => ` ${this.transformName(v.name, opts.constantCase)}: ${quote}${v.name}${quote}`)
|
||||
.join(',\n');
|
||||
} else {
|
||||
// 命名空间变量
|
||||
const nsName = this.toPascalCase(namespace);
|
||||
const entries = vars
|
||||
.map((v) => {
|
||||
const shortName = v.name.substring(namespace.length + 1);
|
||||
return ` ${this.transformName(shortName, opts.constantCase)}: ${quote}${v.name}${quote}`;
|
||||
})
|
||||
.join(',\n');
|
||||
return ` ${nsName}: {\n${entries}\n }`;
|
||||
}
|
||||
})
|
||||
.join(',\n');
|
||||
|
||||
return `/**
|
||||
* 全局变量名称常量
|
||||
* 使用常量避免拼写错误
|
||||
*/
|
||||
export const ${opts.constantsName} = {
|
||||
${namespaces}
|
||||
} as const;`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成接口定义
|
||||
*/
|
||||
private static generateInterface(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
||||
if (variables.length === 0) {
|
||||
return `/**
|
||||
* 全局变量类型定义
|
||||
*/
|
||||
export interface ${opts.interfaceName} {}`;
|
||||
}
|
||||
|
||||
const properties = variables
|
||||
.map((v) => {
|
||||
const tsType = this.mapBlackboardTypeToTS(v.type);
|
||||
const comment = v.description ? ` /** ${v.description} */\n` : '';
|
||||
return `${comment} ${v.name}: ${tsType};`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return `/**
|
||||
* 全局变量类型定义
|
||||
*/
|
||||
export interface ${opts.interfaceName} {
|
||||
${properties}
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成类型别名
|
||||
*/
|
||||
private static generateTypeAliases(opts: Required<TypeGenerationOptions>): string {
|
||||
return `/**
|
||||
* 全局变量名称联合类型
|
||||
*/
|
||||
export type ${opts.typeAliasName} = keyof ${opts.interfaceName};`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成类型安全包装类
|
||||
*/
|
||||
private static generateTypedClass(opts: Required<TypeGenerationOptions>): string {
|
||||
return `/**
|
||||
* 类型安全的全局黑板服务包装器
|
||||
*
|
||||
* @example
|
||||
* \`\`\`typescript
|
||||
* // 游戏运行时使用
|
||||
* const service = core.services.resolve(GlobalBlackboardService);
|
||||
* const gb = new ${opts.wrapperClassName}(service);
|
||||
*
|
||||
* // 类型安全的获取
|
||||
* const hp = gb.getValue('playerHP'); // 类型: number | undefined
|
||||
*
|
||||
* // 类型安全的设置
|
||||
* gb.setValue('playerHP', 100); // ✅ 正确
|
||||
* gb.setValue('playerHP', 'invalid'); // ❌ 编译错误
|
||||
* \`\`\`
|
||||
*/
|
||||
export class ${opts.wrapperClassName} {
|
||||
constructor(private service: GlobalBlackboardService) {}
|
||||
|
||||
/**
|
||||
* 获取全局变量(类型安全)
|
||||
*/
|
||||
getValue<K extends ${opts.typeAliasName}>(
|
||||
name: K
|
||||
): ${opts.interfaceName}[K] | undefined {
|
||||
return this.service.getValue(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全局变量(类型安全)
|
||||
*/
|
||||
setValue<K extends ${opts.typeAliasName}>(
|
||||
name: K,
|
||||
value: ${opts.interfaceName}[K]
|
||||
): boolean {
|
||||
return this.service.setValue(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查全局变量是否存在
|
||||
*/
|
||||
hasVariable(name: ${opts.typeAliasName}): boolean {
|
||||
return this.service.hasVariable(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有变量名
|
||||
*/
|
||||
getVariableNames(): ${opts.typeAliasName}[] {
|
||||
return this.service.getVariableNames() as ${opts.typeAliasName}[];
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成默认值配置
|
||||
*/
|
||||
private static generateDefaults(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
||||
if (variables.length === 0) {
|
||||
return `/**
|
||||
* 默认值配置
|
||||
*/
|
||||
export const ${opts.defaultsName}: ${opts.interfaceName} = {};`;
|
||||
}
|
||||
|
||||
const properties = variables
|
||||
.map((v) => {
|
||||
const value = this.formatValue(v.value, v.type, opts);
|
||||
return ` ${v.name}: ${value}`;
|
||||
})
|
||||
.join(',\n');
|
||||
|
||||
return `/**
|
||||
* 默认值配置
|
||||
*
|
||||
* 可在游戏启动时用于初始化全局黑板
|
||||
*
|
||||
* @example
|
||||
* \`\`\`typescript
|
||||
* // 获取服务
|
||||
* const service = core.services.resolve(GlobalBlackboardService);
|
||||
*
|
||||
* // 初始化配置
|
||||
* const config = {
|
||||
* version: '1.0',
|
||||
* variables: Object.entries(${opts.defaultsName}).map(([name, value]) => ({
|
||||
* name,
|
||||
* type: typeof value as BlackboardValueType,
|
||||
* value
|
||||
* }))
|
||||
* };
|
||||
* service.importConfig(config);
|
||||
* \`\`\`
|
||||
*/
|
||||
export const ${opts.defaultsName}: ${opts.interfaceName} = {
|
||||
${properties}
|
||||
};`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按命名空间分组变量
|
||||
*/
|
||||
private static groupVariablesByNamespace(variables: any[]): Record<string, any[]> {
|
||||
const groups: Record<string, any[]> = { '': [] };
|
||||
|
||||
for (const variable of variables) {
|
||||
const dotIndex = variable.name.indexOf('.');
|
||||
if (dotIndex === -1) {
|
||||
groups['']!.push(variable);
|
||||
} else {
|
||||
const namespace = variable.name.substring(0, dotIndex);
|
||||
if (!groups[namespace]) {
|
||||
groups[namespace] = [];
|
||||
}
|
||||
groups[namespace]!.push(variable);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将变量名转换为常量名(UPPER_SNAKE_CASE)
|
||||
*/
|
||||
private static toConstantName(name: string): string {
|
||||
// player.hp -> PLAYER_HP
|
||||
// playerHP -> PLAYER_HP
|
||||
return name
|
||||
.replace(/\./g, '_')
|
||||
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 PascalCase
|
||||
*/
|
||||
private static toPascalCase(str: string): string {
|
||||
return str
|
||||
.split(/[._-]/)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射黑板类型到 TypeScript 类型
|
||||
*/
|
||||
private static mapBlackboardTypeToTS(type: BlackboardValueType): string {
|
||||
switch (type) {
|
||||
case BlackboardValueType.Number:
|
||||
return 'number';
|
||||
case BlackboardValueType.String:
|
||||
return 'string';
|
||||
case BlackboardValueType.Boolean:
|
||||
return 'boolean';
|
||||
case BlackboardValueType.Vector2:
|
||||
return '{ x: number; y: number }';
|
||||
case BlackboardValueType.Vector3:
|
||||
return '{ x: number; y: number; z: number }';
|
||||
case BlackboardValueType.Object:
|
||||
return 'any';
|
||||
case BlackboardValueType.Array:
|
||||
return 'any[]';
|
||||
default:
|
||||
return 'any';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化值为 TypeScript 字面量
|
||||
*/
|
||||
private static formatValue(value: any, type: BlackboardValueType, opts: Required<TypeGenerationOptions>): string {
|
||||
if (value === null || value === undefined) {
|
||||
return 'undefined';
|
||||
}
|
||||
|
||||
const quote = opts.quoteStyle === 'single' ? "'" : '"';
|
||||
const escapeRegex = opts.quoteStyle === 'single' ? /'/g : /"/g;
|
||||
const escapeChar = opts.quoteStyle === 'single' ? "\\'" : '\\"';
|
||||
|
||||
switch (type) {
|
||||
case BlackboardValueType.String:
|
||||
return `${quote}${value.toString().replace(escapeRegex, escapeChar)}${quote}`;
|
||||
case BlackboardValueType.Number:
|
||||
case BlackboardValueType.Boolean:
|
||||
return String(value);
|
||||
case BlackboardValueType.Vector2:
|
||||
if (typeof value === 'object' && value.x !== undefined && value.y !== undefined) {
|
||||
return `{ x: ${value.x}, y: ${value.y} }`;
|
||||
}
|
||||
return '{ x: 0, y: 0 }';
|
||||
case BlackboardValueType.Vector3:
|
||||
if (typeof value === 'object' && value.x !== undefined && value.y !== undefined && value.z !== undefined) {
|
||||
return `{ x: ${value.x}, y: ${value.y}, z: ${value.z} }`;
|
||||
}
|
||||
return '{ x: 0, y: 0, z: 0 }';
|
||||
case BlackboardValueType.Array:
|
||||
return '[]';
|
||||
case BlackboardValueType.Object:
|
||||
return '{}';
|
||||
default:
|
||||
return 'undefined';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定的大小写风格转换变量名
|
||||
*/
|
||||
private static transformName(name: string, caseStyle: 'UPPER_SNAKE' | 'camelCase' | 'PascalCase'): string {
|
||||
switch (caseStyle) {
|
||||
case 'UPPER_SNAKE':
|
||||
return this.toConstantName(name);
|
||||
case 'camelCase':
|
||||
return this.toCamelCase(name);
|
||||
case 'PascalCase':
|
||||
return this.toPascalCase(name);
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 camelCase
|
||||
*/
|
||||
private static toCamelCase(str: string): string {
|
||||
const parts = str.split(/[._-]/);
|
||||
if (parts.length === 0) return str;
|
||||
return (parts[0] || '').toLowerCase() + parts.slice(1)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* 局部黑板变量信息
|
||||
*/
|
||||
export interface LocalBlackboardVariable {
|
||||
name: string;
|
||||
type: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 局部黑板类型生成配置
|
||||
*/
|
||||
export interface LocalTypeGenerationOptions {
|
||||
/** 行为树名称 */
|
||||
behaviorTreeName: string;
|
||||
|
||||
/** 是否生成常量枚举 */
|
||||
includeConstants?: boolean;
|
||||
|
||||
/** 是否生成默认值 */
|
||||
includeDefaults?: boolean;
|
||||
|
||||
/** 是否生成辅助函数 */
|
||||
includeHelpers?: boolean;
|
||||
|
||||
/** 使用单引号还是双引号 */
|
||||
quoteStyle?: 'single' | 'double';
|
||||
}
|
||||
|
||||
/**
|
||||
* 局部黑板 TypeScript 类型生成器
|
||||
*
|
||||
* 为行为树的局部黑板变量生成类型安全的 TypeScript 定义
|
||||
*/
|
||||
export class LocalBlackboardTypeGenerator {
|
||||
/**
|
||||
* 生成局部黑板的 TypeScript 类型定义
|
||||
*
|
||||
* @param variables 黑板变量列表
|
||||
* @param options 生成配置
|
||||
* @returns TypeScript 代码
|
||||
*/
|
||||
static generate(
|
||||
variables: Record<string, any>,
|
||||
options: LocalTypeGenerationOptions
|
||||
): string {
|
||||
const opts = {
|
||||
includeConstants: true,
|
||||
includeDefaults: true,
|
||||
includeHelpers: true,
|
||||
quoteStyle: 'single' as const,
|
||||
...options
|
||||
};
|
||||
|
||||
const quote = opts.quoteStyle === 'single' ? "'" : '"';
|
||||
const now = new Date().toLocaleString('zh-CN', { hour12: false });
|
||||
const treeName = opts.behaviorTreeName;
|
||||
const interfaceName = `${this.toPascalCase(treeName)}Blackboard`;
|
||||
const constantsName = `${this.toPascalCase(treeName)}Vars`;
|
||||
const defaultsName = `${this.toPascalCase(treeName)}Defaults`;
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
// 文件头部注释
|
||||
parts.push(`/**
|
||||
* 行为树黑板变量类型定义
|
||||
*
|
||||
* 行为树: ${treeName}
|
||||
* ⚠️ 此文件由编辑器自动生成,请勿手动修改!
|
||||
* 生成时间: ${now}
|
||||
*/`);
|
||||
|
||||
const varEntries = Object.entries(variables);
|
||||
|
||||
// 如果没有变量
|
||||
if (varEntries.length === 0) {
|
||||
parts.push(`\n/**
|
||||
* 黑板变量类型定义(空)
|
||||
*/
|
||||
export interface ${interfaceName} {}`);
|
||||
return parts.join('\n') + '\n';
|
||||
}
|
||||
|
||||
// 生成常量枚举
|
||||
if (opts.includeConstants) {
|
||||
const constants = varEntries
|
||||
.map(([name]) => ` ${this.toConstantName(name)}: ${quote}${name}${quote}`)
|
||||
.join(',\n');
|
||||
|
||||
parts.push(`\n/**
|
||||
* 黑板变量名称常量
|
||||
* 使用常量避免拼写错误
|
||||
*
|
||||
* @example
|
||||
* \`\`\`typescript
|
||||
* // 使用常量代替字符串
|
||||
* const hp = blackboard.getValue(${constantsName}.PLAYER_HP); // ✅ 类型安全
|
||||
* const hp = blackboard.getValue('playerHP'); // ❌ 容易拼写错误
|
||||
* \`\`\`
|
||||
*/
|
||||
export const ${constantsName} = {
|
||||
${constants}
|
||||
} as const;`);
|
||||
}
|
||||
|
||||
// 生成类型接口
|
||||
const interfaceProps = varEntries
|
||||
.map(([name, value]) => {
|
||||
const tsType = this.inferType(value);
|
||||
return ` ${name}: ${tsType};`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
parts.push(`\n/**
|
||||
* 黑板变量类型定义
|
||||
*/
|
||||
export interface ${interfaceName} {
|
||||
${interfaceProps}
|
||||
}`);
|
||||
|
||||
// 生成变量名联合类型
|
||||
parts.push(`\n/**
|
||||
* 黑板变量名称联合类型
|
||||
*/
|
||||
export type ${this.toPascalCase(treeName)}VariableName = keyof ${interfaceName};`);
|
||||
|
||||
// 生成默认值
|
||||
if (opts.includeDefaults) {
|
||||
const defaultProps = varEntries
|
||||
.map(([name, value]) => {
|
||||
const formattedValue = this.formatValue(value, opts.quoteStyle);
|
||||
return ` ${name}: ${formattedValue}`;
|
||||
})
|
||||
.join(',\n');
|
||||
|
||||
parts.push(`\n/**
|
||||
* 黑板变量默认值
|
||||
*
|
||||
* 可用于初始化行为树黑板
|
||||
*
|
||||
* @example
|
||||
* \`\`\`typescript
|
||||
* // 创建行为树时使用默认值
|
||||
* const blackboard = { ...${defaultsName} };
|
||||
* const tree = new BehaviorTree(rootNode, blackboard);
|
||||
* \`\`\`
|
||||
*/
|
||||
export const ${defaultsName}: ${interfaceName} = {
|
||||
${defaultProps}
|
||||
};`);
|
||||
}
|
||||
|
||||
// 生成辅助函数
|
||||
if (opts.includeHelpers) {
|
||||
parts.push(`\n/**
|
||||
* 创建类型安全的黑板访问器
|
||||
*
|
||||
* @example
|
||||
* \`\`\`typescript
|
||||
* const blackboard = create${this.toPascalCase(treeName)}Blackboard();
|
||||
*
|
||||
* // 类型安全的访问
|
||||
* const hp = blackboard.playerHP; // 类型: number
|
||||
* blackboard.playerHP = 100; // ✅ 正确
|
||||
* blackboard.playerHP = 'invalid'; // ❌ 编译错误
|
||||
* \`\`\`
|
||||
*/
|
||||
export function create${this.toPascalCase(treeName)}Blackboard(
|
||||
initialValues?: Partial<${interfaceName}>
|
||||
): ${interfaceName} {
|
||||
return { ...${defaultsName}, ...initialValues };
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型守卫:检查变量名是否有效
|
||||
*/
|
||||
export function is${this.toPascalCase(treeName)}Variable(
|
||||
name: string
|
||||
): name is ${this.toPascalCase(treeName)}VariableName {
|
||||
return name in ${defaultsName};
|
||||
}`);
|
||||
}
|
||||
|
||||
return parts.join('\n') + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断 TypeScript 类型
|
||||
*/
|
||||
private static inferType(value: any): string {
|
||||
if (value === null || value === undefined) {
|
||||
return 'any';
|
||||
}
|
||||
|
||||
const type = typeof value;
|
||||
|
||||
switch (type) {
|
||||
case 'number':
|
||||
return 'number';
|
||||
case 'string':
|
||||
return 'string';
|
||||
case 'boolean':
|
||||
return 'boolean';
|
||||
case 'object':
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return 'any[]';
|
||||
}
|
||||
const elementType = this.inferType(value[0]);
|
||||
return `${elementType}[]`;
|
||||
}
|
||||
// 检查是否是 Vector2 或 Vector3
|
||||
if ('x' in value && 'y' in value) {
|
||||
if ('z' in value) {
|
||||
return '{ x: number; y: number; z: number }';
|
||||
}
|
||||
return '{ x: number; y: number }';
|
||||
}
|
||||
return 'any';
|
||||
default:
|
||||
return 'any';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化值为 TypeScript 字面量
|
||||
*/
|
||||
private static formatValue(value: any, quoteStyle: 'single' | 'double'): string {
|
||||
if (value === null) {
|
||||
return 'null';
|
||||
}
|
||||
if (value === undefined) {
|
||||
return 'undefined';
|
||||
}
|
||||
|
||||
const quote = quoteStyle === 'single' ? "'" : '"';
|
||||
const type = typeof value;
|
||||
|
||||
switch (type) {
|
||||
case 'string':
|
||||
const escaped = value
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(quoteStyle === 'single' ? /'/g : /"/g,
|
||||
quoteStyle === 'single' ? "\\'" : '\\"');
|
||||
return `${quote}${escaped}${quote}`;
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
return String(value);
|
||||
case 'object':
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return '[]';
|
||||
}
|
||||
const items = value.map((v) => this.formatValue(v, quoteStyle)).join(', ');
|
||||
return `[${items}]`;
|
||||
}
|
||||
// Vector2/Vector3
|
||||
if ('x' in value && 'y' in value) {
|
||||
if ('z' in value) {
|
||||
return `{ x: ${value.x}, y: ${value.y}, z: ${value.z} }`;
|
||||
}
|
||||
return `{ x: ${value.x}, y: ${value.y} }`;
|
||||
}
|
||||
// 普通对象
|
||||
return '{}';
|
||||
default:
|
||||
return 'undefined';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 UPPER_SNAKE_CASE
|
||||
*/
|
||||
private static toConstantName(name: string): string {
|
||||
return name
|
||||
.replace(/\./g, '_')
|
||||
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 PascalCase
|
||||
*/
|
||||
private static toPascalCase(str: string): string {
|
||||
return str
|
||||
.split(/[._-]/)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user