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.
823 lines
31 KiB
823 lines
31 KiB
|
3 weeks ago
|
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 { isComboHero } 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;
|
||
|
|
/** 专属倍率:personalweapon 对上为 2,否则 1 */
|
||
|
|
exclusiveMul: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 专属加倍 */
|
||
|
|
export const EQUIP_EXCLUSIVE_ATTR_MUL = 2;
|
||
|
|
|
||
|
|
/** 武器类型 → 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 = 未穿成品(展示 startweapon)
|
||
|
|
*/
|
||
|
|
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===0:显示、可出售)
|
||
|
|
* isshow===1:不显示、无法出售
|
||
|
|
*/
|
||
|
|
isEquipBagVisible(equipId: number): boolean {
|
||
|
|
const conf = this.getEquip(equipId);
|
||
|
|
if (!conf) return false;
|
||
|
|
return Number(conf.isshow) === 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 全量武器配表 */
|
||
|
|
getAllEquipList(): ITableWeapon[] {
|
||
|
|
return gg.data.table.getTableList<ITableWeapon>(TableNames.Weapon) ?? [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Item.para 指向的成品武器 id;0 表示不是武器碎片
|
||
|
|
*/
|
||
|
|
getItemEquipId(itemId: number): number {
|
||
|
|
const item = gg.data.table.getItemData(itemId);
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 武器库展示:成品与碎片各占一格。
|
||
|
|
* isshow===1 不展示(不显示、无法出售)。
|
||
|
|
*/
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 可回收:未穿戴的绿/蓝/紫成品 + 同品质武器碎片(isshow===0)
|
||
|
|
* 成品单价 Weapon.price;碎片单价 Item.price
|
||
|
|
* @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 };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 单件回收单价:成品 Weapon.price,碎片 Item.price。
|
||
|
|
* @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;
|
||
|
|
}
|
||
|
|
const conf = this.getEquip(entry.equipId);
|
||
|
|
if (!conf) return 0;
|
||
|
|
const price = Number(conf.price);
|
||
|
|
if (!Number.isFinite(price) || price < 0) {
|
||
|
|
console.log('[Equip] Weapon.price 非法', entry.equipId, conf.price);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
return price;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 卖掉勾选品质的未穿戴成品与武器碎片,返回卖出数量和金币 */
|
||
|
|
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 把(含 isshow=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 表有效成品(不要求 isshow) */
|
||
|
|
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:未穿成品,展示 startweapon */
|
||
|
|
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,否则用 startweapon.skin
|
||
|
|
*/
|
||
|
|
getHeroWeaponSkinName(heroId: number): string {
|
||
|
|
const worn = this.getHeroWearEquip(heroId);
|
||
|
|
if (worn) return String(worn.skin || '').trim();
|
||
|
|
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId);
|
||
|
|
const startId = Number(hero?.startweapon) || 0;
|
||
|
|
return String(this.getEquip(startId)?.skin || '').trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 展示/战斗实际使用的武器 id(未穿则 startweapon) */
|
||
|
|
getHeroDisplayWeaponId(heroId: number): number {
|
||
|
|
const worn = this.getHeroEquipId(heroId);
|
||
|
|
if (worn > 0) return worn;
|
||
|
|
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId);
|
||
|
|
return Number(hero?.startweapon) || 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),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 专属加倍:穿戴者 personalweapon 等于该武器 id 时为 2 */
|
||
|
|
getEquipExclusiveMul(heroId: number, eq?: ITableWeapon | null): number {
|
||
|
|
const conf = eq ?? this.getHeroWearEquip(heroId);
|
||
|
|
const id = Number(heroId) | 0;
|
||
|
|
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, id);
|
||
|
|
const exclusive = Number(hero?.personalweapon) | 0;
|
||
|
|
const eqId = Number(conf?.id) | 0;
|
||
|
|
return exclusive > 0 && exclusive === eqId ? EQUIP_EXCLUSIVE_ATTR_MUL : 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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 类型能否穿:专属(Weapon.heroid>0)仅所属英雄,不受类型限制;
|
||
|
|
* 非专属须同 Weapon.type,或等于该神将 personalweapon
|
||
|
|
*/
|
||
|
|
canWearType(heroId: number, equipId: number): boolean {
|
||
|
|
if (equipId === HERO_DEFAULT_EQUIP_ID) return true;
|
||
|
|
const conf = this.getEquip(equipId);
|
||
|
|
if (!conf) return false;
|
||
|
|
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId);
|
||
|
|
if (!hero) return false;
|
||
|
|
const owner = Number(conf.heroid) | 0;
|
||
|
|
if (owner > 0) return owner === (Number(heroId) | 0);
|
||
|
|
if (Number(hero.personalweapon) === Number(equipId)) return true;
|
||
|
|
return Number(hero.weapon) === Number(conf.type);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 类型 + 库存能否穿 */
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 卸下成品,回到 startweapon */
|
||
|
|
unequip(heroId: number): boolean {
|
||
|
|
return this.wear(heroId, HERO_DEFAULT_EQUIP_ID);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 名将库网格:合成神将 herotype===2 */
|
||
|
|
getNormalHeroList(): ITableHero[] {
|
||
|
|
const all = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
|
||
|
|
const out: ITableHero[] = [];
|
||
|
|
for (let i = 0; i < all.length; i++) {
|
||
|
|
if (isComboHero(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];
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 名将库页签红点:有未读碎片(仅 isshow===0 的武器) */
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 该武器碎片是否未读(isshow===1 不显示红点) */
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|