import { Node } from 'cc'; import { GBundle, GPath } from '../game/ConfigRes'; import { TableNames } from '../game/ConfigTableData'; import { GEvent } from '../../mx/module/event/GEvent'; import { SpriteLoad } from '../../mx/components/SpriteLoad'; import { ConstNumType, ItemNumType, StatisticsType } from '../game/GameDataEnums'; /** 匹配段位随机权重 Const id(对齐 ConstNumType.MatchRankWeight) */ const MATCH_RANK_WEIGHT_CONST_ID = ConstNumType.MatchRankWeight; /** Rank.name 旧后缀(一~五)→ icons/level 阶位文件名 */ const RANK_SUB_TO_STAGE: Record = { '一': '初阶', '二': '中阶', '三': '高阶', '四': '高阶', '五': '圆满', }; /** 已是 icons/level 用的阶位名 */ const RANK_STAGE_NAMES: Record = { '初阶': true, '中阶': true, '高阶': true, '圆满': true, }; /** Daily_Rank.reward_rank 解析项 */ export type RankRewardGroup = { /** 奖励类型(2=成品 equip) */ type: number; /** 道具/装备 id */ id: number; /** 数量 */ num: number; }; /** 段位与战场匹配、排行榜展示数据(存档仅 rankStars,段位由 Rank.level 推导) */ export class RankDataManager { /** 玩家总星数(存档字段,默认 1,最低 1) */ static getPlayerRankStars(): number { const n = Math.floor(Number(gg.data.doc.rankStars) || 0); return n >= 1 ? n : 1; } /** 由总星数推导当前段位 id */ static getPlayerRankId(): number { return this.resolveTierByStars(this.getPlayerRankStars())?.id ?? 1; } /** 读取 Rank 配表 */ static getRankConfig(rankId: number): ITableRank | null { return gg.data.table.getTableData(TableNames.Rank, rankId) ?? null; } /** 按总星数匹配 Rank 行(level[1]===-1 表示无上限,仅末档) */ static resolveTierByStars(stars: number): ITableRank | null { const s = Math.max(1, Math.floor(Number(stars) || 0)); const list = (gg.data.table.getTableList(TableNames.Rank) ?? []) .slice() .sort((a, b) => (Number(a.id) || 0) - (Number(b.id) || 0)); if (list.length <= 0) return null; for (const row of list) { const range = row.level; if (!range || range.length < 2) continue; const min = Math.floor(Number(range[0]) || 0); const max = Math.floor(Number(range[1]) || 0); if (s >= min && (max === -1 || s <= max)) return row; } return list[list.length - 1] ?? null; } /** 段内星数 1~N(供俸禄弹窗星条) */ static getIntraTierStars(stars?: number): number { const s = stars ?? this.getPlayerRankStars(); const row = this.resolveTierByStars(s); if (!row?.level || row.level.length < 2) return 1; const min = Math.floor(Number(row.level[0]) || 1); return Math.max(1, s - min + 1); } /** * 首页星条点亮数:总星数对 5 取余,余 0 为满 5 星。 * @param stars 总星数,缺省为玩家当前总星数 */ static getStarModCount(stars?: number): number { const rem = this.clampRankStars(stars ?? this.getPlayerRankStars()) % 5; return rem === 0 ? 5 : rem; } /** 段位难度加值 */ static getRankDifficultyBonus(rankId?: number): number { const row = this.getRankConfig(rankId ?? this.getPlayerRankId()); return row?.diffcult ?? 0; } /** 存档是否仍无对局痕迹(用于首局地图 / 首战引导) */ static hasNoBattleHistory(): boolean { const doc = gg.data.doc; if ((doc.matchWinCount ?? 0) > 0 || (doc.matchLoseCount ?? 0) > 0) return false; if ((doc.rankStars ?? 1) > 1) return false; if ((doc.failCount ?? 0) > 0 || (doc.dailyWins ?? 0) > 0) return false; return true; } /** 是否尚未进入过战斗(新玩家首局) */ static isFirstEnterBattle(): boolean { if (gg.data.getStatistics(StatisticsType.IsFirstEnter) !== 0) return false; return this.hasNoBattleHistory(); } /** 标记已进入过战斗 */ static markBattleEntered(): void { if (gg.data.getStatistics(StatisticsType.IsFirstEnter) !== 0) return; gg.data.setStatistics(StatisticsType.IsFirstEnter, 1); gg.data.saveLocal(); } /** 是否还有未完成的首次进战操作引导 */ static shouldShowFirstBattleGuide(): boolean { if (gg.data.getStatistics(StatisticsType.Chapter1BattleGuideDone) !== 0) return false; return this.hasNoBattleHistory(); } /** 标记首次进战操作引导已完成 */ static markFirstBattleGuideDone(): void { if (gg.data.getStatistics(StatisticsType.Chapter1BattleGuideDone) !== 0) return; gg.data.setStatistics(StatisticsType.Chapter1BattleGuideDone, 1); gg.data.saveLocal(); } /** 读 Const 28 新玩家首关 Level id */ static readFirstEnterBattleLevelId(): number { const id = Math.floor(Number(gg.data.table.getConst(ConstNumType.NewPlayerFirstMap)) || 0); if (id <= 0) { console.error('[RankDataManager] Const 28 新玩家首关地图 id 非法', id); return id; } const clamped = gg.data.project.clampBattleLevelId(id); if (clamped !== id) { console.error('[RankDataManager] Const 28 地图 id 不在 Level 表', id); } return clamped; } /** 选本局 Level:GM 固定 > 首次进战读 Const28 > 按当天星期地图池随机 */ static pickBattleLevelId(): number { const forced = Math.floor(Number(gg.game?.gmMapId) || 0); if (forced > 0) { return gg.data.project.clampBattleLevelId(forced); } if (this.isFirstEnterBattle()) { return this.readFirstEnterBattleLevelId(); } return this.pickRandomBattleLevelId(); } /** * JS `Date.getDay()`(0=周日 … 6=周六)→ Const 31~37。 * @param jsWeekday `getDay()` 返回值 */ static resolveWeekdayLevelConstId(jsWeekday: number): number { const d = Math.floor(Number(jsWeekday)); if (d < 0 || d > 6) { console.error('[RankDataManager] 星期非法', jsWeekday); return 0; } return ConstNumType.WeekMapMon + ((d + 6) % 7); } /** 按当天 Const 31~37 的 para1 随机 Level.id;GM gmMapId>0 时固定 */ static pickRandomBattleLevelId(): number { const forced = Math.floor(Number(gg.game?.gmMapId) || 0); if (forced > 0) { return gg.data.project.clampBattleLevelId(forced); } const constId = this.resolveWeekdayLevelConstId(new Date().getDay()); if (constId < ConstNumType.WeekMapMon || constId > ConstNumType.WeekMapSun) { console.error('[RankDataManager] 当日地图常量表 id 非法', constId); return 0; } const row = gg.data.table.getConstData(constId); const raw = gg.data.table.getConstArray(constId); if (raw.length <= 0) { console.error('[RankDataManager] 当日地图池为空', constId, '行存在=', !!row, 'para1=', row?.para1 ?? ''); return 0; } const pool: number[] = []; for (let i = 0; i < raw.length; i++) { const id = Math.floor(Number(raw[i]) || 0); if (id <= 0) { console.warn('[RankDataManager] 当日地图池含非法 id', constId, raw[i]); continue; } if (!gg.data.table.getTableData(TableNames.Level, id)) { console.warn('[RankDataManager] 当日地图池 id 不在 Level 表', constId, id); continue; } pool.push(id); } if (pool.length <= 0) { console.error('[RankDataManager] 当日地图池无有效 Level', constId); return 0; } const idx = Math.floor(Math.random() * pool.length); return pool[idx]; } /** * 按玩家总星 + Const 11 十档偏移匹配对手(§4.1)。 * @param playerStars 玩家总星;缺省读存档 */ static resolveOpponentStarMatch(playerStars?: number): { /** 相对玩家星偏移 */ starOffset: number; /** 对手总星 */ opponentStars: number; /** 对手段位 id */ opponentRankId: number; } | null { const list = (gg.data.table.getTableList(TableNames.Rank) ?? []) .slice() .sort((a, b) => (Number(a.id) || 0) - (Number(b.id) || 0)); if (list.length <= 0) { console.log('[RankDataManager] Rank 表为空,无法匹配对手'); return null; } const first = list[0]; const last = list[list.length - 1]; const minStar = Math.max(1, Math.floor(Number(first.level?.[0]) || 1)); let maxStar = Math.floor(Number(last.level?.[1]) || 0); if (maxStar === -1) { maxStar = Math.max(minStar, Math.floor(Number(last.level?.[0]) || minStar)); } if (maxStar < minStar) { console.log('[RankDataManager] Rank 星范围非法', minStar, maxStar); return null; } const pStars = this.clampRankStars(playerStars ?? this.getPlayerRankStars()); const offsets = [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]; const raw = gg.data.table.getConstArray(ConstNumType.MatchRankWeight); const weights: number[] = []; for (let i = 0; i < offsets.length; i++) { const w = Math.max(0, Math.floor(Number(raw[i]) || 0)); const s = pStars + offsets[i]; weights.push(s < minStar || s > maxStar ? 0 : w); } if (raw.length !== offsets.length) { console.log('[RankDataManager] Const 11 应为 10 档权重'); } let total = 0; for (let i = 0; i < weights.length; i++) total += weights[i]; if (total <= 0) { console.log(`[RankDataManager] Const 11 有效权重全为 0 pStars=${pStars} minStar=${minStar} maxStar=${maxStar}`); return null; } let roll = Math.random() * total; let starOffset = 0; for (let i = 0; i < weights.length; i++) { roll -= weights[i]; if (roll < 0) { starOffset = offsets[i]; break; } if (i === weights.length - 1) starOffset = offsets[i]; } const opponentStars = Math.max(minStar, Math.min(maxStar, pStars + starOffset)); const tier = this.resolveTierByStars(opponentStars); if (!tier) { console.log('[RankDataManager] resolveTierByStars 失败', opponentStars); return null; } return { starOffset, opponentStars, opponentRankId: tier.id, }; } /** * 匹配对手段位 id:Const 11 按总星 `-5~+4` 十档偏移。 * @param _playerRankId 保留签名兼容;实际以总星为准 */ static pickOpponentRankId(_playerRankId?: number): number { const match = this.resolveOpponentStarMatch(); if (!match) { console.log('[RankDataManager] 匹配失败,回退玩家当前段位'); return this.getPlayerRankId(); } return match.opponentRankId; } /** 确保 Rank 表已加载 */ static async ensureTable(): Promise { await gg.data.table.ensureTableLoaded(TableNames.Rank); } /** 排行榜展示/比较用星数下限(默认 1) */ static clampRankStars(stars: number): number { const n = Math.floor(Number(stars) || 0); return n >= 1 ? n : 1; } /** 排行榜上传/展示分数(总星数) */ static getRankStars(): number { return this.getPlayerRankStars(); } /** @deprecated 使用 getRankStars */ static getSoulCount(): number { return this.getRankStars(); } /** 当前段位行(rank_detail=段内星数,供俸禄弹窗) */ static getCurrentRank(): (ITableRank & { rank_detail: number }) | null { const row = this.resolveTierByStars(this.getPlayerRankStars()); if (!row) return null; return Object.assign({}, row, { rank_detail: this.getIntraTierStars() }); } /** 段位展示名 */ static rankDisplayName(rank: ITableRank | null): string { return rank?.name?.trim() ?? ''; } /** * Rank 昵称 → icons/level 图片名(一/二 → 初阶/中阶;已是初阶等则原样)。 * 优先 `Rank.icon`;无后缀段位使用「段位·圆满」。 */ static resolveRankLevelIconName(rankId?: number): string { const id = rankId ?? this.getPlayerRankId(); const row = this.getRankConfig(id); if (!row) { console.log('[RankDataManager] 段位不存在', id); return ''; } const icon = String(row.icon ?? '').trim(); if (icon) return icon; const name = String(row.name ?? '').trim(); if (!name) { console.log('[RankDataManager] Rank.name 为空', id); return ''; } const dot = name.indexOf('·'); if (dot < 0) { return `${name}·圆满`; } const tier = name.slice(0, dot); const sub = name.slice(dot + 1); if (RANK_STAGE_NAMES[sub]) return name; const stage = RANK_SUB_TO_STAGE[sub]; if (!stage) { console.log('[RankDataManager] 未知段位后缀', name); return ''; } return `${tier}·${stage}`; } /** * 匹配/结算用段位文案(含段内星数)。 * @param rankId 段位 id * @param stars 总星;缺省不拼星数 */ static formatRankLevelDesc(rankId: number, stars?: number): string { const name = this.resolveRankLevelIconName(rankId) || this.rankDisplayName(this.getRankConfig(rankId)); const base = String(name ?? '').trim(); if (!base) return ''; if (stars == null) return base; const mod = this.getStarModCount(stars); return `${base} ${mod}星`; } /** 绑定段位昵称图到节点(需已挂 SpriteLoad) */ static applyRankLevelIconToNode(node: Node | null, rankId?: number): void { if (!node?.isValid) return; const iconName = this.resolveRankLevelIconName(rankId); if (!iconName) return; const loader = node.getComponent(SpriteLoad); if (!loader) { console.log('[RankDataManager] 节点缺少 SpriteLoad', node.name); return; } loader.setSprite(GPath.RankLevelIcon(iconName), GBundle.Items); } /** 确保 Rank 表后绑定段位昵称图 */ static async applyRankLevelIconToNodeAsync(node: Node | null, rankId?: number): Promise { await this.ensureTable(); this.applyRankLevelIconToNode(node, rankId); } /** * 结算页 ui_star:子节点 star1~star5,内层 star 控制点亮。 * @param starRoot ui_star 根节点 * @param stars 总星数,缺省为玩家当前总星数 */ static applySettlementStarsToNode(starRoot: Node | null, stars?: number): void { if (!starRoot?.isValid) { console.log('[RankDataManager] ui_star 缺失'); return; } const count = this.getStarModCount(stars); for (let i = 1; i <= 5; i++) { const slot = starRoot.getChildByName(`star${i}`); if (!slot?.isValid) { console.log('[RankDataManager] 星槽缺失', `star${i}`); continue; } slot.active = true; const icon = slot.getChildByName('star'); if (!icon?.isValid) { console.log('[RankDataManager] 星图标缺失', `star${i}`); continue; } icon.active = i <= count; } } /** 结算页段位展示:ui_star 星条 + ui_rankLevel 昵称图(icons/level) */ static async refreshSettlementRankDisplay(starRoot: Node | null, rankLevelNode: Node | null, stars?: number): Promise { await this.ensureTable(); const totalStars = stars ?? this.getPlayerRankStars(); this.applySettlementStarsToNode(starRoot, totalStars); const rankId = this.resolveTierByStars(totalStars)?.id; this.applyRankLevelIconToNode(rankLevelNode, rankId); } /** 结算后段位 id 变化视为大段提升 */ static isMajorTierPromotion(before: ITableRank | null | undefined, after: ITableRank | null | undefined): boolean { if (!before || !after) return false; return (Number(before.id) || 0) !== (Number(after.id) || 0); } /** 按总星数反查榜单条目段位 */ static resolveByStars(stars: number): ITableRank | null { return this.resolveTierByStars(stars); } /** @deprecated 使用 resolveByStars */ static resolveBySoulOrDefault(score: number): ITableRank | null { return this.resolveByStars(score); } /** * 战斗结算调整星数:胜 +1,负 -1,最低 1。 * @returns 星数是否变化 */ static applyBattleResult(won: boolean, opts?: { save?: boolean }): boolean { const cur = this.getPlayerRankStars(); const next = won ? cur + 1 : Math.max(1, cur - 1); if (next === cur) return false; gg.data.doc.rankStars = next; if (opts?.save !== false) gg.data.saveLocal(); GEvent.Ins.emit(GEvent.UpdateRankStars); return true; } /** * 段位 id + 段内星级 → 总星数(不写档)。 * @param rankId Rank.id * @param intraStar 段内星级(从 1 起;末档无上限时按 1~5) * @returns 总星;非法返回 0 */ static resolveTotalStarsByRankAndIntra(rankId: number, intraStar: number): number { const id = Math.floor(Number(rankId) || 0); const star = Math.floor(Number(intraStar) || 0); const row = this.getRankConfig(id); if (!row?.level || row.level.length < 2) { console.warn('[RankDataManager] 段位 id 无效', rankId); return 0; } const min = Math.floor(Number(row.level[0]) || 1); const max = Math.floor(Number(row.level[1]) || 0); const span = max === -1 ? 5 : Math.max(1, max - min + 1); if (!(star >= 1 && star <= span)) { console.warn('[RankDataManager] 段内星级越界', rankId, intraStar, 'span=', span); return 0; } const total = min + star - 1; if (max !== -1 && total > max) { console.warn('[RankDataManager] 总星超出段位上限', rankId, total, max); return 0; } return Math.max(1, total); } /** * GM:按段位 id + 段内星级写入存档总星,并同步 doc.rankId(不发事件,由调用方刷新 UI)。 * @param rankId Rank.id * @param intraStar 段内星级(从 1 起) * @returns 写入后的总星;失败返回 0 */ static applyGmRankAndStar(rankId: number, intraStar: number): number { const total = this.resolveTotalStarsByRankAndIntra(rankId, intraStar); if (!(total > 0)) return 0; gg.data.doc.rankStars = total; gg.data.doc.rankId = Math.floor(Number(rankId) || 0); return total; } /** * Rank.reward 金币数量(格式 `道具id|数量`,如 `1|14`)。 * @param stars 总星数 */ static parseRankTierMoneyReward(stars: number): number { const row = this.resolveTierByStars(stars); if (!row) { console.log('[RankDataManager] 俸禄段位缺失', stars); return 0; } const raw = String(row.reward ?? '').trim(); const parts = raw.split('|'); if (parts.length < 2) { console.log('[RankDataManager] Rank.reward 非法', row.id); return 0; } const itemId = Math.floor(Number(parts[0]) || 0); const num = Math.floor(Number(parts[1]) || 0); if (itemId !== ItemNumType.money) { console.log('[RankDataManager] Rank.reward 非金币', row.id); return 0; } if (!(num > 0)) { console.log('[RankDataManager] Rank.reward 数量无效', row.id); return 0; } return num; } /** Daily_Rank.reward_rank 解析(格式 type,id,num) */ static parseRewardRank(rewardStr: string): RankRewardGroup[] { const raw = String(rewardStr ?? '').trim(); if (!raw) return []; const list: RankRewardGroup[] = []; const segments = raw.split('|'); for (let i = 0; i < segments.length; i++) { const parts = segments[i].split(','); if (parts.length < 3) continue; const type = Math.floor(Number(parts[0]) || 0); const id = Math.floor(Number(parts[1]) || 0); const num = Math.floor(Number(parts[2]) || 0); if (!(id > 0) || !(num > 0)) continue; list.push({ type, id, num }); } if (list.length <= 0 && raw.indexOf('|') < 0) { const parts = raw.split(','); if (parts.length >= 3) { const type = Math.floor(Number(parts[0]) || 0); const id = Math.floor(Number(parts[1]) || 0); const num = Math.floor(Number(parts[2]) || 0); if (id > 0 && num > 0) list.push({ type, id, num }); } } return list; } }