import { assetManager, JsonAsset, AssetManager } from "cc"; import { Singleton } from "../../mx/tools/Singleton"; import { TableAnomaly } from "./TableAnomaly"; import { LoadRecoveryConfig } from "../../mx/module/res/LoadRecovery"; import { createMembershipSet, membershipAdd, membershipDelete, membershipHas, mapValuesToArray, type MembershipSet, } from "../../mx/tools/MembershipSet"; /** * 配置表名称(新产品 12 张表) */ export enum TableNames { /** 常量表 */ Const = "Const", /** 道具表 */ Item = "Item", /** 段位表 */ Rank = "Rank", /** 怪物表 */ Monster = "Monster", /** 怪物血量难度组表 */ Monster1 = "Monster1", /** 关卡表 */ Level = "Level", /** 基础技能表 */ Skill = "Skill", /** 战斗 buff */ Battlebuff = "Battlebuff", /** 头像 */ Headicon = "Headicon", /** 武器表 */ Weapon = "Weapon", /** 文字表 */ Text = "Text", /** 英雄表 */ Hero = "Hero", /** 省榜每日俸禄 */ Daily_Rank = "Daily_Rank", } /** * 配置表数据 */ export class ConfigTableData extends Singleton { private _tableMap: Map> = new Map>(); private _loadTableCount = 0; private _bootLoaded = false; private _deferredLoaded = false; private _loadingTables: MembershipSet = createMembershipSet(); /** initSdk 前必须就绪的核心表(Const 含刷新消耗等,须尽早可用) */ static readonly BOOT_TABLES: TableNames[] = [ TableNames.Const, TableNames.Item, TableNames.Rank, TableNames.Level, TableNames.Monster, TableNames.Skill, ]; /** 进游戏后后台加载 */ static readonly DEFERRED_TABLES: TableNames[] = Object.values(TableNames).filter( (v) => typeof v === 'string' && !ConfigTableData.BOOT_TABLES.includes(v as TableNames), ) as TableNames[]; /** 开战必须齐的延后表(组装进战包 / 武器 / 字块) */ static readonly BATTLE_TABLES: TableNames[] = [ TableNames.Hero, TableNames.Weapon, TableNames.Text, TableNames.Monster1, TableNames.Battlebuff, ]; loadBootTables(onprogress: Function, callback: Function) { this._loadTableBatch(ConfigTableData.BOOT_TABLES, onprogress, () => { this._bootLoaded = true; this.validateConstLoaded(); TableAnomaly.scan(this); if (callback) callback(); }); } loadDeferredTables(onprogress: Function, callback: Function) { if (this._deferredLoaded) { if (callback) callback(); return; } this._loadTableBatch(ConfigTableData.DEFERRED_TABLES, onprogress, () => { this._deferredLoaded = true; TableAnomaly.scan(this); if (callback) callback(); }); } loadConfig(onprogress: Function, callback: Function) { this._loadTableCount = 0; const all = [...ConfigTableData.BOOT_TABLES, ...ConfigTableData.DEFERRED_TABLES]; this._loadTableBatch(all, onprogress, () => { this._bootLoaded = true; this._deferredLoaded = true; this.validateConstLoaded(); TableAnomaly.scan(this); if (callback) callback(); }); } private _loadTableBatch(tables: TableNames[], onprogress: Function, callback: Function) { let loaded = 0; const max = tables.length; if (max <= 0) { if (callback) callback(); return; } for (const v of tables) { this.loadTable(v, () => { loaded++; if (onprogress) onprogress(loaded, max); if (loaded >= max && callback) callback(); }); } } /** 开战前等齐延后表;未齐时 getTableData 只点火不阻塞,首局会漏资源 */ ensureBattleTables(): Promise { const names = ConfigTableData.BATTLE_TABLES; const tasks: Promise[] = []; for (let i = 0; i < names.length; i++) { tasks.push(this.ensureTableLoaded(names[i])); } return Promise.all(tasks).then(() => undefined); } /** Deferred 表首次访问时按需补载 */ ensureTableLoaded(tableName: TableNames | string): Promise { if (this._tableMap.has(tableName)) return Promise.resolve(); if (ConfigTableData.BOOT_TABLES.includes(tableName as TableNames)) { console.error('[ConfigTableData] Boot 表未就绪:', tableName); return Promise.resolve(); } if (membershipHas(this._loadingTables, tableName)) { return this._waitTableLoaded(tableName); } membershipAdd(this._loadingTables, tableName); return new Promise((resolve) => { let settled = false; const finish = (): void => { if (settled) return; settled = true; membershipDelete(this._loadingTables, tableName); resolve(); }; const timer = setTimeout(() => { console.error('[ConfigTableData] ensureTableLoaded timeout', tableName); finish(); }, LoadRecoveryConfig.prefabTimeoutMs); this.loadTable(tableName, () => { clearTimeout(timer); finish(); }); }); } /** * 等待已在加载中的表;超时结束等待,避免匹配门闩永久挂起。 * @param tableName 表名 */ private _waitTableLoaded(tableName: TableNames | string): Promise { return new Promise((resolve) => { let settled = false; const finish = (): void => { if (settled) return; settled = true; resolve(); }; const timer = setTimeout(() => { console.error('[ConfigTableData] waitTableLoaded timeout', tableName); finish(); }, LoadRecoveryConfig.prefabTimeoutMs); const check = (): void => { if (this._tableMap.has(tableName) || !membershipHas(this._loadingTables, tableName)) { clearTimeout(timer); finish(); return; } setTimeout(check, 16); }; check(); }); } private loadTable(tableName: string, callback?: Function) { const loadFun = (bundle: AssetManager.Bundle) => { if (bundle == null) { if (callback) callback(); } else { const paths = tableName === TableNames.Const ? ['config/ConstConf', 'config/Const', 'config/const'] : [`config/${tableName}`]; this._loadTableJsonPaths(bundle, tableName, paths, 0, callback); } }; const bundle = assetManager.getBundle("first"); if (bundle) { loadFun(bundle); } else { assetManager.loadBundle("first", (err, loadedBundle) => { if (err) { loadFun(null); } else { loadFun(loadedBundle); } }); } } /** 按路径顺序尝试加载配表(兼容旧文件名 ConstConf) */ private _loadTableJsonPaths( bundle: AssetManager.Bundle, tableName: string, paths: string[], index: number, callback?: Function, ): void { if (index >= paths.length) { if (callback) callback(); return; } const path = paths[index]; const cached = bundle.get(path, JsonAsset); if (cached?.json != null) { this.onTableLoaded(tableName, cached.json); if (callback) callback(); return; } bundle.load(path, JsonAsset, (err, asset) => { if (!err && asset?.json != null) { this.onTableLoaded(tableName, asset.json); if (callback) callback(); return; } this._loadTableJsonPaths(bundle, tableName, paths, index + 1, callback); }); } private onTableLoaded(tableName: string, json: any[] | Record) { const rows = Array.isArray(json) ? json : Object.values(json); const data: Map = new Map(); for (let i = 0; i < rows.length; i++) { data.set(rows[i].id + "", rows[i]); } this._tableMap.set(tableName, data); } /** * 用热更新/Bin 解压后的数据覆盖 _tableMap。 * 键为表名字符串(与 TableNames 枚举值、bundle 内 config 文件名一致),值为行对象数组(每行需含 id)。 */ applyHotUpdateTables(tables: Record) { if (!tables || typeof tables !== 'object') return; for (const tableName of Object.keys(tables)) { const json = tables[tableName]; if (!Array.isArray(json)) continue; const key = (tableName === 'Const' || tableName === 'const' || tableName === 'ConstConf') ? TableNames.Const : tableName; this.onTableLoaded(key, json); } TableAnomaly.scan(this); } /** 获取表数据键值对(key 为 id) - 通用 */ getTableMap(tableName: string) { return this._tableMap.get(tableName); } /** 获取表数据列表 - 通用 */ getTableList(tableName: TableNames | string): T[] { const table = this._tableMap.get(tableName); if (!table) { if (!ConfigTableData.BOOT_TABLES.includes(tableName as TableNames)) { void this.ensureTableLoaded(tableName); } return []; } return mapValuesToArray(table) as T[]; } /** 获取表数据(根据 id 获取一条数据) - 通用 */ getTableData(tableName: TableNames | string, id: number): T | null { const table = this._tableMap.get(tableName); if (table) { return table.get(id + "") as T; } if (!ConfigTableData.BOOT_TABLES.includes(tableName as TableNames)) { void this.ensureTableLoaded(tableName); } return null; } /** 获取配置的常量数值 para */ getConst(id: number): number { const d = this._tableMap.get(TableNames.Const)?.get(id + "") as ITableConst | undefined; return d?.para ?? 0; } /** 获取常量数组参数 para1(逗号分隔) */ getConstArray(id: number): number[] { const d = this._tableMap.get(TableNames.Const)?.get(id + "") as ITableConst | undefined; if (!d?.para1) return []; return d.para1.split(',').map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n)); } /** 获取常量字符串参数 para1 */ getConstString(id: number): string { const d = this._tableMap.get(TableNames.Const)?.get(id + "") as ITableConst | undefined; return d?.para1 ?? ''; } /** 获取常量配置行 */ getConstData(id: number): ITableConst | null { return (this._tableMap.get(TableNames.Const)?.get(id + "") as ITableConst) ?? null; } /** 配表加载完成后校验 Const 是否生效 */ validateConstLoaded(): void { const table = this._tableMap.get(TableNames.Const); const rowCount = table?.size ?? 0; if (rowCount === 0) { console.error('[ConfigTableData] Const 未加载,请确认 first 包内存在 config/Const.json'); } } /** 获取道具数据 */ getItemData(itemid: number): ITableItem | null { return (this._tableMap.get(TableNames.Item)?.get(itemid + "") as ITableItem) ?? null; } } /** 道具池权重列表数据结构 */ export interface IItemPoolData { /** 编号 */ id: number; /** 数量 */ num: number; /** 权重 */ weight: number; } /** 技能乘区类型 */ export enum SkillMultiplyType { /** 暴击率 */ CritRate = 1, /** 暴击伤害 */ CritDamage = 2, /** 伤害 */ Damage = 3, /** CD */ CD = 4, /** 范围 */ Range = 5, /** 数量 */ Number = 6, /** 连发 */ Fire = 7, /** 穿透 */ Penetration = 8, /** 伤害倍数1 */ DamageMultiple1 = 9, /** 伤害倍数2 */ DamageMultiple2 = 10, /** 伤害倍数3 */ DamageMultiple3 = 11, }