You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
380 lines
12 KiB
380 lines
12 KiB
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<string, Map<string, any>> = new Map<string, Map<string, any>>();
|
|
private _loadTableCount = 0;
|
|
private _bootLoaded = false;
|
|
private _deferredLoaded = false;
|
|
private _loadingTables: MembershipSet<string> = createMembershipSet<string>();
|
|
|
|
/** 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<void> {
|
|
const names = ConfigTableData.BATTLE_TABLES;
|
|
const tasks: Promise<void>[] = [];
|
|
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<void> {
|
|
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<void> {
|
|
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<string, any>) {
|
|
const rows = Array.isArray(json) ? json : Object.values(json);
|
|
const data: Map<string, any> = new Map<string, any>();
|
|
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<string, any>) {
|
|
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<T>(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<T>(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,
|
|
}
|
|
|