import { Singleton } from '../../mx/tools/Singleton'; import { GEvent } from '../../mx/module/event/GEvent'; import { TableNames } from '../game/ConfigTableData'; import { HERO_DEFAULT_EQUIP_ID, IHaveEquipSave, IHeroEquipSave, OtherDataType, } from '../game/GameDataEnums'; import { isElementHero } from '../battle/config/BattleHeroTable'; import { membershipAdd, membershipFromArray, membershipHas, membershipToArray } from '../../mx/tools/MembershipSet'; import { HeadIconManager } from './HeadIconManager'; /** 武器库一格:成品武器 / 碎片 */ export type MingjiangkuBagKind = 'equip' | 'fragment'; /** 已穿戴成品对神将的数值加成 */ export interface IHeroEquipBonus { /** 攻击力(配表原值) */ atk: number; /** 专属倍率(字段已删,恒为 1) */ exclusiveMul: number; } /** @deprecated 专属加倍字段已删 */ export const EQUIP_EXCLUSIVE_ATTR_MUL = 1; /** 武器类型 → battleHero/prefab/weapon 预制体名 */ const WEAPON_TYPE_PREFAB = ['', '刀', '枪', '棍', '弓']; /** 红点:未读碎片 id 列表的存档 key2 */ const RED_FRAG_KEY = 'frag'; /** 武器库一格数据 */ export interface IMingjiangkuBagEntry { /** 成品 / 碎片 */ kind: MingjiangkuBagKind; /** Weapon.id */ equipId: number; /** 碎片道具 Item.id */ itemId: number; /** 展示数量 */ count: number; /** 合成所需碎片数 */ need: number; } /** * 名将库装备存档: * - 武器碎片 = Item(Item.para → Weapon.id,Weapon.itemid 回指碎片) * - 成品武器 = HaveEquip(Weapon.id + 数量),不进 Item * - HeroEquip:神将 id → 武器 id;-1 = 未穿成品 */ export class EquipDataManager extends Singleton { /** 未穿戴成品时的存档标记 */ readonly DEFAULT_EQUIP_ID = HERO_DEFAULT_EQUIP_ID; /** 碎片数量快照(红点:数量增加则未读) */ private _fragSnap: { [itemId: number]: number } = {}; /** 是否已监听道具数量变化 */ private _itemListen = false; /** 确保 Weapon / Item / Hero 已加载,并开始碎片红点监听 */ async ensureTable(): Promise { await Promise.all([ gg.data.table.ensureTableLoaded(TableNames.Weapon), gg.data.table.ensureTableLoaded(TableNames.Item), gg.data.table.ensureTableLoaded(TableNames.Hero), ]); this._stripIllegalWears(); this._bindItemListen(); } /** 读取 Weapon 配表 */ getEquip(equipId: number): ITableWeapon | null { const n = Number(equipId) || 0; if (n <= 0) return null; return gg.data.table.getTableData(TableNames.Weapon, n) ?? null; } /** * 武器/宝石库是否可见(Weapon.isshow 已删:有配表行即可见)。 */ isEquipBagVisible(equipId: number): boolean { return this.getEquip(equipId) != null; } /** 全量武器配表 */ getAllEquipList(): ITableWeapon[] { return gg.data.table.getTableList(TableNames.Weapon) ?? []; } /** * Item.para 指向的成品武器 id;0 表示不是武器碎片(Item.type 须为 2) */ getItemEquipId(itemId: number): number { const item = gg.data.table.getItemData(itemId); if (!item || Math.floor(Number(item.type) || 0) !== 2) return 0; const para = Math.floor(Number(item?.para) || 0); if (para <= 0) return 0; const conf = this.getEquip(para); if (!conf || Number(conf.itemid) !== itemId) return 0; return para; } /** 合成消耗的碎片道具 id(Weapon.itemid) */ getFragmentItemId(equipId: number): number { return Number(this.getEquip(equipId)?.itemid) || 0; } /** 背包里该武器碎片数量 */ getFragmentCount(equipId: number): number { const itemId = this.getFragmentItemId(equipId); if (itemId <= 0) return 0; return gg.data.getItemNum(itemId); } /** 合成所需碎片数(Weapon.synthesisnumber) */ getComposeNeed(equipId: number): number { return Math.max(0, Number(this.getEquip(equipId)?.synthesisnumber) || 0); } /** 碎片是否够合成一把 */ canCompose(equipId: number): boolean { if (!this.isEquipBagVisible(equipId)) return false; const need = this.getComposeNeed(equipId); return need > 0 && this.getFragmentCount(equipId) >= need; } /** * 合成一把成品:扣碎片,HaveEquip +1。成品不进 Item。 */ compose(equipId: number): boolean { if (!this.isEquipBagVisible(equipId)) return false; const itemId = this.getFragmentItemId(equipId); const need = this.getComposeNeed(equipId); if (itemId <= 0 || need <= 0) return false; if (gg.data.getItemNum(itemId) < need) return false; gg.data.subItemNum(itemId, need); this.addHaveEquip(equipId, 1); return true; } /** 把背包里够数量的碎片全部合成成品 */ composeReadyFragments(): number { let made = 0; const list = this.getAllEquipList(); for (let i = 0; i < list.length; i++) { const id = Number(list[i]?.id) || 0; if (id <= 0 || !this.isEquipBagVisible(id)) continue; while (this.canCompose(id)) { if (!this.compose(id)) break; made++; } } return made; } /** * 武器库展示:成品与碎片各占一格。 */ getBagDisplayList(): IMingjiangkuBagEntry[] { const list: IMingjiangkuBagEntry[] = []; const have = this.getHaveEquipList(); for (let i = 0; i < have.length; i++) { const row = have[i]; if (!this.isEquipBagVisible(row.id)) continue; const conf = this.getEquip(row.id); if (!conf) continue; const free = this.getFreeEquipCount(row.id); if (free <= 0) continue; list.push({ kind: 'equip', equipId: row.id, itemId: Number(conf.itemid) || 0, count: free, need: Number(conf.synthesisnumber) || 0, }); } const bag = gg.data.getAllItemNum() ?? []; for (let i = 0; i < bag.length; i++) { const it = bag[i]; if (!it || it.count <= 0) continue; const equipId = this.getItemEquipId(it.id); if (equipId <= 0 || !this.isEquipBagVisible(equipId)) continue; const conf = this.getEquip(equipId); if (!conf) continue; list.push({ kind: 'fragment', equipId, itemId: it.id, count: it.count, need: Number(conf.synthesisnumber) || 0, }); } list.sort((a, b) => this._compareBagDisplayEntry(a, b)); return list; } /** * 武器库展示排序:成品在前、碎片在后;同类按品质降序、同品质 Weapon.id 升序。 * @param a 格子 A * @param b 格子 B */ private _compareBagDisplayEntry(a: IMingjiangkuBagEntry, b: IMingjiangkuBagEntry): number { if (a.kind !== b.kind) return a.kind === 'equip' ? -1 : 1; const qa = Number(this.getEquip(a.equipId)?.quality) || 0; const qb = Number(this.getEquip(b.equipId)?.quality) || 0; if (qb !== qa) return qb - qa; if (a.equipId !== b.equipId) return a.equipId - b.equipId; return 0; } /** * 可回收:未穿戴成品 + 同品质碎片。 * 成品单价已无 Weapon.price(仅碎片 Item.price 有价);成品按 0 计并 warn。 * @param qualities 勾选品质,空则列表为空 */ getRecyclePreview(qualities: number[]): { list: IMingjiangkuBagEntry[]; money: number } { const list: IMingjiangkuBagEntry[] = []; let money = 0; if (!qualities || qualities.length <= 0) return { list, money }; const have = this.getHaveEquipList(); for (let i = 0; i < have.length; i++) { const row = have[i]; if (!this.isEquipBagVisible(row.id)) continue; const conf = this.getEquip(row.id); if (!conf) continue; const q = Number(conf.quality); if (!Number.isFinite(q) || q < 0 || q > 4) { console.log('[Equip] Weapon.quality 无效', row.id, conf.quality); continue; } if (qualities.indexOf(q) < 0) continue; const free = this.getFreeEquipCount(row.id); if (free <= 0) continue; const entry: IMingjiangkuBagEntry = { kind: 'equip', equipId: row.id, itemId: Number(conf.itemid) || 0, count: free, need: Number(conf.synthesisnumber) || 0, }; list.push(entry); money += this.getRecycleUnitPrice(entry) * free; } const bag = gg.data.getAllItemNum() ?? []; for (let i = 0; i < bag.length; i++) { const it = bag[i]; if (!it || it.count <= 0) continue; const equipId = this.getItemEquipId(it.id); if (equipId <= 0 || !this.isEquipBagVisible(equipId)) continue; const conf = this.getEquip(equipId); if (!conf) continue; const q = Number(conf.quality); if (!Number.isFinite(q) || q < 0 || q > 4) { console.log('[Equip] Weapon.quality 无效', equipId, conf.quality); continue; } if (qualities.indexOf(q) < 0) continue; const entry: IMingjiangkuBagEntry = { kind: 'fragment', equipId, itemId: it.id, count: it.count, need: Number(conf.synthesisnumber) || 0, }; list.push(entry); money += this.getRecycleUnitPrice(entry) * it.count; } list.sort((a, b) => this._compareBagDisplayEntry(a, b)); return { list, money }; } /** * 单件回收单价:仅碎片读 Item.price;成品无 price 字段,返回 0。 * @param entry 武器库格 */ getRecycleUnitPrice(entry: Pick): number { if (entry.kind === 'fragment') { const item = gg.data.table.getItemData(entry.itemId); if (!item) { console.log('[Equip] 碎片 Item 缺失', entry.itemId); return 0; } const price = Number(item.price); if (!Number.isFinite(price) || price < 0) { console.log('[Equip] Item.price 非法', entry.itemId, item.price); return 0; } return price; } if (!this.getEquip(entry.equipId)) return 0; console.log('[Equip] Weapon.price 已删,成品回收价按 0', entry.equipId); return 0; } /** 卖掉勾选品质的未穿戴成品与武器碎片,返回卖出数量和金币 */ recycleFreeEquips(qualities: number[]): { count: number; money: number } { const { list } = this.getRecyclePreview(qualities); let count = 0; let money = 0; for (let i = 0; i < list.length; i++) { const e = list[i]; if (!this.isEquipBagVisible(e.equipId)) continue; const unitPrice = this.getRecycleUnitPrice(e); if (e.kind === 'fragment') { if (gg.data.getItemNum(e.itemId) < e.count) continue; gg.data.subItemNum(e.itemId, e.count); count += e.count; money += unitPrice * e.count; continue; } if (!this.subHaveEquip(e.equipId, e.count)) continue; count += e.count; money += unitPrice * e.count; } if (money > 0) gg.data.addMoney(money); gg.data.saveLocal(); return { count, money }; } /** 重置:卸下穿戴、清空成品与武器碎片 */ clearAll(): void { gg.data.setStringData(OtherDataType.HaveEquip, ''); gg.data.setStringData(OtherDataType.HeroEquip, ''); const byBag = gg.data.getAllItemNum() ?? []; for (let i = 0; i < byBag.length; i++) { const it = byBag[i]; if (!it || it.count <= 0) continue; if (this.getItemEquipId(it.id) <= 0) continue; gg.data.setItemNum(it.id, 0); } gg.data.setkeyKeyValue(OtherDataType.RedDotState, RED_FRAG_KEY, ''); this._fragSnap = {}; GEvent.Ins.emit(GEvent.UpdateHaveEquip, 0); GEvent.Ins.emit(GEvent.UpdateHeroEquip, 0, HERO_DEFAULT_EQUIP_ID); gg.dot?.updateRedDot(); } /** 某把成品的库存数量 */ getHaveEquipCount(equipId: number): number { const v = gg.data.getkeyKeyValue(OtherDataType.HaveEquip, equipId); return v === '' ? 0 : Math.max(0, Number(v) || 0); } /** 写入成品库存 */ setHaveEquipCount(equipId: number, count: number): void { const prev = this.getHaveEquipCount(equipId); const n = Math.max(0, Math.floor(Number(count) || 0)); gg.data.setkeyKeyValue(OtherDataType.HaveEquip, equipId, n); GEvent.Ins.emit(GEvent.UpdateHaveEquip, equipId); if (n > prev && n > 0) { HeadIconManager.onEquipGained(equipId); } if (prev <= 0 && n > 0 && this.isEquipBagVisible(equipId)) { gg.dot?.updateRedDot(); } } /** 增加成品库存 */ addHaveEquip(equipId: number, num: number): void { this.setHaveEquipCount(equipId, this.getHaveEquipCount(equipId) + num); } /** * GM:Weapon 全表成品各保证至少 1 把;已有库存不覆盖。 * 仅对原本已拥有的武器标记 PopNewWeapon/角标已读;新发放的可见武器保留新武器流程。 * @returns 本次新发放的武器种数 */ async gmGrantAllBagEquips(): Promise { await this.ensureTable(); const list = this.getAllEquipList(); if (!list.length) { console.warn('[Equip] GM 一键发武:Weapon 表为空或未加载'); return 0; } /** 原本已有库存、仅需消弹窗/角标的 id */ const alreadyOwnedIds: number[] = []; let added = 0; for (let i = 0; i < list.length; i++) { const id = Math.floor(Number(list[i]?.id) || 0); if (!this._isGmGrantableWeapon(id)) continue; const had = this.getHaveEquipCount(id) >= 1; if (!had) { this.setHaveEquipCount(id, 1); added++; } else { alreadyOwnedIds.push(id); } } this.markPopNewWeaponsShown(alreadyOwnedIds); this.markNewWeaponBadgeDismissed(alreadyOwnedIds); return added; } /** GM 发武:Weapon 表有效成品 */ private _isGmGrantableWeapon(equipId: number): boolean { const id = Math.floor(Number(equipId) || 0); if (id <= 0) return false; const conf = this.getEquip(id); if (!conf) return false; return Math.floor(Number(conf.itemid) || 0) > 0; } /** 扣成品;不能扣到低于已穿戴数 */ subHaveEquip(equipId: number, num: number): boolean { const have = this.getHaveEquipCount(equipId); const worn = this.getWornCount(equipId); const next = have - num; if (next < worn) return false; this.setHaveEquipCount(equipId, Math.max(0, next)); return true; } /** 库存列表(count>0) */ getHaveEquipList(): IHaveEquipSave[] { const list: IHaveEquipSave[] = []; const kv = this._parseKv(gg.data.getStringData(OtherDataType.HaveEquip)); for (let i = 0; i < kv.length; i++) { if (kv[i].val > 0) list.push({ id: kv[i].id, count: kv[i].val }); } return list; } /** 未穿戴可用数 = 库存 - 已穿戴 */ getFreeEquipCount(equipId: number): number { return Math.max(0, this.getHaveEquipCount(equipId) - this.getWornCount(equipId)); } /** 有多少名将穿着这把成品 */ getWornCount(equipId: number): number { if (equipId <= 0) return 0; let n = 0; const worn = this.getHeroEquipList(); for (let i = 0; i < worn.length; i++) { if (worn[i].equipId === equipId) n++; } return n; } /** -1:未穿成品 */ getHeroEquipId(heroId: number): number { const v = gg.data.getkeyKeyValue(OtherDataType.HeroEquip, heroId); if (v === '') return HERO_DEFAULT_EQUIP_ID; const n = Number(v); return Number.isFinite(n) ? n : HERO_DEFAULT_EQUIP_ID; } /** 是否未穿成品 */ isHeroDefaultWeapon(heroId: number): boolean { return this.getHeroEquipId(heroId) <= 0; } /** 当前穿戴的成品配表;未穿返回 null */ getHeroWearEquip(heroId: number): ITableWeapon | null { return this.getEquip(this.getHeroEquipId(heroId)); } /** * 展示用皮肤名:仅已穿成品的 Weapon.skin(无默认武器回落)。 */ getHeroWeaponSkinName(heroId: number): string { const worn = this.getHeroWearEquip(heroId); return worn ? String(worn.skin || '').trim() : ''; } /** 展示/战斗实际使用的武器 id(未穿为 0) */ getHeroDisplayWeaponId(heroId: number): number { const worn = this.getHeroEquipId(heroId); return worn > 0 ? worn : 0; } /** 已穿成品的神将数 */ getWornFinishedCount(): number { let n = 0; const list = this.getHeroEquipList(); for (let i = 0; i < list.length; i++) { if (list[i].equipId > 0) n++; } return n; } /** 成品加成(含专属倍率);未穿返回 null */ getHeroEquipBonus(heroId: number): IHeroEquipBonus | null { return this.buildEquipBonus(heroId, this.getHeroWearEquip(heroId)); } /** 指定成品对某神将的加成 */ buildEquipBonus(heroId: number, eq: ITableWeapon | null | undefined): IHeroEquipBonus | null { if (!eq) return null; return { atk: Number(eq.atk) || 0, exclusiveMul: this.getEquipExclusiveMul(heroId, eq), }; } /** 专属加倍(Hero.personalweapon 已删,恒 1) */ getEquipExclusiveMul(_heroId: number, _eq?: ITableWeapon | null): number { return 1; } /** 神将穿戴列表 */ getHeroEquipList(): IHeroEquipSave[] { const list: IHeroEquipSave[] = []; const kv = this._parseKv(gg.data.getStringData(OtherDataType.HeroEquip)); for (let i = 0; i < kv.length; i++) { list.push({ heroId: kv[i].id, equipId: kv[i].val }); } return list; } /** * 能否穿(heroid/weapon/personalweapon 已删):有配表行且英雄存在即可。 */ canWearType(heroId: number, equipId: number): boolean { if (equipId === HERO_DEFAULT_EQUIP_ID) return true; if (!this.getEquip(equipId)) return false; return gg.data.table.getTableData(TableNames.Hero, heroId) != null; } /** 类型 + 库存能否穿 */ canWear(heroId: number, equipId: number): boolean { if (equipId === HERO_DEFAULT_EQUIP_ID) return true; if (!this.canWearType(heroId, equipId)) return false; if (this.getHeroEquipId(heroId) !== equipId && this.getFreeEquipCount(equipId) <= 0) return false; return true; } /** * 把 from 身上的成品移到 to:to 无成品则 from 回默认;双方都有则互换 */ moveOrSwapHeroEquip(fromHeroId: number, toHeroId: number): boolean { const from = Number(fromHeroId) | 0; const to = Number(toHeroId) | 0; if (from <= 0 || to <= 0 || from === to) return false; const fromEq = this.getHeroEquipId(from); if (fromEq <= 0) return false; if (!this.canWearType(to, fromEq)) return false; const toEq = this.getHeroEquipId(to); if (toEq > 0 && !this.canWearType(from, toEq)) return false; gg.data.setkeyKeyValue(OtherDataType.HeroEquip, from, toEq > 0 ? toEq : HERO_DEFAULT_EQUIP_ID); gg.data.setkeyKeyValue(OtherDataType.HeroEquip, to, fromEq); gg.data.saveLocal(); GEvent.Ins.emit(GEvent.UpdateHeroEquip, from, this.getHeroEquipId(from)); GEvent.Ins.emit(GEvent.UpdateHeroEquip, to, fromEq); return true; } /** 穿戴成品或卸成默认(-1) */ wear(heroId: number, equipId: number): boolean { if (!this.canWear(heroId, equipId)) return false; gg.data.setkeyKeyValue(OtherDataType.HeroEquip, heroId, equipId); gg.data.saveLocal(); GEvent.Ins.emit(GEvent.UpdateHeroEquip, heroId, equipId); if (equipId > 0) this.markFragSeen(equipId); return true; } /** 卸下成品 */ unequip(heroId: number): boolean { return this.wear(heroId, HERO_DEFAULT_EQUIP_ID); } /** 名将库网格:五行英雄 */ getNormalHeroList(): ITableHero[] { const all = gg.data.table.getTableList(TableNames.Hero) ?? []; const out: ITableHero[] = []; for (let i = 0; i < all.length; i++) { if (isElementHero(all[i])) out.push(all[i]); } return out; } /** Weapon.type → battleHero 武器预制体名;非 1~4 返回空 */ resolveWeaponPrefabName(weaponType: number): string { const t = weaponType | 0; if (t < 1 || t > 4) return ''; return WEAPON_TYPE_PREFAB[t]; } /** 名将库页签红点:有未读碎片 */ hasPageRedDot(): boolean { const list = this._unreadFragIds(); for (let i = 0; i < list.length; i++) { if (this.isEquipBagVisible(list[i])) return true; } return false; } /** 是否有应显示 newWeapon 角标的新成品武器 */ hasNewWeaponBadgeRedDot(): boolean { return this.collectVisibleNewWeaponBadgeIds().length > 0; } /** 该武器碎片是否未读 */ hasNewFrag(equipId: number): boolean { const id = Math.floor(Number(equipId) || 0); if (id <= 0 || !this.isEquipBagVisible(id)) return false; return this._unreadFragIds().indexOf(id) >= 0; } /** 打开名将库时消掉「新神将」类红点(当前全表可见,仅刷新红点) */ markHeroSeen(): void { gg.dot?.updateRedDot(); } /** 标记该武器碎片已读 */ markFragSeen(equipId: number): void { const id = Math.floor(Number(equipId) || 0); if (id <= 0) return; const list = this._unreadFragIds(); const idx = list.indexOf(id); if (idx < 0) return; list.splice(idx, 1); this._writeFragIds(list); gg.dot?.updateRedDot(); } /** 卸下已穿但当前规则不允许的成品(如专属错人) */ private _stripIllegalWears(): void { const list = this.getHeroEquipList(); for (let i = 0; i < list.length; i++) { const heroId = list[i].heroId; const equipId = list[i].equipId; if (equipId <= 0) continue; if (this.canWearType(heroId, equipId)) continue; this.unequip(heroId); } } private _bindItemListen(): void { if (this._itemListen) return; this._itemListen = true; this._snapshotFragCounts(); GEvent.Ins.on(GEvent.UpdateItemNum, this._onItemNum, this); } private _onItemNum(itemId?: number): void { const id = Math.floor(Number(itemId) || 0); if (id <= 0) return; const now = gg.data.getItemNum(id); const last = this._fragSnap[id] ?? 0; this._fragSnap[id] = now; if (now <= last) return; const equipId = this.getItemEquipId(id); if (equipId <= 0 || !this.isEquipBagVisible(equipId)) return; const list = this._unreadFragIds(); if (list.indexOf(equipId) >= 0) return; list.push(equipId); this._writeFragIds(list); gg.dot?.updateRedDot(); } private _snapshotFragCounts(): void { this._fragSnap = {}; const all = gg.data.getAllItemNum() ?? []; for (let i = 0; i < all.length; i++) { const it = all[i]; if (!it) continue; this._fragSnap[it.id] = it.count; } } private _unreadFragIds(): number[] { const raw = gg.data.getkeyKeyValue(OtherDataType.RedDotState, RED_FRAG_KEY) || ''; if (!raw) return []; const out: number[] = []; const parts = raw.split(','); for (let i = 0; i < parts.length; i++) { const n = Math.floor(Number(parts[i]) || 0); if (n > 0 && out.indexOf(n) < 0) out.push(n); } return out; } private _writeFragIds(ids: number[]): void { const out: number[] = []; for (let i = 0; i < ids.length; i++) { const n = Math.floor(Number(ids[i]) || 0); if (n > 0 && out.indexOf(n) < 0) out.push(n); } gg.data.setkeyKeyValue(OtherDataType.RedDotState, RED_FRAG_KEY, out.join(',')); gg.data.saveLocal(); } /** 已在 PopNewWeapon 展示过的成品武器 id */ getShownPopNewWeaponIds(): number[] { return gg.data.getArrayDataInt(OtherDataType.ShownPopNewWeaponIds); } /** 记录 PopNewWeapon 已展示过的成品武器 id */ markPopNewWeaponsShown(ids: readonly number[]): void { if (!ids.length) return; const seen = membershipFromArray(this.getShownPopNewWeaponIds()); for (let i = 0; i < ids.length; i++) { const id = Math.floor(Number(ids[i]) || 0); if (id > 0) membershipAdd(seen, id); } gg.data.setArrayData(OtherDataType.ShownPopNewWeaponIds, membershipToArray(seen)); } /** * 待 PopNewWeapon 展示的成品武器 id(HaveEquip 有库存、可见、未展示过;碎片不算) */ collectPendingPopNewWeaponIds(): number[] { const shown = membershipFromArray(this.getShownPopNewWeaponIds()); const out: number[] = []; const have = this.getHaveEquipList(); for (let i = 0; i < have.length; i++) { const row = have[i]; const id = Math.floor(Number(row.id) || 0); if (id <= 0 || row.count <= 0) continue; if (!this._isOwnedVisibleEquip(id)) continue; if (membershipHas(shown, id)) continue; out.push(id); } out.sort((a, b) => a - b); return out; } /** 是否为待 PopNewWeapon 展示的新成品武器(碎片不算) */ isPendingPopNewWeapon(equipId: number): boolean { const id = Math.floor(Number(equipId) || 0); if (id <= 0) return false; if (!this._isOwnedVisibleEquip(id)) return false; const shown = membershipFromArray(this.getShownPopNewWeaponIds()); return !membershipHas(shown, id); } /** 武器库 newWeapon 角标是否应显示(与 PopNewWeapon 展示记录独立) */ isNewWeaponBadgeVisible(equipId: number): boolean { const id = Math.floor(Number(equipId) || 0); if (id <= 0) return false; if (!this._isOwnedVisibleEquip(id)) return false; const dismissed = membershipFromArray(this.getDismissedNewWeaponBadgeIds()); return !membershipHas(dismissed, id); } /** 已隐藏 newWeapon 角标的成品武器 id */ getDismissedNewWeaponBadgeIds(): number[] { return gg.data.getArrayDataInt(OtherDataType.DismissedNewWeaponBadgeIds); } /** 记录武器库 newWeapon 角标已隐藏 */ markNewWeaponBadgeDismissed(ids: readonly number[]): void { if (!ids.length) return; const dismissed = membershipFromArray(this.getDismissedNewWeaponBadgeIds()); for (let i = 0; i < ids.length; i++) { const id = Math.floor(Number(ids[i]) || 0); if (id > 0) membershipAdd(dismissed, id); } gg.data.setArrayData(OtherDataType.DismissedNewWeaponBadgeIds, membershipToArray(dismissed)); gg.dot?.updateRedDot(); } /** 关闭神将页时,仍应显示角标的成品武器 id */ collectVisibleNewWeaponBadgeIds(): number[] { const out: number[] = []; const have = this.getHaveEquipList(); for (let i = 0; i < have.length; i++) { const id = Math.floor(Number(have[i].id) || 0); if (id <= 0 || have[i].count <= 0) continue; if (!this.isNewWeaponBadgeVisible(id)) continue; out.push(id); } out.sort((a, b) => a - b); return out; } /** 库存中可见成品武器 */ private _isOwnedVisibleEquip(equipId: number): boolean { const id = Math.floor(Number(equipId) || 0); if (id <= 0) return false; if (!this.isEquipBagVisible(id)) return false; if (!this.getEquip(id)) return false; return this.getHaveEquipCount(id) > 0; } /** 解析 HaveEquip / HeroEquip 存档串 */ private _parseKv(raw: string): { id: number; val: number }[] { const list: { id: number; val: number }[] = []; if (!raw) return list; 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 id = Number(part.substring(0, sep)); const val = Number(part.substring(sep + 1)); if (!Number.isFinite(id)) continue; list.push({ id, val: Number.isFinite(val) ? val : 0 }); } return list; } }