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.
 
 
 
 

694 lines
25 KiB

//*********************
// create by 流云
// time: Sat Aug 15 2026
// desc: 开局匹配对手
//*********************
import { _decorator, Label, Node, sp, Tween, tween, UIOpacity, Vec3, view } from 'cc';
import { Auto_match } from './Auto_match';
import { RankDataManager } from '../../manager/RankDataManager';
import { MatchWinStats } from '../../manager/MatchWinStats';
import { SpriteLoad } from '../../../mx/components/SpriteLoad';
import { GAudioPath, UIID } from '../../game/ConfigRes';
import { TableNames } from '../../game/ConfigTableData';
import { HeadIconManager } from '../../manager/HeadIconManager';
import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback';
const { ccclass } = _decorator;
/** 匹配最短展示下限(秒) */
const MATCH_WAIT_MIN_SEC = 2;
/** 匹配最短展示上限(秒) */
const MATCH_WAIT_MAX_SEC = 3;
const RED_HEAD_WAIT_SEC = 0.8;
const RED_HEAD_ROLL_INTERVAL = 0.35;
const PANEL_MOVE_SEC = 0.5;
const MATCH_SPINE_MATCHING = '匹配中';
const MATCH_SPINE_STAND = '站';
/** 「站」里「战」淡到全透明的时刻(秒),与 匹配中.json 槽位「战」color 关键帧一致;此后是空尾巴,拉开不再等它 */
const MATCH_STAND_VANISH_SEC = 1;
/** 段位/头像绑定超时后仍进入完成态 */
const MATCH_BIND_FALLBACK_SEC = 8;
/** 匹配全流程超时:关页并回大厅 */
const MATCH_STUCK_SEC = 90;
const MATCH_SPINE_RED = '红方';
const MATCH_SPINE_BLUE = '蓝方';
const MATCH_SPINE_RED2 = '红方2';
const MATCH_SPINE_BLUE2 = '蓝方2';
const MATCH_SPINE_SEARCH = 'animation';
const EMPTY_RANK_NAME = '无段位';
/** 匹配门闩:进战斗结果回传匹配页 */
export type MatchGateHandlers = {
/** 局内页已打开,可拉开匹配页并关闭 */
notifyBattleEntered: () => void;
/** 进战斗失败 */
notifyEnterFailed: () => void;
};
/** 打开匹配页参数 */
export type MatchParam = {
/** 预载与合拢完成:开始进战斗(匹配页仍保持显示,最短展示到点且局内页打开后再切完成态) */
onGateReady?: (handlers: MatchGateHandlers) => void;
};
/** 匹配页展示的对手信息 */
type AiMatchDisplay = {
/** 段位展示文案(含星) */
rankName: string;
/** 胜率 0~100 */
winRate: number;
/** 对手段位 id */
opponentRankId: number;
/** 对手总星 */
opponentStars: number;
};
/** 开局匹配对手页:打开即匹配中;对手就绪且战斗页打开后进入匹配完成,播完动画关页 */
@ccclass('UImatch')
export class UImatch extends Auto_match {
/** 进战斗门闩回调 */
private _finishCb: ((handlers: MatchGateHandlers) => void) | null = null;
/** 最短展示时间已到 */
private _minWaitDone = false;
/** 战斗资源预载完成 */
private _loadDone = false;
/** 段位/头像绑定完成 */
private _bindDone = false;
/** 已进入匹配完成(播「站」及后续退出动画) */
private _completeEntered = false;
/** 「站」播完 */
private _standDone = false;
/** 入场合拢完成 */
private _enterMoveDone = false;
/** 局内页已打开 */
private _battleReady = false;
/** 防止重复触发进战斗 */
private _gateTriggered = false;
/** 正在退出关闭 */
private _finishing = false;
/** 本局对手展示 */
private _aiInfo: AiMatchDisplay | null = null;
/** 头像滚动世代,取消过期动画 */
private _rollGen = 0;
/** 位移动画剩余节点数 */
private _panelMoveAnimLeft = 0;
/** 当前位移阶段:合拢 / 拉开 */
private _panelMoveKind: 'in' | 'out' | null = null;
/** ui_match Spine */
private _matchSk: sp.Skeleton | null = null;
/** ui_spine 放大镜 */
private _searchSk: sp.Skeleton | null = null;
/** ui_red / ui_bule 侧 Spine */
private _redSk: sp.Skeleton | null = null;
private _blueSk: sp.Skeleton | null = null;
/** 上下栏/design 位 */
private _topRest = new Vec3();
private _bottomRest = new Vec3();
private _redRest = new Vec3();
private _buleRest = new Vec3();
private _redBoxRest = new Vec3();
private _blueBoxRest = new Vec3();
/** 是否已缓存 design 位 */
private _restPosCaptured = false;
onLoad(): void {
super.onLoad();
this._matchSk = this.ui_match?.getComponent(sp.Skeleton) ?? null;
this._searchSk = this.ui_spine?.getComponent(sp.Skeleton) ?? null;
this._redSk = this.ui_red?.getComponent(sp.Skeleton) ?? null;
this._blueSk = this.ui_bule?.getComponent(sp.Skeleton) ?? null;
this._captureRestPositions();
this._hideMatchingCenter();
this._placePanelsOffscreen();
}
onDisable(): void {
this.unschedule(this._onMinWait);
this.unschedule(this._onStandComplete);
this.unschedule(this._onBindFallback);
this.unschedule(this._onMatchStuck);
this.unschedule(this._onPanelMoveWatchdog);
this._rollGen++;
this._stopPanelTweens();
this._clearMatchSpines();
super.onDisable();
}
/** 停止匹配页 Spine 并清除回调 */
private _clearMatchSpines(): void {
const list = [this._matchSk, this._searchSk, this._redSk, this._blueSk];
for (let i = 0; i < list.length; i++) {
const sk = list[i];
if (!sk?.isValid) continue;
BattleSpinePlayback.clearPendingComplete(sk);
BattleSpinePlayback.resetTracks(sk);
}
}
onInit(): void {
const p = (this.prema ?? {}) as MatchParam;
this._finishCb = typeof p.onGateReady === 'function' ? p.onGateReady : null;
this._minWaitDone = false;
this._loadDone = false;
this._bindDone = false;
this._completeEntered = false;
this._standDone = false;
this._enterMoveDone = false;
this._battleReady = false;
this._gateTriggered = false;
this._finishing = false;
this._aiInfo = null;
this._rollGen++;
this.unschedule(this._onStandComplete);
this._resetMatchUi();
this._setWinRate(this.ui_blueBox, MatchWinStats.getPlayerWinRate());
this._playPanelEnterMove();
void this._bindMatchInfo();
void this._runRedHeadRoll();
void this._preloadBattleRes();
this.scheduleOnce(this._onMinWait, this._rollMatchWaitSec());
this.scheduleOnce(this._onBindFallback, MATCH_BIND_FALLBACK_SEC);
this.scheduleOnce(this._onMatchStuck, MATCH_STUCK_SEC);
}
/** 缓存 ui_top/ui_bottom 等 design 坐标 */
private _captureRestPositions(): void {
if (this._restPosCaptured) return;
if (this.ui_top?.isValid) this._topRest.set(this.ui_top.position);
if (this.ui_bottom?.isValid) this._bottomRest.set(this.ui_bottom.position);
if (this.ui_red?.isValid) this._redRest.set(this.ui_red.position);
if (this.ui_bule?.isValid) this._buleRest.set(this.ui_bule.position);
if (this.ui_redBox?.isValid) this._redBoxRest.set(this.ui_redBox.position);
if (this.ui_blueBox?.isValid) this._blueBoxRest.set(this.ui_blueBox.position);
this._restPosCaptured = true;
}
/** 复位匹配态:上下栏置于屏外待合拢;中间「匹配中」与放大镜待合拢后再显示 */
private _resetMatchUi(): void {
this._captureRestPositions();
this._hideMatchingCenter();
this._placePanelsOffscreen();
this._resetOpacity(this.ui_center);
this._resetOpacity(this.ui_bottom);
this._resetOpacity(this.ui_top);
this._resetOpacity(this.ui_bule);
this._resetOpacity(this.ui_red);
this._resetOpacity(this.ui_redBox);
this._resetOpacity(this.ui_blueBox);
this._resetOpacity(this.ui_spine);
this._resetRedHeadUi();
this._playSideSpineLoops();
}
/** 隐藏中间「匹配中」、放大镜与合拢光效 */
private _hideMatchingCenter(): void {
if (this.ui_match?.isValid) this.ui_match.active = false;
if (this.ui_spine?.isValid) this.ui_spine.active = false;
if (this.ui_matchLight?.isValid) this.ui_matchLight.active = false;
}
/** 上下栏移到屏幕外(上往上、下往下) */
private _placePanelsOffscreen(): void {
const dy = this._offscreenDy();
this._setPanelOffset(this.ui_top, this._topRest, dy);
this._setPanelOffset(this.ui_red, this._redRest, dy);
this._setPanelOffset(this.ui_redBox, this._redBoxRest, dy);
this._setPanelOffset(this.ui_bottom, this._bottomRest, -dy);
this._setPanelOffset(this.ui_bule, this._buleRest, -dy);
this._setPanelOffset(this.ui_blueBox, this._blueBoxRest, -dy);
}
/**
* 按 rest + dy 立刻设位置
* @param node 目标
* @param rest design 坐标
* @param dy 纵向偏移
*/
private _setPanelOffset(node: Node | null, rest: Vec3, dy: number): void {
if (!node?.isValid) return;
node.setPosition(rest.x, rest.y + dy, rest.z);
}
/** 屏外位移量(可见高度) */
private _offscreenDy(): number {
return view.getVisibleSize().height;
}
/** 停止上下栏 tween */
private _stopPanelTweens(): void {
this._panelMoveKind = null;
this._panelMoveAnimLeft = 0;
this.unschedule(this._onPanelMoveWatchdog);
const nodes = [this.ui_top, this.ui_bottom, this.ui_red, this.ui_bule, this.ui_redBox, this.ui_blueBox];
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
if (n?.isValid) Tween.stopAllByTarget(n);
}
}
/** 循环播放 ui_spine 放大镜 */
private _playSearchSpineLoop(): void {
const sk = this._searchSk;
if (!sk?.isValid) return;
BattleSpinePlayback.play(sk, MATCH_SPINE_SEARCH, { loop: true, optional: true });
}
/** 循环播放 ui_red/ui_bule 侧 Spine(匹配中) */
private _playSideSpineLoops(): void {
if (this._redSk?.isValid) {
BattleSpinePlayback.play(this._redSk, MATCH_SPINE_RED, { loop: true, optional: true });
}
if (this._blueSk?.isValid) {
BattleSpinePlayback.play(this._blueSk, MATCH_SPINE_BLUE, { loop: true, optional: true });
}
}
/** 匹配完成:红方2 / 蓝方2 */
private _playSideCompleteAnims(): void {
if (this._redSk?.isValid) {
BattleSpinePlayback.play(this._redSk, MATCH_SPINE_RED2, { loop: true });
}
if (this._blueSk?.isValid) {
BattleSpinePlayback.play(this._blueSk, MATCH_SPINE_BLUE2, { loop: true });
}
}
/** 循环播放 ui_match「匹配中」 */
private _playMatchingLoop(): void {
const sk = this._matchSk;
if (!sk?.isValid) {
console.warn('[UImatch] ui_match 缺少 sp.Skeleton');
return;
}
BattleSpinePlayback.play(sk, MATCH_SPINE_MATCHING, { loop: true });
}
/**
* 将节点不透明度置为 255
* @param node 目标节点
*/
private _resetOpacity(node: Node | null | undefined): void {
if (!node?.isValid) return;
const uo = node.getComponent(UIOpacity) ?? node.addComponent(UIOpacity);
uo.opacity = 255;
}
/** 本局匹配最短展示秒数,落在 [2, 3] */
private _rollMatchWaitSec(): number {
return MATCH_WAIT_MIN_SEC + Math.random() * (MATCH_WAIT_MAX_SEC - MATCH_WAIT_MIN_SEC);
}
private _onMinWait = (): void => {
this._minWaitDone = true;
this._tryEnterComplete();
};
/** 绑定超时:不再空等表,避免一直停在「匹配中」 */
private _onBindFallback = (): void => {
if (this._bindDone || this._finishing || !this.isValid) return;
console.error('[UImatch] 绑定超时,继续匹配完成');
this._bindDone = true;
this._tryEnterComplete();
};
/** 全流程超时:未进战则回大厅;已进战则强制关匹配页 */
private _onMatchStuck = (): void => {
if (this._finishing || this._completeEntered || !this.isValid) return;
if (this._battleReady) {
console.error('[UImatch] 匹配完成态超时,强制关页');
this._bindDone = true;
this._minWaitDone = true;
this._tryEnterComplete();
return;
}
if (this._gateTriggered) return;
console.error('[UImatch] 匹配流程超时', 'load=', this._loadDone, 'enterMove=', this._enterMoveDone);
this._onEnterFailed();
gg.game.recoverHome('match_stuck');
};
/** 预载本局战斗资源 */
private async _preloadBattleRes(): Promise<void> {
try {
await gg.game.preloadBattleForMatch();
} catch (err) {
console.error('[UImatch] preload battle res failed', String(err));
}
this._loadDone = true;
this._checkGateReady();
}
/**
* 匹配到对手且战斗页已打开 → 进入匹配完成:藏放大镜、揭示对手、播「站」;「战」一消失立刻拉开关页。
*/
private _tryEnterComplete(): void {
if (this._completeEntered || this._finishing) return;
if (!this._bindDone || !this._battleReady || !this._minWaitDone) return;
if (!this.isValid) return;
this._completeEntered = true;
this._stopRedHeadRoll();
gg.audio.playEffect({ name: '匹配完成', path: GAudioPath.SfxUi });
if (this.ui_matchLight?.isValid) this.ui_matchLight.active = false;
this._hideSearchSpine();
this._applyMatchReveal();
this._playSideCompleteAnims();
const sk = this._matchSk;
if (!sk?.isValid) {
console.warn('[UImatch] ui_match 缺少 sp.Skeleton,跳过「站」');
this._onStandComplete();
return;
}
const ok = BattleSpinePlayback.play(sk, MATCH_SPINE_STAND, { loop: false });
if (!ok) {
console.warn('[UImatch] ui_match 动画「站」不存在');
this._onStandComplete();
return;
}
sk.timeScale = 1;
this.unschedule(this._onStandComplete);
this.scheduleOnce(this._onStandComplete, MATCH_STAND_VANISH_SEC);
}
/** 「战」已消失:立刻拉开上下栏后关页 */
private _onStandComplete = (): void => {
if (!this.isValid) return;
if (this.ui_match?.isValid) this.ui_match.active = false;
this._standDone = true;
this._tryPlayExitMove();
};
/** 预载与合拢完成 → 立刻进战斗(不空等最短展示,加载与匹配中并行) */
private _checkGateReady(): void {
if (!this._loadDone || !this._enterMoveDone || this._gateTriggered) return;
if (!this.isValid) return;
this._gateTriggered = true;
const cb = this._finishCb;
if (cb) {
cb({
notifyBattleEntered: this._onBattleEntered,
notifyEnterFailed: this._onEnterFailed,
});
}
}
private _onBattleEntered = (): void => {
this._battleReady = true;
if (this.node?.isValid) {
this.node.setSiblingIndex(-1);
}
this._tryEnterComplete();
};
private _onEnterFailed = (): void => {
if (!this.isValid || this._finishing) return;
this._finishing = true;
void this.close(1).catch((err) => {
console.error('[UImatch] close failed', String(err));
});
};
/** 匹配完成动画走完 → 上下栏拉开后关页 */
private _tryPlayExitMove(): void {
if (!this._standDone || this._finishing || this._panelMoveKind === 'out') return;
if (!this.isValid) return;
if (this.node?.isValid) {
this.node.setSiblingIndex(-1);
}
this._playPanelExitMove();
}
/** 从屏外合拢到 design 位 */
private _playPanelEnterMove(): void {
this._playPanelMove('in', 0);
}
/** 从 design 位拉开到屏外 */
private _playPanelExitMove(): void {
this._playPanelMove('out', this._offscreenDy());
}
/**
* 上下栏同时位移
* @param kind 合拢 / 拉开
* @param offDy 拉开时的屏外偏移;合拢为 0
*/
private _playPanelMove(kind: 'in' | 'out', offDy: number): void {
this._stopPanelTweens();
this._panelMoveKind = kind;
this._panelMoveAnimLeft = 0;
this._tweenPanelToOffset(this.ui_top, this._topRest, offDy);
this._tweenPanelToOffset(this.ui_red, this._redRest, offDy);
this._tweenPanelToOffset(this.ui_redBox, this._redBoxRest, offDy);
this._tweenPanelToOffset(this.ui_bottom, this._bottomRest, -offDy);
this._tweenPanelToOffset(this.ui_bule, this._buleRest, -offDy);
this._tweenPanelToOffset(this.ui_blueBox, this._blueBoxRest, -offDy);
if (this._panelMoveAnimLeft <= 0) {
this._onPanelMoveTick();
} else {
this.unschedule(this._onPanelMoveWatchdog);
this.scheduleOnce(this._onPanelMoveWatchdog, PANEL_MOVE_SEC + 0.4);
}
}
/** 合拢/拉开 tween 丢失回调时强制收尾 */
private _onPanelMoveWatchdog = (): void => {
if (!this.isValid || this._panelMoveAnimLeft <= 0) return;
console.warn('[UImatch] 栏位移画超时');
this._panelMoveAnimLeft = 1;
this._onPanelMoveTick();
};
/**
* 单节点 tween 到 rest + dy
* @param node 目标
* @param rest design 坐标
* @param dy 纵向偏移
*/
private _tweenPanelToOffset(node: Node | null, rest: Vec3, dy: number): void {
if (!node?.isValid) return;
this._panelMoveAnimLeft++;
Tween.stopAllByTarget(node);
tween(node)
.to(PANEL_MOVE_SEC, { position: new Vec3(rest.x, rest.y + dy, rest.z) })
.call(this._onPanelMoveTick)
.start();
}
private _onPanelMoveTick = (): void => {
this._panelMoveAnimLeft--;
if (this._panelMoveAnimLeft > 0) return;
const kind = this._panelMoveKind;
this._panelMoveKind = null;
if (kind === 'in') {
this._onEnterMoveDone();
return;
}
if (kind === 'out') {
this._onExitMoveDone();
}
};
/** 合拢完成:显示匹配中、放大镜与光效,关首页,再允许进战斗 */
private _onEnterMoveDone(): void {
if (!this.isValid) return;
if (this.ui_match?.isValid) this.ui_match.active = true;
if (this.ui_spine?.isValid) this.ui_spine.active = true;
if (this.ui_matchLight?.isValid) this.ui_matchLight.active = true;
this._playMatchingLoop();
this._playSearchSpineLoop();
void gg.ui.closePanel(UIID.Home);
this._enterMoveDone = true;
this._checkGateReady();
}
/** 拉开完成:关匹配页 */
private _onExitMoveDone(): void {
if (!this.isValid || this._finishing) return;
this._finishing = true;
void this._closeAfterExitAnim();
}
/** 退出动画播完后关匹配页,并清理其它弹窗(保留局内页) */
private _closeAfterExitAnim = async (): Promise<void> => {
if (!this.isValid) return;
try {
await this.close(1);
} catch (err) {
console.error('[UImatch] close failed', String(err));
}
await gg.ui.closeAllPanel([UIID.BattleGame]);
};
/** 绑定己方段位/头像,并抽出对手 */
private async _bindMatchInfo(): Promise<void> {
await RankDataManager.ensureTable();
await gg.data.table.ensureTableLoaded(TableNames.Headicon);
if (!this.isValid) return;
const playerStars = RankDataManager.getPlayerRankStars();
const playerRankId = RankDataManager.getPlayerRankId();
this._aiInfo = this._rollAiDisplay();
this._setSideLevel(
this.ui_blueBox,
RankDataManager.formatRankLevelDesc(playerRankId, playerStars),
);
this._setSideLevel(this.ui_redBox, '???');
this._bindPlayerHeadFromSettings();
this._bindDone = true;
this._tryEnterComplete();
}
/** 蓝方 head:读取设置页存档头像 */
private _bindPlayerHeadFromSettings(): void {
const headNode = this._findHead(this.ui_blueBox);
if (!headNode?.isValid) {
console.warn('[UImatch] ui_blueBox 缺少 head');
return;
}
headNode.getOrAddComponent(SpriteLoad);
HeadIconManager.applyToNode(headNode);
}
/** 取消红方头像滚动 */
private _stopRedHeadRoll(): void {
this._rollGen++;
}
/** 红方头像滚动:匹配中循环,匹配完成时停在当前头像 */
private async _runRedHeadRoll(): Promise<void> {
const gen = ++this._rollGen;
await this._delay(RED_HEAD_WAIT_SEC);
if (!this.isValid || gen !== this._rollGen || this._completeEntered) return;
await gg.data.table.ensureTableLoaded(TableNames.Headicon);
if (!this.isValid || gen !== this._rollGen || this._completeEntered) return;
const headRows = HeadIconManager.getHeadIconList();
if (headRows.length <= 0) {
console.warn('[UImatch] Headicon 表无数据,跳过红方头像滚动');
return;
}
this._showRedHeadFilled();
while (this.isValid && gen === this._rollGen && !this._completeEntered) {
const row = headRows[Math.floor(Math.random() * headRows.length)];
this._setRedHeadRow(row);
await this._delay(RED_HEAD_ROLL_INTERVAL);
}
}
/** 匹配完成:隐藏 ui_spine 放大镜 */
private _hideSearchSpine(): void {
if (this.ui_spine?.isValid) this.ui_spine.active = false;
const sk = this._searchSk;
if (sk?.isValid) {
BattleSpinePlayback.clearPendingComplete(sk);
BattleSpinePlayback.resetTracks(sk);
}
}
/** 红方初始态:仅显示空底框 */
private _resetRedHeadUi(): void {
const side = this.ui_redBox;
if (!side?.isValid) return;
const emptyHead = side.getChildByName('emptyHead');
const headBg = side.getChildByName('headBg');
const head = side.getChildByName('head');
if (emptyHead) emptyHead.active = true;
if (headBg) headBg.active = false;
if (head) head.active = false;
}
/** 红方开始展示头像:显示底框与头像,隐藏空底框 */
private _showRedHeadFilled(): void {
const side = this.ui_redBox;
if (!side?.isValid) return;
const emptyHead = side.getChildByName('emptyHead');
const headBg = side.getChildByName('headBg');
const head = side.getChildByName('head');
if (emptyHead) emptyHead.active = false;
if (headBg) headBg.active = true;
if (head) head.active = true;
}
/**
* 绑定红方 Headicon 头像
* @param row Headicon 表行
*/
private _setRedHeadRow(row: ITableHeadicon | null | undefined): void {
HeadIconManager.applyRowToNode(this._findHead(this.ui_redBox), row);
}
/** 揭示对手段位昵称与双方胜率(播「站」时展示) */
private _applyMatchReveal(): void {
const ai = this._aiInfo;
if (!ai) return;
this._setSideLevel(this.ui_redBox, ai.rankName);
this._setWinRate(this.ui_redBox, ai.winRate);
}
/**
* 延迟
* @param sec 秒
*/
private _delay(sec: number): Promise<void> {
return new Promise((resolve) => {
this.scheduleOnce(resolve, sec);
});
}
/** 按 Const 11 抽出对手段位 */
private _rollAiDisplay(): AiMatchDisplay {
const oppId = MatchWinStats.ensurePendingOpponentRank();
const oppStars = MatchWinStats.peekPendingOpponentStars();
const stars = oppStars > 0 ? oppStars : RankDataManager.getPlayerRankStars();
const rankId = oppId > 0
? oppId
: (RankDataManager.resolveTierByStars(stars)?.id ?? 0);
return {
rankName: RankDataManager.formatRankLevelDesc(rankId, stars) || EMPTY_RANK_NAME,
winRate: MatchWinStats.getAiWinRate(rankId),
opponentRankId: rankId,
opponentStars: stars,
};
}
/**
* 查找头像节点
* @param side 蓝/红侧根
*/
private _findHead(side: Node | null | undefined): Node | null {
if (!side?.isValid) return null;
return side.getChildByName('headMask').getChildByName('head') ?? side.find('headMask/head');
}
/**
* 设置段位描述
* @param side 蓝/红侧
* @param rankName 段位文案
*/
private _setSideLevel(side: Node | null | undefined, rankName: string): void {
if (!side?.isValid) return;
const lb = side.getChildByName('level')?.getComponent(Label);
if (!lb) return;
lb.string = String(rankName ?? '').trim() || EMPTY_RANK_NAME;
}
/**
* 设置胜率文案
* @param side 蓝/红侧
* @param rate 0~100
*/
private _setWinRate(side: Node | null | undefined, rate: number): void {
if (!side?.isValid) return;
const lb = side.getChildByName('num')?.getComponent(Label);
if (!lb) return;
const n = Math.max(0, Math.min(100, Number(rate) || 0));
lb.string = n <= 0 ? '胜率:0%' : `胜率:${n.toFixed(1)}%`;
}
}