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/RankDataManager.ts

558 lines
21 KiB

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<string, string> = {
'一': '初阶',
'二': '中阶',
'三': '高阶',
'四': '高阶',
'五': '圆满',
};
/** 已是 icons/level 用的阶位名 */
const RANK_STAGE_NAMES: Record<string, boolean> = {
'初阶': 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<ITableRank>(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<ITableRank>(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<ITableLevel>(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<ITableRank>(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<void> {
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<void> {
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<void> {
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;
}
}