import { Singleton } from '../../mx/tools/Singleton'; import { TableNames } from '../game/ConfigTableData'; import { GEM_SLOT_COUNT, IElementGemSave, IGemBagEntry, IGemFuseGain, OtherDataType, } from '../game/GameDataEnums'; const LOG = '[GemDataManager]'; /** 五行数量 */ const ELEMENT_COUNT = 5; /** * 宝石存档与规则:Item 库存 + ElementGem 镶嵌。 * 可用数 = 背包 − 已镶嵌;合成只消耗可用数。 */ export class GemDataManager extends Singleton { /** 确保 Weapon / Item 已加载 */ async ensureTable(): Promise { await Promise.all([ gg.data.table.ensureTableLoaded(TableNames.Weapon), gg.data.table.ensureTableLoaded(TableNames.Item), ]); this._stripIllegalWears(); } /** 读取宝石配表 */ getGem(gemId: number): ITableWeapon | null { const n = Math.floor(Number(gemId) || 0); if (n <= 0) return null; return gg.data.table.getTableData(TableNames.Weapon, n) ?? null; } /** 全量宝石配表 */ getAllGemList(): ITableWeapon[] { return gg.data.table.getTableList(TableNames.Weapon) ?? []; } /** 宝石道具 id(Weapon.itemid) */ getItemId(gemId: number): number { return Math.floor(Number(this.getGem(gemId)?.itemid) || 0); } /** 背包库存 */ getOwnCount(gemId: number): number { const itemId = this.getItemId(gemId); if (itemId <= 0) return 0; return Math.max(0, gg.data.getItemNum(itemId) | 0); } /** 已镶嵌数量 */ getWornCount(gemId: number): number { const id = Math.floor(Number(gemId) || 0); if (id <= 0) return 0; let n = 0; for (let e = 1; e <= ELEMENT_COUNT; e++) { const slots = this.getElementSlots(e); for (let s = 0; s < slots.length; s++) { if (slots[s] === id) n++; } } return n; } /** 可用数 = 库存 − 已镶嵌 */ getFreeCount(gemId: number): number { return Math.max(0, this.getOwnCount(gemId) - this.getWornCount(gemId)); } /** * 仓库列表。 * @param filterType 0 全部;1 通用;2~6 金木水火土 */ getBagList(filterType: number): IGemBagEntry[] { const filter = Math.floor(Number(filterType) || 0); const list: IGemBagEntry[] = []; const all = this.getAllGemList(); for (let i = 0; i < all.length; i++) { const conf = all[i]; const gemId = Math.floor(Number(conf?.id) || 0); if (gemId <= 0) continue; if (filter > 0 && Math.floor(Number(conf.type) || 0) !== filter) continue; const free = this.getFreeCount(gemId); if (free <= 0) continue; list.push({ gemId, count: free }); } list.sort((a, b) => this._compareBag(a, b)); return list; } /** 五行 4 槽宝石 id(长度固定 4,空为 0) */ getElementSlots(element: number): number[] { const e = Math.floor(Number(element) || 0); const out = [0, 0, 0, 0]; if (e < 1 || e > ELEMENT_COUNT) return out; const row = this._loadMap()[e]; if (!row) return out; for (let i = 0; i < GEM_SLOT_COUNT; i++) { out[i] = Math.max(0, Math.floor(Number(row[i]) || 0)); } return out; } /** 某槽宝石 id */ getSlotGem(element: number, slot: number): number { const s = Math.floor(Number(slot) || 0); if (s < 0 || s >= GEM_SLOT_COUNT) return 0; return this.getElementSlots(element)[s] || 0; } /** * 能否镶嵌(含库存;同槽替换自身不额外占库存;替换/交换时排除目标槽旧宝石的专属占用)。 */ canWear(element: number, slot: number, gemId: number): boolean { const e = Math.floor(Number(element) || 0); const s = Math.floor(Number(slot) || 0); const id = Math.floor(Number(gemId) || 0); if (e < 1 || e > ELEMENT_COUNT || s < 0 || s >= GEM_SLOT_COUNT) return false; if (id <= 0) return true; const conf = this.getGem(id); if (!conf) { console.warn(LOG, '宝石配表缺失', id); return false; } if (!this._canWearType(e, conf, s)) return false; const cur = this.getSlotGem(e, s); if (cur === id) return true; if (this.getFreeCount(id) <= 0) return false; return true; } /** 镶嵌或清空(gemId=0 卸下) */ wear(element: number, slot: number, gemId: number): boolean { const e = Math.floor(Number(element) || 0); const s = Math.floor(Number(slot) || 0); const id = Math.floor(Number(gemId) || 0); if (e < 1 || e > ELEMENT_COUNT || s < 0 || s >= GEM_SLOT_COUNT) return false; if (id > 0 && !this.canWear(e, s, id)) return false; const map = this._loadMap(); const slots = map[e] ? map[e].slice() : [0, 0, 0, 0]; while (slots.length < GEM_SLOT_COUNT) slots.push(0); slots[s] = id; map[e] = slots; this._saveMap(map); return true; } /** 卸下 */ unequip(element: number, slot: number): boolean { return this.wear(element, slot, 0); } /** * 仓库宝石装到槽:目标有宝石则卸旧装新(旧回仓库)。 */ wearFromBag(element: number, slot: number, gemId: number): boolean { return this.wear(element, slot, gemId); } /** * 两槽互换;目标空则等同移动。 */ swapSlots(fromElement: number, fromSlot: number, toElement: number, toSlot: number): boolean { const fe = Math.floor(Number(fromElement) || 0); const fs = Math.floor(Number(fromSlot) || 0); const te = Math.floor(Number(toElement) || 0); const ts = Math.floor(Number(toSlot) || 0); if (fe < 1 || fe > ELEMENT_COUNT || te < 1 || te > ELEMENT_COUNT) return false; if (fs < 0 || fs >= GEM_SLOT_COUNT || ts < 0 || ts >= GEM_SLOT_COUNT) return false; if (fe === te && fs === ts) return false; const fromId = this.getSlotGem(fe, fs); if (fromId <= 0) return false; const toId = this.getSlotGem(te, ts); const map = this._loadMap(); const a = this._slotsOf(map, fe); const b = fe === te ? a : this._slotsOf(map, te); a[fs] = 0; b[ts] = 0; map[fe] = a; map[te] = b; this._saveMap(map); const ok = this.canWear(te, ts, fromId) && (toId <= 0 || this.canWear(fe, fs, toId)); if (!ok) { a[fs] = fromId; b[ts] = toId; map[fe] = a; map[te] = b; this._saveMap(map); return false; } b[ts] = fromId; a[fs] = toId; map[fe] = a; map[te] = b; this._saveMap(map); return true; } /** 是否有可用宝石达到合成门槛(free ≥ synthesisnumber 且有目标) */ canFuseAny(): boolean { const all = this.getAllGemList(); for (let i = 0; i < all.length; i++) { const conf = all[i]; const fromId = Math.floor(Number(conf?.id) || 0); if (fromId <= 0) continue; const need = Math.floor(Number(conf.synthesisnumber) || 0); const targets = conf.synthesisitem ?? []; if (need <= 0 || !targets.length) continue; if (this.getFreeCount(fromId) >= need) return true; } return false; } /** * 多轮一键合成;返回净增可用数 >0 的宝石。 */ fuseAll(): IGemFuseGain[] { const before: { [id: number]: number } = {}; this._snapshotFree(before); let fused = false; let guard = 0; while (guard < 64) { guard++; if (!this._fuseOneRound()) break; fused = true; } if (guard >= 64) console.warn(LOG, '合成轮次达上限'); if (fused) gg.data.saveLocal(); const after: { [id: number]: number } = {}; this._snapshotFree(after); const gains: IGemFuseGain[] = []; for (const key in after) { if (!Object.prototype.hasOwnProperty.call(after, key)) continue; const id = Math.floor(Number(key) || 0); if (id <= 0) continue; const delta = (after[id] | 0) - (before[id] | 0); if (delta > 0) gains.push({ gemId: id, count: delta }); } gains.sort((a, b) => a.gemId - b.gemId); return gains; } /** 五行皮肤名 */ elementSkinName(element: number): string { switch (Math.floor(Number(element) || 0)) { case 1: return '金'; case 2: return '木'; case 3: return '水'; case 4: return '火'; case 5: return '土'; default: return ''; } } /** 全量镶嵌列表(局内预留) */ getAllElementGems(): IElementGemSave[] { const list: IElementGemSave[] = []; for (let e = 1; e <= ELEMENT_COUNT; e++) { list.push({ element: e, slots: this.getElementSlots(e) }); } return list; } /** * GM:Weapon 全表每颗宝石再发 1 颗(Item 库存)。每次点击叠加一套。 * @returns 本次发放的宝石种数 */ async gmGrantAllGems(): Promise { await this.ensureTable(); const list = this.getAllGemList(); if (!list.length) { console.warn(LOG, 'GM 一键发宝石:Weapon 表为空或未加载'); return 0; } let added = 0; for (let i = 0; i < list.length; i++) { const id = Math.floor(Number(list[i]?.id) || 0); const itemId = Math.floor(Number(list[i]?.itemid) || 0); if (id <= 0 || itemId <= 0) continue; gg.data.addItemNum(itemId, 1); added++; } return added; } /** 类型 + 专属上限(slot 上原宝石不计入专属占用) */ private _canWearType(element: number, conf: ITableWeapon, slot: number): boolean { const t = Math.floor(Number(conf.type) || 0); if (t === 1) return true; const needType = element + 1; if (t !== needType) return false; const slots = this.getElementSlots(element); for (let i = 0; i < slots.length; i++) { if (i === slot) continue; const other = this.getGem(slots[i]); if (!other) continue; const ot = Math.floor(Number(other.type) || 0); if (ot >= 2 && ot <= 6) return false; } return true; } /** 一轮:按快照合成 */ private _fuseOneRound(): boolean { const snap: { [id: number]: number } = {}; this._snapshotFree(snap); const ops: { from: number; to: number; times: number }[] = []; const all = this.getAllGemList(); for (let i = 0; i < all.length; i++) { const conf = all[i]; const fromId = Math.floor(Number(conf?.id) || 0); if (fromId <= 0) continue; const need = Math.floor(Number(conf.synthesisnumber) || 0); const targets = conf.synthesisitem ?? []; if (need <= 0 || !targets.length) continue; const free = snap[fromId] | 0; const times = Math.floor(free / need); if (times <= 0) continue; for (let n = 0; n < times; n++) { const toId = this._pickTarget(targets); if (toId <= 0) { console.warn(LOG, '合成目标无效', fromId, targets); continue; } ops.push({ from: fromId, to: toId, times: 1 }); } snap[fromId] = free - times * need; } if (!ops.length) return false; let done = 0; for (let i = 0; i < ops.length; i++) { const op = ops[i]; const itemFrom = this.getItemId(op.from); const itemTo = this.getItemId(op.to); const need = Math.floor(Number(this.getGem(op.from)?.synthesisnumber) || 0); if (itemFrom <= 0 || itemTo <= 0 || need <= 0) { console.warn(LOG, '合成道具缺失', op.from, op.to); continue; } if (gg.data.getItemNum(itemFrom) < need) { console.warn(LOG, '合成材料不足', op.from); continue; } gg.data.subItemNum(itemFrom, need); gg.data.addItemNum(itemTo, 1); done++; } return done > 0; } /** 多目标随机取一 */ private _pickTarget(targets: readonly number[]): number { const list: number[] = []; for (let i = 0; i < targets.length; i++) { const id = Math.floor(Number(targets[i]) || 0); if (id > 0) list.push(id); } if (!list.length) return 0; if (list.length === 1) return list[0]; return list[Math.floor(Math.random() * list.length)]; } /** 快照可用数(含配表全部 id,便于算净增) */ private _snapshotFree(out: { [id: number]: number }): void { const all = this.getAllGemList(); for (let i = 0; i < all.length; i++) { const id = Math.floor(Number(all[i]?.id) || 0); if (id <= 0) continue; const free = this.getFreeCount(id); if (free > 0) out[id] = free; } } /** 仓库排序:品质降序,id 升序 */ private _compareBag(a: IGemBagEntry, b: IGemBagEntry): number { const ca = this.getGem(a.gemId); const cb = this.getGem(b.gemId); const qa = Math.floor(Number(ca?.quality) || 0); const qb = Math.floor(Number(cb?.quality) || 0); if (qa !== qb) return qb - qa; return a.gemId - b.gemId; } /** 卸掉非法镶嵌 */ private _stripIllegalWears(): void { let dirty = false; const map = this._loadMap(); for (let e = 1; e <= ELEMENT_COUNT; e++) { const slots = map[e] ? map[e].slice() : [0, 0, 0, 0]; while (slots.length < GEM_SLOT_COUNT) slots.push(0); let exclusive = 0; for (let s = 0; s < GEM_SLOT_COUNT; s++) { const id = slots[s]; if (id <= 0) continue; const conf = this.getGem(id); if (!conf) { console.warn(LOG, '卸下无效宝石', e, s, id); slots[s] = 0; dirty = true; continue; } const t = Math.floor(Number(conf.type) || 0); if (t === 1) continue; if (t !== e + 1) { console.warn(LOG, '卸下五行不匹配宝石', e, s, id, t); slots[s] = 0; dirty = true; continue; } exclusive++; if (exclusive > 1) { console.warn(LOG, '卸下多余专属宝石', e, s, id); slots[s] = 0; exclusive--; dirty = true; } } map[e] = slots; } if (dirty) this._saveMap(map); } /** 取某五行槽位数组副本(长度 4) */ private _slotsOf(map: { [element: number]: number[] }, element: number): number[] { const src = map[element]; const slots = src ? src.slice() : [0, 0, 0, 0]; while (slots.length < GEM_SLOT_COUNT) slots.push(0); return slots; } /** 解析存档 → element → slots */ private _loadMap(): { [element: number]: number[] } { const map: { [element: number]: number[] } = {}; const raw = gg.data.getStringData(OtherDataType.ElementGem); if (!raw) return map; const parts = raw.split('#'); for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (!part) continue; const sep = part.indexOf('|'); if (sep < 0) continue; const element = Math.floor(Number(part.substring(0, sep)) || 0); if (element < 1 || element > ELEMENT_COUNT) continue; const slotStr = part.substring(sep + 1); const nums = slotStr.split(','); const slots = [0, 0, 0, 0]; for (let s = 0; s < GEM_SLOT_COUNT; s++) { slots[s] = Math.max(0, Math.floor(Number(nums[s]) || 0)); } map[element] = slots; } return map; } /** 写回存档 */ private _saveMap(map: { [element: number]: number[] }): void { const parts: string[] = []; for (let e = 1; e <= ELEMENT_COUNT; e++) { const slots = map[e] ?? [0, 0, 0, 0]; const a = Math.max(0, slots[0] | 0); const b = Math.max(0, slots[1] | 0); const c = Math.max(0, slots[2] | 0); const d = Math.max(0, slots[3] | 0); if (a === 0 && b === 0 && c === 0 && d === 0) continue; parts.push(`${e}|${a},${b},${c},${d}`); } gg.data.setStringData(OtherDataType.ElementGem, parts.join('#')); gg.data.saveLocal(); } }