|
|
|
|
import type { IComposeConfigProvider } from 'db://assets/mx/module/merge';
|
|
|
|
|
import {
|
|
|
|
|
createMembershipSet,
|
|
|
|
|
membershipAdd,
|
|
|
|
|
membershipHas,
|
|
|
|
|
mapValuesToArray,
|
|
|
|
|
type MembershipSet,
|
|
|
|
|
} from '../../../mx/tools/MembershipSet';
|
|
|
|
|
import { Weapon2TileType } from 'db://assets/mx/module/merge';
|
|
|
|
|
import { TableNames } from '../../game/ConfigTableData';
|
|
|
|
|
import { PEACH_TREE_MAX_LEVEL, PEACH_TREE_WEAPON2_ID } from './BagPrepConfig';
|
|
|
|
|
|
|
|
|
|
/** 归一化 Weapon2 数值字段(抖音/热更 JSON 常见字符串数字) */
|
|
|
|
|
function normalizeWeapon2Row(w: ITableText): ITableText | null {
|
|
|
|
|
const id = Number(w.id);
|
|
|
|
|
if (!Number.isFinite(id)) return null;
|
|
|
|
|
return {
|
|
|
|
|
...w,
|
|
|
|
|
id,
|
|
|
|
|
type: Number(w.type),
|
|
|
|
|
weight: Number(w.weight),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Weapon1 / Weapon2 配表 → MX 合成配置 */
|
|
|
|
|
export class WeaponComposeConfigProvider implements IComposeConfigProvider {
|
|
|
|
|
private static _instance: WeaponComposeConfigProvider | null = null;
|
|
|
|
|
|
|
|
|
|
private _weapon1List: ITableHero[] = [];
|
|
|
|
|
private _weapon2Map = new Map<number, ITableText>();
|
|
|
|
|
private _groupIndex = new Map<string, number>();
|
|
|
|
|
private _maxLevel = new Map<number, number>();
|
|
|
|
|
private _loaded = false;
|
|
|
|
|
|
|
|
|
|
static get instance(): WeaponComposeConfigProvider {
|
|
|
|
|
if (!this._instance) {
|
|
|
|
|
this._instance = new WeaponComposeConfigProvider();
|
|
|
|
|
}
|
|
|
|
|
return this._instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 配表热更后调用,强制下次 load 重读 */
|
|
|
|
|
invalidate(): void {
|
|
|
|
|
this._loaded = false;
|
|
|
|
|
this._weapon2Map.clear();
|
|
|
|
|
this._weapon1List = [];
|
|
|
|
|
this._groupIndex.clear();
|
|
|
|
|
this._maxLevel.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
load(): void {
|
|
|
|
|
if (this._loaded) return;
|
|
|
|
|
|
|
|
|
|
const w1List = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
|
|
|
|
|
const w2List = gg.data.table.getTableList<ITableText>(TableNames.Text) ?? [];
|
|
|
|
|
// Weapon2 是字块 type 的权威来源;H5 可能先到 Weapon1,此时绝不能 _loaded=true 锁死空表
|
|
|
|
|
if (w2List.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._weapon2Map.clear();
|
|
|
|
|
for (const w of w2List) {
|
|
|
|
|
const row = normalizeWeapon2Row(w);
|
|
|
|
|
if (!row) continue;
|
|
|
|
|
this._weapon2Map.set(row.id, row);
|
|
|
|
|
}
|
|
|
|
|
if (this._weapon2Map.size === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._weapon1List = w1List;
|
|
|
|
|
this._groupIndex.clear();
|
|
|
|
|
for (const w1 of this._weapon1List) {
|
|
|
|
|
const group = w1.text;
|
|
|
|
|
if (!Array.isArray(group)) continue;
|
|
|
|
|
const key = group.join(',');
|
|
|
|
|
this._groupIndex.set(key, Number(w1.id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._maxLevel.clear();
|
|
|
|
|
for (const w1 of this._weapon1List) {
|
|
|
|
|
if (Number(w1.level_limit) > 0) {
|
|
|
|
|
this._maxLevel.set(Number(w1.id), Number(w1.level_limit));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._loaded = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Weapon2 解锁章节(新产品无 unlock 字段,默认第 1 章可用) */
|
|
|
|
|
getWeapon2UnlockChapter(_weapon2: ITableText): number {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 背包刷新候选:weight > 0、当前章节已解锁;单字/组合字须属于局外上阵武器的 text,铲不受此限 */
|
|
|
|
|
getWeapon2RefreshCandidates(chapterId: number): ITableText[] {
|
|
|
|
|
this.load();
|
|
|
|
|
const equippedWeapon2Ids = this._getEquippedWeapon2IdSet();
|
|
|
|
|
const result: ITableText[] = [];
|
|
|
|
|
for (const w2 of this._weapon2Map.values()) {
|
|
|
|
|
const w2Type = Number(w2.type);
|
|
|
|
|
const isShovel = w2Type === Weapon2TileType.Shovel;
|
|
|
|
|
if (Number(w2.weight) <= 0) continue;
|
|
|
|
|
if (this.getWeapon2UnlockChapter(w2) > chapterId) continue;
|
|
|
|
|
if (w2Type === Weapon2TileType.Grid) continue;
|
|
|
|
|
if (!isShovel && !membershipHas(equippedWeapon2Ids, Number(w2.id))) continue;
|
|
|
|
|
result.push(w2);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 局外上阵武器 text:基础上阵单字(herotype===1)+ 英雄组合字(herotype===2) */
|
|
|
|
|
private _getEquippedWeapon2IdSet(): MembershipSet<number> {
|
|
|
|
|
const ids = createMembershipSet<number>();
|
|
|
|
|
for (const weapon of gg.data.project.getEquippedWeaponDataList()) {
|
|
|
|
|
for (const weapon2Id of weapon.text ?? []) {
|
|
|
|
|
const id = Number(weapon2Id);
|
|
|
|
|
if (Number.isFinite(id) && id > 0) membershipAdd(ids, id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ids;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getAllWeapon2Configs(): readonly ITableText[] {
|
|
|
|
|
this.load();
|
|
|
|
|
return mapValuesToArray(this._weapon2Map);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getWeapon2Type(weapon2Id: number): number {
|
|
|
|
|
this.load();
|
|
|
|
|
const id = Number(weapon2Id);
|
|
|
|
|
const row = this._weapon2Map.get(id);
|
|
|
|
|
const t = Number(row?.type);
|
|
|
|
|
return Number.isFinite(t) ? t : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getWeapon1Group(weapon1Id: number): readonly number[] {
|
|
|
|
|
this.load();
|
|
|
|
|
const w1 = this._weapon1List.find((w) => Number(w.id) === Number(weapon1Id));
|
|
|
|
|
return w1?.text ?? [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getWeapon1ByGroupSequence(sequence: readonly number[]): number | null {
|
|
|
|
|
this.load();
|
|
|
|
|
const key = sequence.join(',');
|
|
|
|
|
return this._groupIndex.get(key) ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getWeapon2IdForWeapon1(weapon1Id: number): number | null {
|
|
|
|
|
const group = this.getWeapon1Group(weapon1Id);
|
|
|
|
|
return group.length > 0 ? group[0] : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getMaxWeaponLevel(weapon1Id: number): number {
|
|
|
|
|
this.load();
|
|
|
|
|
const id = Number(weapon1Id);
|
|
|
|
|
const cached = this._maxLevel.get(id);
|
|
|
|
|
if (cached != null) return cached;
|
|
|
|
|
const row = this._weapon1List.find((w) => Number(w.id) === id);
|
|
|
|
|
if (!row) {
|
|
|
|
|
console.log('[WeaponCompose] Hero 缺失,等级上限按 1', id);
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
const ht = Math.floor(Number(row.herotype));
|
|
|
|
|
if (ht >= 1 && ht <= 5) {
|
|
|
|
|
const max = this.getElementMaxLevel(ht);
|
|
|
|
|
this._maxLevel.set(id, max);
|
|
|
|
|
return max;
|
|
|
|
|
}
|
|
|
|
|
console.log('[WeaponCompose] Hero.level_limit 未配置,等级上限按 1', id);
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isSingleCellWeapon1(weapon1Id: number): boolean {
|
|
|
|
|
return this.getWeapon1Group(weapon1Id).length === 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 被动 buff 可在 Dig 格产出的 Weapon2(单字/组合字/蟠桃树) */
|
|
|
|
|
allowsPassiveDigSpawn(weapon2Id: number): boolean {
|
|
|
|
|
this.load();
|
|
|
|
|
const id = Number(weapon2Id);
|
|
|
|
|
const w2Type = this.getWeapon2Type(id) as Weapon2TileType;
|
|
|
|
|
if (w2Type === Weapon2TileType.Single || w2Type === Weapon2TileType.Combo) return true;
|
|
|
|
|
return id === PEACH_TREE_WEAPON2_ID;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 蟠桃树(102):空 Dig/Put 可落,落到 Put 后转 Dig,可拖回背包 */
|
|
|
|
|
passiveSpawnKeepsDigCell(weapon2Id: number): boolean {
|
|
|
|
|
this.load();
|
|
|
|
|
return Number(weapon2Id) === PEACH_TREE_WEAPON2_ID;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 五行显示等级上限:同 herotype 的 Hero.level 最大值。
|
|
|
|
|
* @param textType Text.type(1–5 金木水火土)
|
|
|
|
|
*/
|
|
|
|
|
getElementMaxLevel(textType: number): number {
|
|
|
|
|
this.load();
|
|
|
|
|
const type = Math.floor(Number(textType));
|
|
|
|
|
let max = 0;
|
|
|
|
|
for (let i = 0; i < this._weapon1List.length; i++) {
|
|
|
|
|
const hero = this._weapon1List[i];
|
|
|
|
|
if (Math.floor(Number(hero.herotype)) !== type) continue;
|
|
|
|
|
const level = Math.floor(Number(hero.level));
|
|
|
|
|
if (level > max) max = level;
|
|
|
|
|
}
|
|
|
|
|
if (!(max > 0)) {
|
|
|
|
|
console.log('[WeaponCompose] 五行没有等级行', type);
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
return max;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 五行 Hero.id:herotype 与显示等级对齐。
|
|
|
|
|
* @param textType Text.type(1–5)
|
|
|
|
|
* @param level 显示等级
|
|
|
|
|
*/
|
|
|
|
|
getElementHeroId(textType: number, level: number): number | null {
|
|
|
|
|
this.load();
|
|
|
|
|
const type = Math.floor(Number(textType));
|
|
|
|
|
const lv = Math.floor(Number(level));
|
|
|
|
|
if (type < 1 || type > 5 || !(lv > 0)) return null;
|
|
|
|
|
for (let i = 0; i < this._weapon1List.length; i++) {
|
|
|
|
|
const hero = this._weapon1List[i];
|
|
|
|
|
if (Math.floor(Number(hero.herotype)) !== type) continue;
|
|
|
|
|
if (Math.floor(Number(hero.level)) !== lv) continue;
|
|
|
|
|
return Number(hero.id);
|
|
|
|
|
}
|
|
|
|
|
console.log('[WeaponCompose] 无五行英雄', type, lv);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 蟠桃树合成等级上限(读 Hero.level_limit);其它 weapon2 返回 1 */
|
|
|
|
|
getKeepDigTileMaxLevel(weapon2Id: number): number {
|
|
|
|
|
this.load();
|
|
|
|
|
if (!this.passiveSpawnKeepsDigCell(weapon2Id)) return 1;
|
|
|
|
|
const w1 = this.getWeapon1ByGroupSequence([Number(weapon2Id)]);
|
|
|
|
|
if (w1 == null) {
|
|
|
|
|
console.log('[WeaponCompose] 蟠桃树无对应 Hero,等级上限用常量', weapon2Id, PEACH_TREE_MAX_LEVEL);
|
|
|
|
|
return PEACH_TREE_MAX_LEVEL;
|
|
|
|
|
}
|
|
|
|
|
return this.getMaxWeaponLevel(w1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getWeapon2Config(weapon2Id: number): ITableText | null {
|
|
|
|
|
this.load();
|
|
|
|
|
return this._weapon2Map.get(Number(weapon2Id)) ?? null;
|
|
|
|
|
}
|
|
|
|
|
}
|