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.
285 lines
11 KiB
285 lines
11 KiB
import { Node } from 'cc';
|
|
import { GBundle, GPath } from '../game/ConfigRes';
|
|
import { TableNames } from '../game/ConfigTableData';
|
|
import { OtherDataType } from '../game/GameDataEnums';
|
|
import { GEvent } from '../../mx/module/event/GEvent';
|
|
import { SpriteLoad } from '../../mx/components/SpriteLoad';
|
|
import {
|
|
createMembershipSet,
|
|
membershipAdd,
|
|
membershipFromArray,
|
|
membershipHas,
|
|
membershipToArray,
|
|
type MembershipSet,
|
|
} from '../../mx/tools/MembershipSet';
|
|
|
|
/** Headicon.unlocktype:无需解锁 */
|
|
const UNLOCK_TYPE_NONE = 0;
|
|
/** Headicon.unlocktype:段位星数 */
|
|
const UNLOCK_TYPE_RANK_STARS = 1;
|
|
/** Headicon.unlocktype:合成英雄次数 */
|
|
const UNLOCK_TYPE_HERO_COMPOSE = 2;
|
|
/** Headicon.unlocktype:获得装备 */
|
|
const UNLOCK_TYPE_EQUIP = 3;
|
|
|
|
/** 合成英雄次数存档 para 解析结果 */
|
|
type HeroComposeUnlockPara = {
|
|
/** 英雄 id */
|
|
heroId: number;
|
|
/** 所需次数 */
|
|
count: number;
|
|
};
|
|
|
|
/** Headicon 表头像:设置页选择、首页展示、解锁进度 */
|
|
export class HeadIconManager {
|
|
/** 本局已计入合成次数的英雄 id(每局每英雄只计 1 次) */
|
|
private static _battleComposeCounted: MembershipSet<number> | null = null;
|
|
|
|
/** 新开一局战斗时重置局内合成计数 */
|
|
static resetBattleSession(): void {
|
|
this._battleComposeCounted = createMembershipSet<number>();
|
|
}
|
|
|
|
/** Headicon 全表(id 升序) */
|
|
static getHeadIconList(): ITableHeadicon[] {
|
|
const list = gg.data.table.getTableList<ITableHeadicon>(TableNames.Headicon) ?? [];
|
|
return list.slice().sort((a, b) => (Number(a.id) || 0) - (Number(b.id) || 0));
|
|
}
|
|
|
|
/** 解析 Headicon.icon;空则回退 name */
|
|
static resolveIconName(row: ITableHeadicon | null | undefined): string {
|
|
if (!row) return '';
|
|
const icon = String(row.icon ?? '').trim();
|
|
if (icon) return icon;
|
|
const name = String(row.name ?? '').trim();
|
|
if (!name) {
|
|
console.log('[HeadIconManager] Headicon 缺少 icon/name', row.id);
|
|
return '';
|
|
}
|
|
return name;
|
|
}
|
|
|
|
/** 解析 Headicon.unlocktype(空 / 0 视为未配置) */
|
|
static resolveUnlockType(row: ITableHeadicon | null | undefined): number {
|
|
if (!row) return UNLOCK_TYPE_NONE;
|
|
const raw = row.unlocktype as unknown;
|
|
if (raw === null || raw === undefined) return UNLOCK_TYPE_NONE;
|
|
const text = String(raw).trim();
|
|
if (!text) return UNLOCK_TYPE_NONE;
|
|
const n = Math.floor(Number(text));
|
|
return Number.isFinite(n) ? n : UNLOCK_TYPE_NONE;
|
|
}
|
|
|
|
/** 是否配置了解锁条件(仅 1/2/3 有效) */
|
|
static needsUnlock(row: ITableHeadicon | null | undefined): boolean {
|
|
const t = this.resolveUnlockType(row);
|
|
return t === UNLOCK_TYPE_RANK_STARS
|
|
|| t === UNLOCK_TYPE_HERO_COMPOSE
|
|
|| t === UNLOCK_TYPE_EQUIP;
|
|
}
|
|
|
|
/** 设置页是否显示 lock 遮罩 */
|
|
static shouldShowLock(row: ITableHeadicon | null | undefined): boolean {
|
|
if (!this.needsUnlock(row)) return false;
|
|
return !this.isUnlocked(row);
|
|
}
|
|
|
|
/** 已解锁头像 id 列表(解锁头像 id 存档) */
|
|
static getUnlockedHeadIconIds(): number[] {
|
|
return gg.data.getArrayDataInt(OtherDataType.UnlockedHeadIconIds);
|
|
}
|
|
|
|
/** 头像是否已解锁(unlocktype=0 视为默认解锁) */
|
|
static isUnlocked(rowOrId: ITableHeadicon | number): boolean {
|
|
const row = typeof rowOrId === 'number'
|
|
? gg.data.table.getTableData<ITableHeadicon>(TableNames.Headicon, rowOrId)
|
|
: rowOrId;
|
|
if (!row) return false;
|
|
if (!this.needsUnlock(row)) return true;
|
|
return membershipHas(membershipFromArray(this.getUnlockedHeadIconIds()), row.id);
|
|
}
|
|
|
|
/** 某英雄累计合成次数 */
|
|
static getHeroComposeCount(heroId: number): number {
|
|
const id = Math.floor(Number(heroId) || 0);
|
|
if (id <= 0) return 0;
|
|
const v = gg.data.getkeyKeyValue(OtherDataType.HeroComposeCount, id);
|
|
return v === '' ? 0 : Math.max(0, Math.floor(Number(v) || 0));
|
|
}
|
|
|
|
/** 记录一次英雄合成并尝试解锁头像(同局同英雄只计 1 次) */
|
|
static onHeroComposed(heroId: number): void {
|
|
const id = Math.floor(Number(heroId) || 0);
|
|
if (id <= 0) return;
|
|
if (!this._battleComposeCounted) {
|
|
this._battleComposeCounted = createMembershipSet<number>();
|
|
}
|
|
if (membershipHas(this._battleComposeCounted, id)) return;
|
|
membershipAdd(this._battleComposeCounted, id);
|
|
const next = this.getHeroComposeCount(id) + 1;
|
|
gg.data.setkeyKeyValue(OtherDataType.HeroComposeCount, id, next);
|
|
const newly = this._syncUnlocksFromProgress();
|
|
if (newly.length <= 0) gg.data.saveLocal();
|
|
}
|
|
|
|
/** 获得装备后尝试解锁头像 */
|
|
static onEquipGained(equipId: number): void {
|
|
const id = Math.floor(Number(equipId) || 0);
|
|
if (id <= 0) return;
|
|
this._syncUnlocksFromProgress();
|
|
}
|
|
|
|
/** 段位星数变化后尝试解锁头像 */
|
|
static onRankStarsChanged(): void {
|
|
this._syncUnlocksFromProgress();
|
|
}
|
|
|
|
/** 读档/进大厅后按当前进度补解锁(异步加载 Headicon 表) */
|
|
static async syncUnlocksFromProgressAsync(): Promise<number[]> {
|
|
await gg.data.table.ensureTableLoaded(TableNames.Headicon);
|
|
return this._syncUnlocksFromProgress();
|
|
}
|
|
|
|
/** 存档头像 id;0 或未设置时取首个可用头像 */
|
|
static getPlayerHeadIconId(): number {
|
|
const saved = Math.floor(Number(gg.data.doc.headIconId) || 0);
|
|
if (saved > 0 && this.isUnlocked(saved)) {
|
|
const row = gg.data.table.getTableData<ITableHeadicon>(TableNames.Headicon, saved);
|
|
if (row) return saved;
|
|
console.log('[HeadIconManager] headIconId 无效', saved);
|
|
}
|
|
const list = this.getHeadIconList();
|
|
for (let i = 0; i < list.length; i++) {
|
|
if (this.isUnlocked(list[i])) return list[i].id;
|
|
}
|
|
if (list.length <= 0) {
|
|
console.log('[HeadIconManager] Headicon 表为空');
|
|
return 0;
|
|
}
|
|
return list[0].id;
|
|
}
|
|
|
|
/** 写入首页头像 id(须已解锁) */
|
|
static setPlayerHeadIconId(id: number): void {
|
|
const headId = Math.floor(Number(id) || 0);
|
|
const row = gg.data.table.getTableData<ITableHeadicon>(TableNames.Headicon, headId);
|
|
if (!row) {
|
|
console.log('[HeadIconManager] 设置头像失败,无效 id', headId);
|
|
return;
|
|
}
|
|
if (!this.isUnlocked(row)) {
|
|
console.log('[HeadIconManager] 设置头像失败,未解锁', headId);
|
|
return;
|
|
}
|
|
gg.data.doc.headIconId = headId;
|
|
gg.data.saveLocal();
|
|
}
|
|
|
|
/** 按 Headicon 行绑定 head 节点(需已挂 SpriteLoad) */
|
|
static applyRowToNode(headNode: Node | null, row: ITableHeadicon | null | undefined): void {
|
|
if (!headNode?.isValid) return;
|
|
const iconName = this.resolveIconName(row);
|
|
if (!iconName) return;
|
|
const loader = headNode.getComponent(SpriteLoad);
|
|
if (!loader) {
|
|
console.error('[HeadIconManager] 节点缺少 SpriteLoad', headNode.name);
|
|
return;
|
|
}
|
|
loader.setSprite(GPath.HeadIcon(iconName), GBundle.Items);
|
|
}
|
|
|
|
/** 按存档或指定 id 绑定 head 节点 */
|
|
static applyToNode(headNode: Node | null, iconId?: number): void {
|
|
const id = iconId ?? this.getPlayerHeadIconId();
|
|
if (id <= 0) return;
|
|
const row = gg.data.table.getTableData<ITableHeadicon>(TableNames.Headicon, id);
|
|
if (!row) {
|
|
console.log('[HeadIconManager] 未找到 Headicon', id);
|
|
return;
|
|
}
|
|
this.applyRowToNode(headNode, row);
|
|
}
|
|
|
|
/** 刷新 ui_head 头像区(headMask/head,隐藏 man/woman) */
|
|
static async refreshUiHeadAsync(uiHead: Node | null): Promise<void> {
|
|
if (!uiHead?.isValid) return;
|
|
await gg.data.table.ensureTableLoaded(TableNames.Headicon);
|
|
this.refreshUiHead(uiHead);
|
|
}
|
|
|
|
/** 刷新 ui_head 头像区(headMask/head,隐藏 man/woman) */
|
|
static refreshUiHead(uiHead: Node | null): void {
|
|
if (!uiHead?.isValid) return;
|
|
const headMask = uiHead.getChildByName('headMask');
|
|
const headNode = headMask?.getChildByName('head') ?? uiHead.find('headMask/head');
|
|
if (!headNode?.isValid) return;
|
|
this.applyToNode(headNode);
|
|
if (headMask) headMask.active = true;
|
|
}
|
|
|
|
/** 按当前段位/合成次数/装备补写解锁头像 id */
|
|
private static _syncUnlocksFromProgress(): number[] {
|
|
const list = this.getHeadIconList();
|
|
const unlocked = membershipFromArray(this.getUnlockedHeadIconIds());
|
|
const newly: number[] = [];
|
|
for (let i = 0; i < list.length; i++) {
|
|
const row = list[i];
|
|
if (!this.needsUnlock(row)) continue;
|
|
if (membershipHas(unlocked, row.id)) continue;
|
|
if (!this._meetsUnlockCondition(row)) continue;
|
|
membershipAdd(unlocked, row.id);
|
|
newly.push(row.id);
|
|
}
|
|
if (newly.length <= 0) return newly;
|
|
gg.data.setArrayData(OtherDataType.UnlockedHeadIconIds, membershipToArray(unlocked));
|
|
gg.data.saveLocal();
|
|
GEvent.Ins.emit(GEvent.UpdateHeadIcon);
|
|
return newly;
|
|
}
|
|
|
|
/** 判定 Headicon 解锁条件是否已满足 */
|
|
private static _meetsUnlockCondition(row: ITableHeadicon): boolean {
|
|
const unlockType = this.resolveUnlockType(row);
|
|
if (unlockType === UNLOCK_TYPE_RANK_STARS) {
|
|
const needStars = Math.floor(Number(row.para) || 0);
|
|
if (needStars <= 0) {
|
|
console.log('[HeadIconManager] 段位解锁 para 无效', row.id, row.para);
|
|
return false;
|
|
}
|
|
const stars = Math.floor(Number(gg.data.doc.rankStars) || 0);
|
|
return stars >= 1 && stars >= needStars;
|
|
}
|
|
if (unlockType === UNLOCK_TYPE_HERO_COMPOSE) {
|
|
const para = this._parseHeroComposePara(row.para);
|
|
if (!para) {
|
|
console.log('[HeadIconManager] 合成解锁 para 无效', row.id, row.para);
|
|
return false;
|
|
}
|
|
return this.getHeroComposeCount(para.heroId) >= para.count;
|
|
}
|
|
if (unlockType === UNLOCK_TYPE_EQUIP) {
|
|
const equipId = Math.floor(Number(row.para) || 0);
|
|
if (equipId <= 0) {
|
|
console.log('[HeadIconManager] 装备解锁 para 无效', row.id, row.para);
|
|
return false;
|
|
}
|
|
return gg.equip.getHaveEquipCount(equipId) >= 1;
|
|
}
|
|
if (unlockType !== UNLOCK_TYPE_NONE) {
|
|
console.log('[HeadIconManager] 未知 unlocktype', row.id, unlockType);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** 解析 unlocktype=2 的 para(heroId,count) */
|
|
private static _parseHeroComposePara(para: string | number): HeroComposeUnlockPara | null {
|
|
const raw = String(para ?? '').trim();
|
|
if (!raw) return null;
|
|
const parts = raw.split(',');
|
|
if (parts.length < 2) return null;
|
|
const heroId = Math.floor(Number(parts[0]) || 0);
|
|
const count = Math.floor(Number(parts[1]) || 0);
|
|
if (heroId <= 0 || count <= 0) return null;
|
|
return { heroId, count };
|
|
}
|
|
}
|
|
|