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.
AdLike2048/assets/script/game/ConfigTableData.ts

381 lines
12 KiB

3 weeks ago
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,
}