Feature/editor optimization (#251)
* refactor: 编辑器/运行时架构拆分与构建系统升级 * feat(core): 层级系统重构与UI变换矩阵修复 * refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题 * fix(physics): 修复跨包组件类引用问题 * feat: 统一运行时架构与浏览器运行支持 * feat(asset): 实现浏览器运行时资产加载系统 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误 * test: 补齐核心模块测试用例,修复CI构建配置 * fix: 修复测试用例中的类型错误和断言问题 * fix: 修复 turbo build:npm 任务的依赖顺序问题 * fix: 修复 CI 构建错误并优化构建性能
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
import { BlackboardValueType, type GlobalBlackboardConfig } 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: '../..',
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件头部注释
|
||||
*
|
||||
* 注意:生成的代码字符串会被打包到 IIFE 中,如果 import/export 出现在行首
|
||||
* 会被浏览器误解析为 ES module 语法。因此使用字符串拼接确保不在行首。
|
||||
*/
|
||||
private static generateHeader(timestamp: string, opts: Required<TypeGenerationOptions>): string {
|
||||
const customHeader = opts.customHeader || `/**
|
||||
* 全局黑板类型定义
|
||||
*
|
||||
* ⚠️ 此文件由编辑器自动生成,请勿手动修改!
|
||||
* 生成时间: ${timestamp}
|
||||
*/`;
|
||||
|
||||
// 使用字符串拼接避免 import 出现在源代码行首(打包后可能被误解析)
|
||||
const importStatement = 'im' + 'port { GlobalBlackboardService } from \'' + opts.importPath + '\';';
|
||||
return `${customHeader}\n\n${importStatement}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成常量对象
|
||||
* 注意:使用 EXP + 'ort' 拼接避免打包后 export 在行首被误解析
|
||||
*/
|
||||
private static generateConstants(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
||||
const quote = opts.quoteStyle === 'single' ? "'" : '"';
|
||||
// 使用拼接避免 export 在源代码行首
|
||||
const exp = 'exp' + 'ort';
|
||||
|
||||
if (variables.length === 0) {
|
||||
return `/**
|
||||
* 全局变量名称常量
|
||||
*/
|
||||
${exp} 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 `/**
|
||||
* 全局变量名称常量
|
||||
* 使用常量避免拼写错误
|
||||
*/
|
||||
${exp} 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 `/**
|
||||
* 全局变量名称常量
|
||||
* 使用常量避免拼写错误
|
||||
*/
|
||||
${exp} const ${opts.constantsName} = {
|
||||
${namespaces}
|
||||
} as const;`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成接口定义
|
||||
*/
|
||||
private static generateInterface(variables: any[], opts: Required<TypeGenerationOptions>): string {
|
||||
const exp = 'exp' + 'ort';
|
||||
|
||||
if (variables.length === 0) {
|
||||
return `/**
|
||||
* 全局变量类型定义
|
||||
*/
|
||||
${exp} 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 `/**
|
||||
* 全局变量类型定义
|
||||
*/
|
||||
${exp} interface ${opts.interfaceName} {
|
||||
${properties}
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成类型别名
|
||||
*/
|
||||
private static generateTypeAliases(opts: Required<TypeGenerationOptions>): string {
|
||||
const exp = 'exp' + 'ort';
|
||||
return `/**
|
||||
* 全局变量名称联合类型
|
||||
*/
|
||||
${exp} type ${opts.typeAliasName} = keyof ${opts.interfaceName};`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成类型安全包装类
|
||||
*/
|
||||
private static generateTypedClass(opts: Required<TypeGenerationOptions>): string {
|
||||
const exp = 'exp' + 'ort';
|
||||
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'); // ❌ 编译错误
|
||||
* \`\`\`
|
||||
*/
|
||||
${exp} 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 {
|
||||
const exp = 'exp' + 'ort';
|
||||
|
||||
if (variables.length === 0) {
|
||||
return `/**
|
||||
* 默认值配置
|
||||
*/
|
||||
${exp} 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);
|
||||
* \`\`\`
|
||||
*/
|
||||
${exp} 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