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/manager/EquipDataManager.ts

797 lines
29 KiB

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<void> {
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<ITableWeapon>(TableNames.Weapon, n) ?? null;
}
/**
* 武器/宝石库是否可见(Weapon.isshow 已删:有配表行即可见)。
*/
isEquipBagVisible(equipId: number): boolean {
return this.getEquip(equipId) != null;
}
/** 全量武器配表 */
getAllEquipList(): ITableWeapon[] {
return gg.data.table.getTableList<ITableWeapon>(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<IMingjiangkuBagEntry, 'kind' | 'equipId' | 'itemId'>): 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<number> {
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<ITableHero>(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<ITableHero>(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;
}
}