|
|
|
|
|
|
|
|
|
//*********************
|
|
|
|
|
// create by 流云
|
|
|
|
|
// time: Tue Aug 18 2026
|
|
|
|
|
// desc: 战斗界面 — 地图、出怪、背包刷新(召唤)
|
|
|
|
|
//*********************
|
|
|
|
|
import { _decorator, find, game, Label, Node, Sprite, Tween, tween, v3, Vec3, sp } from 'cc';
|
|
|
|
|
import { Auto_BattleGame } from './Auto_BattleGame';
|
|
|
|
|
import { CocosBattleMapHost } from '../../battle/map/CocosBattleMapHost';
|
|
|
|
|
import { applyMapTopologyToSession } from '../../battle/map/importTopologyFromMapManager';
|
|
|
|
|
import type { MapManager } from '../../battle/map/legacy/MapManager';
|
|
|
|
|
import { CocosBattleMovementBridge } from '../../battle/bridge/CocosBattleMovementBridge';
|
|
|
|
|
import { CocosBattleMonsterPresenter } from '../../battle/view/CocosBattleMonsterPresenter';
|
|
|
|
|
import { CampHpBarPresenter } from '../../battle/view/CampHpBarPresenter';
|
|
|
|
|
import { CocosBattleCombatPresenter } from '../../battle/view/CocosBattleCombatPresenter';
|
|
|
|
|
import { DamageTextSpawner } from '../../battle/view/DamageTextSpawner';
|
|
|
|
|
import { CocosBagBoardView } from '../../battle/view/CocosBagBoardView';
|
|
|
|
|
import { HitEffectResolver } from '../../battle/view/HitEffectResolver';
|
|
|
|
|
import { BATTLE_MONSTER_CCCLASS } from '../../battle/view/IBattleMonsterView';
|
|
|
|
|
import { getDiscountedBagNormalRefreshCost } from '../../battle/bag/BagPrepConfig';
|
|
|
|
|
import { resolveCarriedSummonCostReduceWan } from '../../manager/battleBuff/BattleBuffPassiveSkillUtil';
|
|
|
|
|
import { BattleBuffFunctionType } from '../../manager/battleBuff/BattleBuffTypes';
|
|
|
|
|
import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime';
|
|
|
|
|
import { BattleAudio } from '../../battle/audio/BattleAudio';
|
|
|
|
|
import { BattleUiPauseGate } from '../../battle/BattleUiPauseGate';
|
|
|
|
|
import { SkillController } from '../../battle/skill/SkillController';
|
|
|
|
|
import { SkillPresentationEnv } from '../../battle/skill/SkillPresentationEnv';
|
|
|
|
|
import { MonsterDataManager, MonsterType } from '../../manager/MonsterDataManager';
|
|
|
|
|
import { GEvent } from '../../../mx/module/event/GEvent';
|
|
|
|
|
import { BattleIllustratedGuideManager } from '../../manager/BattleIllustratedGuideManager';
|
|
|
|
|
import { UIID } from '../../game/ConfigRes';
|
|
|
|
|
import type { BattleEvent, EntityId } from 'db://assets/mx/module/battle';
|
|
|
|
|
import { BATTLE_EVENT_ALL, BattleEventType } from 'db://assets/mx/module/battle';
|
|
|
|
|
import { BattleGameDailyShopSkillView } from './BattleGameDailyShopSkillView';
|
|
|
|
|
import { BattleGamePassiveSkillView } from './BattleGamePassiveSkillView';
|
|
|
|
|
import { BattleGameOtherSkillView } from './BattleGameOtherSkillView';
|
|
|
|
|
import { BattleGameAiActiveSkillFxPresenter } from './BattleGameAiActiveSkillFxPresenter';
|
|
|
|
|
import { BattleGameDailyShopSkillDrag } from './BattleGameDailyShopSkillDrag';
|
|
|
|
|
import { BattleGameDailyShopSkillClick } from './BattleGameDailyShopSkillClick';
|
|
|
|
|
import { BattlePeachCoinFlyPresenter } from './BattlePeachCoinFlyPresenter';
|
|
|
|
|
import { BattleWeaponDropPresenter } from './BattleWeaponDropPresenter';
|
|
|
|
|
import { BattleGameItemAdPresenter } from './BattleGameItemAdPresenter';
|
|
|
|
|
import { BattleGameMenShenTrapController } from './BattleGameMenShenTrapController';
|
|
|
|
|
import { getBattleGameMonsterCrisisHintPresenter } from './BattleGameMonsterCrisisHintPresenter';
|
|
|
|
|
import { BattleGameKillPeachPresenter } from './BattleGameKillPeachPresenter';
|
|
|
|
|
import { getBattleBuffBaseThunderHost } from '../../battle/combat/BattleBuffBaseThunderHost';
|
|
|
|
|
import { BattleBuffSelectService } from '../../manager/battleBuff/BattleBuffSelectService';
|
|
|
|
|
import { BattleBuffInGameRefresh } from '../../battle/combat/BattleBuffInGameRefresh';
|
|
|
|
|
import { BattleGameBaseThunderPresenter } from './BattleGameBaseThunderPresenter';
|
|
|
|
|
import { BattleGameBossIntroPresenter } from './BattleGameBossIntroPresenter';
|
|
|
|
|
import { AiBattleBridge } from '../../battle/ai/bridge/AiBattleBridge';
|
|
|
|
|
import { AiHeadlessBagHost } from '../../battle/ai/runtime/AiHeadlessBagHost';
|
|
|
|
|
import type { AiActiveSkillCastFxInfo } from '../../battle/ai/runtime/AiActiveSkillHost';
|
|
|
|
|
import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback';
|
|
|
|
|
import { TweenBreathe } from 'db://assets/mx/components/tween/TweenBreathe';
|
|
|
|
|
import { DailyShopOwnedDesc } from '../PopDailyShopLottery/DailyShopLotteryIcon';
|
|
|
|
|
import { BattleFirstGuidePresenter } from './BattleFirstGuidePresenter';
|
|
|
|
|
import { LangTxt } from 'db://assets/mx/module/lang/LangTxt';
|
|
|
|
|
import { ConstNumType } from '../../game/GameDataEnums';
|
|
|
|
|
import { ClientErrorReporter } from '../../../mx/tools/ClientErrorReporter';
|
|
|
|
|
import { PerfSampler } from '../../game/PerfSampler';
|
|
|
|
|
import { isElementGlyphType, MergeEventType, type MergeEvent } from 'db://assets/mx/module/merge';
|
|
|
|
|
import { WeaponComposeConfigProvider } from '../../battle/bag/WeaponComposeConfigProvider';
|
|
|
|
|
|
|
|
|
|
const { ccclass } = _decorator;
|
|
|
|
|
|
|
|
|
|
/** 召唤按钮进度满后 Q 弹第一下相对缩放增量 */
|
|
|
|
|
const REFRESH_BOUNCE_AMP = 0.1;
|
|
|
|
|
/** 单段弹起/回落时长(秒) */
|
|
|
|
|
const REFRESH_BOUNCE_SEC = 0.22;
|
|
|
|
|
/** 开局未放置数字则强制开怪的等待秒数 */
|
|
|
|
|
const FIRST_PLACE_COMBAT_TIMEOUT_SEC = 10;
|
|
|
|
|
/** 看视频加金币期间暂停 token */
|
|
|
|
|
const COIN_AD_PAUSE_TOKEN = 'ad:battleCoin';
|
|
|
|
|
|
|
|
|
|
/** 砚台 Spine:出字过程/待机均播「待机」(资源内「出字」为空动画) */
|
|
|
|
|
const INKSTONE_SPINE_SPAWN = '待机';
|
|
|
|
|
|
|
|
|
|
@ccclass('UIBattleGame')
|
|
|
|
|
export class UIBattleGame extends Auto_BattleGame {
|
|
|
|
|
|
|
|
|
|
private _battle: ProjectBattleHandle | null = null;
|
|
|
|
|
private _mapHost: CocosBattleMapHost | null = null;
|
|
|
|
|
private readonly _monsterPresenter = new CocosBattleMonsterPresenter();
|
|
|
|
|
/** 己方/敌方营地血条 */
|
|
|
|
|
private readonly _campHpBar = new CampHpBarPresenter();
|
|
|
|
|
private _combatPresenter: CocosBattleCombatPresenter | null = null;
|
|
|
|
|
private _movementBridge: CocosBattleMovementBridge | null = null;
|
|
|
|
|
private _bagView: CocosBagBoardView | null = null;
|
|
|
|
|
/** 普攻/技能表现:监听 PresentationCue → HeroActor */
|
|
|
|
|
private _skillController: SkillController | null = null;
|
|
|
|
|
/** 召唤按钮基准缩放 */
|
|
|
|
|
private readonly _refreshBtnScaleHome = new Vec3(1, 1, 1);
|
|
|
|
|
/** 本段「桃子足够」是否已播过满进度 Q 弹 */
|
|
|
|
|
private _refreshBounceHinted = false;
|
|
|
|
|
/** 战斗 skill 描述浮层 */
|
|
|
|
|
private _ownedDesc: DailyShopOwnedDesc | null = null;
|
|
|
|
|
/** 敌方 skill 描述(ui_otherDesc) */
|
|
|
|
|
private _otherOwnedDesc: DailyShopOwnedDesc | null = null;
|
|
|
|
|
private _combatStarted = false;
|
|
|
|
|
private _onMapGridsReady: ((map: MapManager) => void) | null = null;
|
|
|
|
|
private readonly _monsterDeathFx = new HitEffectResolver();
|
|
|
|
|
private _deathFxParent: Node | null = null;
|
|
|
|
|
private _onBattleEventBound: ((e: BattleEvent) => void) | null = null;
|
|
|
|
|
/** 每日商店主动 buff:skill1 / skill2 */
|
|
|
|
|
private _dailyShopSkillView: BattleGameDailyShopSkillView | null = null;
|
|
|
|
|
/** 每日商店被动 buff:ui_selfskill2List */
|
|
|
|
|
private _passiveSkillView: BattleGamePassiveSkillView | null = null;
|
|
|
|
|
/** 敌方 AI 主/被动:ui_otherskill1List / ui_otherskill2List */
|
|
|
|
|
private _otherSkillView: BattleGameOtherSkillView | null = null;
|
|
|
|
|
/** AI 主动施放:复制图标飞向受体 */
|
|
|
|
|
private _aiActiveSkillFx: BattleGameAiActiveSkillFxPresenter | null = null;
|
|
|
|
|
/** 主动 buff 拖到地图施放 */
|
|
|
|
|
private _dailyShopSkillDrag: BattleGameDailyShopSkillDrag | null = null;
|
|
|
|
|
/** 门神盾牌陷阱 */
|
|
|
|
|
private _menShenTrap: BattleGameMenShenTrapController | null = null;
|
|
|
|
|
/** btnItem 门神盾牌广告 */
|
|
|
|
|
private _itemAd: BattleGameItemAdPresenter | null = null;
|
|
|
|
|
/** 御雷珠被动落雷表现 */
|
|
|
|
|
private _baseThunderFx: BattleGameBaseThunderPresenter | null = null;
|
|
|
|
|
/** 敌方 Boss 出场弹窗 */
|
|
|
|
|
private _bossIntro: BattleGameBossIntroPresenter | null = null;
|
|
|
|
|
/** 点击类主动 buff(Phase1 占位) */
|
|
|
|
|
private _dailyShopSkillClick: BattleGameDailyShopSkillClick | null = null;
|
|
|
|
|
/** 回收/奖励:coinFly 飘仙桃 */
|
|
|
|
|
private _peachCoinFly: BattlePeachCoinFlyPresenter | null = null;
|
|
|
|
|
/** 赏金令:击杀小怪概率额外金币 */
|
|
|
|
|
private _killPeach: BattleGameKillPeachPresenter | null = null;
|
|
|
|
|
/** 击杀武器碎片掉落 */
|
|
|
|
|
private _weaponDropFx: BattleWeaponDropPresenter | null = null;
|
|
|
|
|
/** 掉落表现用死亡坐标 */
|
|
|
|
|
private readonly _resolveWeaponDropPos = (victim: EntityId): Vec3 | null => this._monsterDeathWorldPos(victim);
|
|
|
|
|
/** 首次进战点召唤 / 拖字弱引导 */
|
|
|
|
|
private readonly _firstGuide = new BattleFirstGuidePresenter();
|
|
|
|
|
/** 倍增卡:本次刷新待入场结束后掷骰升级的槽 */
|
|
|
|
|
private _pendingDigitDoubleSlots: number[] = [];
|
|
|
|
|
/** 波间提示:第 N 波怪物来袭 */
|
|
|
|
|
private _lbWaveTip: Label | null = null;
|
|
|
|
|
/** 波间倒计时 mm:ss */
|
|
|
|
|
private _lbWaveTipTime: Label | null = null;
|
|
|
|
|
/** btnIllustrated 子节点 spine */
|
|
|
|
|
private _inkstoneSkel: sp.Skeleton | null = null;
|
|
|
|
|
/** 本局是否已触发过第 5 波图鉴引导 */
|
|
|
|
|
private _illustratedHintShownThisBattle = false;
|
|
|
|
|
/** 桃 UI 缓存,避免每帧写 Label */
|
|
|
|
|
private _peachUiGold = -1;
|
|
|
|
|
/** 召唤消耗缓存 */
|
|
|
|
|
private _peachUiCost = -1;
|
|
|
|
|
/** 金币广告已用次数缓存 */
|
|
|
|
|
private _peachUiCoinAdUsed = -1;
|
|
|
|
|
/** btnCoin/视频标 */
|
|
|
|
|
private _coinAdVideoIcon: Node | null = null;
|
|
|
|
|
/** 看视频加金币进行中 */
|
|
|
|
|
private _coinAdPending = false;
|
|
|
|
|
/** 波次 UI 缓存 */
|
|
|
|
|
private _waveUiIdx = -999;
|
|
|
|
|
/** 已提示过配置缺失的下一波下标 */
|
|
|
|
|
private _nextWaveMiss = -2;
|
|
|
|
|
/** 已发过结算金币的已结束波次下标;首波倒计时 waveIndex 为 -1,不发 */
|
|
|
|
|
private _waveGoldIndex = -2;
|
|
|
|
|
/** AI 技能条降频 */
|
|
|
|
|
private _aiSkillUiFrame = 0;
|
|
|
|
|
/** AI 未运行告警节流(秒) */
|
|
|
|
|
private _aiWatchdogSec = 0;
|
|
|
|
|
|
|
|
|
|
/** 刷新按钮世界坐标,供背包出字抛物线起点 */
|
|
|
|
|
private readonly _inkstoneWorldPos = (): Readonly<Vec3> | null => {
|
|
|
|
|
const n = this.btnRefresh;
|
|
|
|
|
return n?.isValid ? n.worldPosition : null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 出字开始:播资源名「待机」 */
|
|
|
|
|
private readonly _onInkstoneEntranceStart = (): void => {
|
|
|
|
|
this._playInkstoneSpine(INKSTONE_SPINE_SPAWN, true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 出字结束:回待机(资源「出字」为空动画,勿强播),再结算倍增卡 */
|
|
|
|
|
private readonly _onInkstoneEntranceEnd = (): void => {
|
|
|
|
|
this._playInkstoneSpine(INKSTONE_SPINE_SPAWN, true);
|
|
|
|
|
this._flushPendingDigitDouble();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 飞入结束后(或入场未播/被清锁时)对本次刷新字掷骰 level×2 */
|
|
|
|
|
private _flushPendingDigitDouble(): void {
|
|
|
|
|
const slots = this._pendingDigitDoubleSlots;
|
|
|
|
|
this._pendingDigitDoubleSlots = [];
|
|
|
|
|
if (!slots.length) return;
|
|
|
|
|
const bag = this._battle?.bagSession;
|
|
|
|
|
if (!bag) return;
|
|
|
|
|
bag.applyDigitDoubleAfterRefresh(slots);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 尽早关掉召唤按钮 TweenBreathe,避免 onEnable 抢缩放 */
|
|
|
|
|
onLoad(): void {
|
|
|
|
|
super.onLoad();
|
|
|
|
|
this._disableRefreshBreathe();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onInit(): void {
|
|
|
|
|
this.initUI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onEnable(): void {
|
|
|
|
|
super.onEnable();
|
|
|
|
|
this._ownedDesc?.bindDismiss();
|
|
|
|
|
GEvent.Ins.on(GEvent.HideBattleIllustratedHint, this._onHideIllustratedHint, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDisable(): void {
|
|
|
|
|
GEvent.Ins.off(GEvent.HideBattleIllustratedHint, this._onHideIllustratedHint, this);
|
|
|
|
|
this._ownedDesc?.unbindDismiss();
|
|
|
|
|
this._otherOwnedDesc?.unbindDismiss();
|
|
|
|
|
this._teardownBattleView();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 关页/end 对局前拆视图,避免 session.dispose 后 Spine/怪物仍同步 */
|
|
|
|
|
teardownBeforeDestroy(): void {
|
|
|
|
|
this._teardownBattleView();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 初始化 skill 描述浮层(ui_descLeft / ui_descRight) */
|
|
|
|
|
private _ensureOwnedDesc(): void {
|
|
|
|
|
if (this._ownedDesc) return;
|
|
|
|
|
const uiDescLeft = find('ui_descLeft', this.node);
|
|
|
|
|
const uiDescRight = find('ui_descRight', this.node);
|
|
|
|
|
if (!uiDescLeft?.isValid && !uiDescRight?.isValid) {
|
|
|
|
|
console.log('[UIBattleGame] ui_descLeft/ui_descRight 缺失');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._ownedDesc = new DailyShopOwnedDesc(uiDescLeft, uiDescRight, {
|
|
|
|
|
activeYOffset: 60,
|
|
|
|
|
passiveYOffset: 40,
|
|
|
|
|
});
|
|
|
|
|
this._ownedDesc.init();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 初始化敌方 skill 描述浮层(ui_otherDesc) */
|
|
|
|
|
private _ensureOtherOwnedDesc(): void {
|
|
|
|
|
if (this._otherOwnedDesc) return;
|
|
|
|
|
const uiOtherDesc = find('ui_otherDesc', this.node);
|
|
|
|
|
if (!uiOtherDesc?.isValid) {
|
|
|
|
|
console.log('[UIBattleGame] ui_otherDesc 缺失');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._otherOwnedDesc = new DailyShopOwnedDesc(uiOtherDesc, uiOtherDesc, {
|
|
|
|
|
activeYOffset: -30,
|
|
|
|
|
passiveYOffset: -20,
|
|
|
|
|
});
|
|
|
|
|
this._otherOwnedDesc.init();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 互斥显示我方/敌方 skill 描述 */
|
|
|
|
|
private _linkSkillDescPeers(): void {
|
|
|
|
|
this._ownedDesc?.setPeerDesc(this._otherOwnedDesc);
|
|
|
|
|
this._otherOwnedDesc?.setPeerDesc(this._ownedDesc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initUI(): void {
|
|
|
|
|
this._battle = gg.game.CurentBattle as ProjectBattleHandle | null;
|
|
|
|
|
if (!this._battle) {
|
|
|
|
|
console.error('[UIBattleGame] CurentBattle missing');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._ensureOwnedDesc();
|
|
|
|
|
this._ensureOtherOwnedDesc();
|
|
|
|
|
this._linkSkillDescPeers();
|
|
|
|
|
this._showRefreshBg1(this._isRefreshGoldEnough());
|
|
|
|
|
this._disableRefreshBreathe();
|
|
|
|
|
this.btnRefresh?.getScale(this._refreshBtnScaleHome);
|
|
|
|
|
this._lbWaveTip = this.ui_nextWaveTime?.getChildByName('lb1')?.getComponent(Label) ?? null;
|
|
|
|
|
this._lbWaveTipTime = this.ui_nextWaveTime?.getChildByName('lb2')?.getComponent(Label) ?? null;
|
|
|
|
|
if (!this.ui_nextWaveTime?.isValid) {
|
|
|
|
|
console.log('[UIBattleGame] ui_nextWaveTime 节点缺失');
|
|
|
|
|
} else if (!this._lbWaveTip || !this._lbWaveTipTime) {
|
|
|
|
|
console.log('[UIBattleGame] ui_nextWaveTime 缺少 lb1 或 lb2');
|
|
|
|
|
}
|
|
|
|
|
this._dailyShopSkillView = new BattleGameDailyShopSkillView(this.ui_selfskill1List);
|
|
|
|
|
this._dailyShopSkillView.bindBattle(this._battle);
|
|
|
|
|
this._dailyShopSkillView.setOwnedDesc(this._ownedDesc);
|
|
|
|
|
this._dailyShopSkillView.setOnDisplayChanged(() => this._onDailyShopSkillCastChanged());
|
|
|
|
|
this._dailyShopSkillView.refresh();
|
|
|
|
|
this._passiveSkillView = new BattleGamePassiveSkillView(this.ui_selfskill2List);
|
|
|
|
|
this._passiveSkillView.bindBattle(this._battle);
|
|
|
|
|
this._passiveSkillView.setOwnedDesc(this._ownedDesc);
|
|
|
|
|
this._bindGoldMineCoinFly();
|
|
|
|
|
this._passiveSkillView.refresh();
|
|
|
|
|
this._otherSkillView = new BattleGameOtherSkillView(this.ui_otherskill1List, this.ui_otherskill2List);
|
|
|
|
|
this._otherSkillView.setOwnedDesc(this._otherOwnedDesc);
|
|
|
|
|
this._otherSkillView.refresh();
|
|
|
|
|
if (this.ui_money?.isValid) this.ui_money.active = false;
|
|
|
|
|
this._coinAdVideoIcon = this.btnCoin?.getChildByName('视频标') ?? null;
|
|
|
|
|
this._waveUiIdx = -999;
|
|
|
|
|
this._peachUiGold = -1;
|
|
|
|
|
this._peachUiCost = -1;
|
|
|
|
|
this._peachUiCoinAdUsed = -1;
|
|
|
|
|
this._refreshPeachUi();
|
|
|
|
|
this._refreshWaveUi();
|
|
|
|
|
this._syncIllustratedHint(false);
|
|
|
|
|
this._ownedDesc?.bindDismiss();
|
|
|
|
|
this._otherOwnedDesc?.bindDismiss();
|
|
|
|
|
this._setupMapAndSpawn();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update(dt: number): void {
|
|
|
|
|
try {
|
|
|
|
|
gg.battle.tick(dt);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
ClientErrorReporter.report('UIBattleGame_battleTick', e);
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
AiBattleBridge.inst.tick(dt);
|
|
|
|
|
if (this._battle && !this._battle.IsGameOver && !AiBattleBridge.inst.running) {
|
|
|
|
|
this._aiWatchdogSec += dt;
|
|
|
|
|
if (this._aiWatchdogSec >= 3) {
|
|
|
|
|
this._aiWatchdogSec = 0;
|
|
|
|
|
console.error('[UIBattleGame] 对手 AI 未运行(start 失败或已 stop)');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this._aiWatchdogSec = 0;
|
|
|
|
|
}
|
|
|
|
|
if (!this._battle?.IsGamePause) {
|
|
|
|
|
this._dailyShopSkillView?.tick(dt);
|
|
|
|
|
this._passiveSkillView?.tick(dt);
|
|
|
|
|
this._aiSkillUiFrame++;
|
|
|
|
|
if (this._aiSkillUiFrame >= 6) {
|
|
|
|
|
this._aiSkillUiFrame = 0;
|
|
|
|
|
this._otherSkillView?.syncFromAi();
|
|
|
|
|
}
|
|
|
|
|
this._bagView?.tickPeachTreePresentations();
|
|
|
|
|
getBattleGameMonsterCrisisHintPresenter().tick();
|
|
|
|
|
this._campHpBar.tick();
|
|
|
|
|
}
|
|
|
|
|
this._menShenTrap?.tick(dt);
|
|
|
|
|
this._itemAd?.tick(dt);
|
|
|
|
|
this._refreshPeachUi();
|
|
|
|
|
this._refreshWaveUi();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
ClientErrorReporter.report('UIBattleGame_uiTick', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDestroy(): void {
|
|
|
|
|
this._teardownBattleView();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _setupMapAndSpawn(): void {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
const parent = this.ui_mapParentNode;
|
|
|
|
|
if (!battle || !parent) {
|
|
|
|
|
console.error('[UIBattleGame] battle or map parent missing');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._mapHost = parent.getComponent(CocosBattleMapHost)
|
|
|
|
|
?? parent.addComponent(CocosBattleMapHost);
|
|
|
|
|
this._mapHost.mapRoot = parent;
|
|
|
|
|
|
|
|
|
|
const mapNode = this._mapHost.loadMap(battle.mapPrefabName);
|
|
|
|
|
if (!mapNode) return;
|
|
|
|
|
|
|
|
|
|
const mapMgr = mapNode.getComponent('MapManager') as MapManager | null;
|
|
|
|
|
if (mapMgr?.gridsInitialized) {
|
|
|
|
|
this._onMapReady(mapMgr, mapNode);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._onMapGridsReady = (map: MapManager) => {
|
|
|
|
|
this._onMapReady(map, mapNode);
|
|
|
|
|
};
|
|
|
|
|
GEvent.Ins.on(GEvent.MapGridsReady, this._onMapGridsReady, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _onMapReady(map: MapManager, mapNode: Node): void {
|
|
|
|
|
if (this._onMapGridsReady) {
|
|
|
|
|
GEvent.Ins.off(GEvent.MapGridsReady, this._onMapGridsReady, this);
|
|
|
|
|
this._onMapGridsReady = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
const parent = this.ui_mapParentNode;
|
|
|
|
|
if (!battle || !parent?.isValid) return;
|
|
|
|
|
|
|
|
|
|
if (!applyMapTopologyToSession(battle.session, map, 'pvp')) {
|
|
|
|
|
console.error('[UIBattleGame] topology import failed');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._deathFxParent = mapNode;
|
|
|
|
|
battle.setDeathFxParent(mapNode);
|
|
|
|
|
this._monsterPresenter.bind(battle, mapNode, parent);
|
|
|
|
|
this._bindBaseThunder(battle);
|
|
|
|
|
this._bindBossIntro(battle);
|
|
|
|
|
this._movementBridge = new CocosBattleMovementBridge(this._monsterPresenter);
|
|
|
|
|
battle.session.setBridge(this._movementBridge);
|
|
|
|
|
|
|
|
|
|
this._combatPresenter?.unbind();
|
|
|
|
|
this._combatPresenter = new CocosBattleCombatPresenter();
|
|
|
|
|
this._combatPresenter.bind(battle, this._monsterPresenter, parent);
|
|
|
|
|
|
|
|
|
|
this._bindSkillPresentationEnv(battle, map);
|
|
|
|
|
this._bindBagBoard(map);
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
this._bindWeaponDrop(battle);
|
|
|
|
|
this._bindSkillController(map);
|
|
|
|
|
this._bindBattleEvents(battle);
|
|
|
|
|
this._refreshPeachUi();
|
|
|
|
|
this._refreshWaveUi();
|
|
|
|
|
|
|
|
|
|
// 须在 AI start 之后再挂血条:AI 镇宅牌会改 wallMaxHp,挂早了格宽会按 3 算
|
|
|
|
|
AiBattleBridge.inst.setMapTextFlyLayer(this.ui_fly ?? this.ui_mapParentNode);
|
|
|
|
|
AiBattleBridge.inst.setTextGridLayer(this.ui_textGrids);
|
|
|
|
|
try {
|
|
|
|
|
if (AiBattleBridge.inst.start(battle, map, undefined, this.node, this.ui_textList)) {
|
|
|
|
|
this._otherSkillView?.refresh();
|
|
|
|
|
this._bindAiActiveSkillFx(map);
|
|
|
|
|
this._bindAiGoldMineCoinFly();
|
|
|
|
|
} else {
|
|
|
|
|
console.error('[UIBattleGame] AiBattleBridge.start 失败,对手 AI 未启动');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('[UIBattleGame] AiBattleBridge.start 异常', String(e));
|
|
|
|
|
}
|
|
|
|
|
this._bindUnlockCoinFly(battle);
|
|
|
|
|
this._bindBattleBuffSelect(battle);
|
|
|
|
|
this._bindPeerMapToolApplied(battle);
|
|
|
|
|
this._campHpBar.bind(battle, map);
|
|
|
|
|
getBattleGameMonsterCrisisHintPresenter().bind(battle, map);
|
|
|
|
|
this._firstGuide.bind(this.ui_guide, battle.bagSession, this.btnRefresh);
|
|
|
|
|
// Widget 布局完成后再对齐一次,避免手指仍停在预制体旧坐标
|
|
|
|
|
this.scheduleOnce(this._realignFirstGuideFinger, 0);
|
|
|
|
|
PerfSampler.beginBattle();
|
|
|
|
|
this._listenFirstPlaceCombat();
|
|
|
|
|
this._refreshWaveUi();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 引导手指对齐召唤按钮 */
|
|
|
|
|
private readonly _realignFirstGuideFinger = (): void => {
|
|
|
|
|
this._firstGuide.realignFinger();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 开局不立刻出怪:本机首次放数字开怪,或超时后强制开怪。
|
|
|
|
|
*/
|
|
|
|
|
private _listenFirstPlaceCombat(): void {
|
|
|
|
|
this._unbindFirstPlaceCombat();
|
|
|
|
|
const bag = this._battle?.bagSession;
|
|
|
|
|
if (!bag) {
|
|
|
|
|
console.error('[UIBattleGame] 无背包会话,无法按首次放字开怪');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
bag.on(MergeEventType.TilePlaced, this._onFirstPlayerNumberPlaced);
|
|
|
|
|
this.scheduleOnce(this._onFirstPlaceCombatTimeout, FIRST_PLACE_COMBAT_TIMEOUT_SEC);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 卸掉首次放字开怪监听与超时 */
|
|
|
|
|
private _unbindFirstPlaceCombat(): void {
|
|
|
|
|
this.unschedule(this._onFirstPlaceCombatTimeout);
|
|
|
|
|
this._battle?.bagSession?.off(MergeEventType.TilePlaced, this._onFirstPlayerNumberPlaced);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 本机首次放下五行数字后开波。
|
|
|
|
|
* @param event 地图落子事件
|
|
|
|
|
*/
|
|
|
|
|
private readonly _onFirstPlayerNumberPlaced = (event: MergeEvent): void => {
|
|
|
|
|
if (this._combatStarted) return;
|
|
|
|
|
if (event.type !== MergeEventType.TilePlaced) return;
|
|
|
|
|
const tileType = Math.floor(Number(WeaponComposeConfigProvider.instance.getWeapon2Type(event.weapon2Id)));
|
|
|
|
|
if (!isElementGlyphType(tileType)) return;
|
|
|
|
|
this._startCombatFromFirstPlaceGate();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 超时仍未放数字:强制开怪 */
|
|
|
|
|
private readonly _onFirstPlaceCombatTimeout = (): void => {
|
|
|
|
|
if (this._combatStarted) return;
|
|
|
|
|
this._startCombatFromFirstPlaceGate();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 首次放字 / 超时共用:开波并卸监听 */
|
|
|
|
|
private _startCombatFromFirstPlaceGate(): void {
|
|
|
|
|
if (this._combatStarted) return;
|
|
|
|
|
this._unbindFirstPlaceCombat();
|
|
|
|
|
this._combatStarted = true;
|
|
|
|
|
gg.battle.startCombat();
|
|
|
|
|
this._refreshWaveUi();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 绑定 HitAfter/Buff/死亡等战斗事件(须在 monsterPresenter 之后) */
|
|
|
|
|
private _bindBattleEvents(battle: ProjectBattleHandle): void {
|
|
|
|
|
if (this._onBattleEventBound) {
|
|
|
|
|
battle.off(BATTLE_EVENT_ALL, this._onBattleEventBound);
|
|
|
|
|
}
|
|
|
|
|
this._onBattleEventBound = (e) => this._onBattleEvent(e);
|
|
|
|
|
battle.on(BATTLE_EVENT_ALL, this._onBattleEventBound);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _onBattleEvent(event: BattleEvent): void {
|
|
|
|
|
if (event.type === BattleEventType.EntityKilled) {
|
|
|
|
|
this._playMonsterDeathFx(event.victim);
|
|
|
|
|
this._playKillPeachFly(event.killer, event.victim);
|
|
|
|
|
this._killPeach?.onEntityKilled(event.killer, event.victim);
|
|
|
|
|
this._weaponDropFx?.onEntityKilled(event.victim);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (event.type === BattleEventType.WaveStart) {
|
|
|
|
|
this._tryShowIllustratedHintOnWave(event.waveIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 非首波倒计时开始时,本机与 AI 各发一次金币。首波倒计时 waveIndex 仍是 -1。
|
|
|
|
|
* 飞金币只播本机。
|
|
|
|
|
* @param endedWaveIndex 刚结束、正在等待下一波的波次下标
|
|
|
|
|
* @param remain 倒计时剩余秒
|
|
|
|
|
*/
|
|
|
|
|
private _grantWaveCountdownGold(endedWaveIndex: number, remain: number): void {
|
|
|
|
|
if (endedWaveIndex < 0 || !(remain > 0)) return;
|
|
|
|
|
if (this._waveGoldIndex === endedWaveIndex) return;
|
|
|
|
|
this._waveGoldIndex = endedWaveIndex;
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
if (!battle || battle.IsGameOver) return;
|
|
|
|
|
const gold = this._waveSettleGold();
|
|
|
|
|
if (!(gold > 0)) return;
|
|
|
|
|
battle.addPlayerGold(battle.localPlayerId, gold);
|
|
|
|
|
this._refreshPeachUi();
|
|
|
|
|
this._playWaveSettleCoin(gold);
|
|
|
|
|
this._grantWaveGoldToOthers(gold);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 非本机玩家槽入账同样的波次金币,不播飞金币。
|
|
|
|
|
* @param gold 本次入账金币
|
|
|
|
|
*/
|
|
|
|
|
private _grantWaveGoldToOthers(gold: number): void {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
if (!battle) return;
|
|
|
|
|
const players = battle.session.players;
|
|
|
|
|
for (let i = 0; i < players.length; i++) {
|
|
|
|
|
const id = players[i].id;
|
|
|
|
|
if (id === battle.localPlayerId) continue;
|
|
|
|
|
battle.addPlayerGold(id, gold);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 常量表 41 para1:每波结算金币。缺失或非法时打日志并返回 0。
|
|
|
|
|
*/
|
|
|
|
|
private _waveSettleGold(): number {
|
|
|
|
|
const list = gg.data.table.getConstArray(ConstNumType.WaveSettleGold);
|
|
|
|
|
if (!list.length) {
|
|
|
|
|
console.log('[UIBattleGame] Const 每波结算金钱为空', ConstNumType.WaveSettleGold);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
const gold = Math.floor(list[0]);
|
|
|
|
|
if (!Number.isFinite(gold) || gold < 0) {
|
|
|
|
|
console.log('[UIBattleGame] Const 每波结算金钱非法', list[0]);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return gold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 常量表 42 para1:每波飘金币个数。缺失或非法时打日志并返回 0。
|
|
|
|
|
*/
|
|
|
|
|
private _waveSettleCoinCount(): number {
|
|
|
|
|
const list = gg.data.table.getConstArray(ConstNumType.WaveSettleCoinCount);
|
|
|
|
|
if (!list.length) {
|
|
|
|
|
console.log('[UIBattleGame] Const 飘金币数量为空', ConstNumType.WaveSettleCoinCount);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
const count = Math.floor(list[0]);
|
|
|
|
|
if (!Number.isFinite(count) || count <= 0) {
|
|
|
|
|
console.log('[UIBattleGame] Const 飘金币数量非法', list[0]);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按次数生成金币挂到 ui_fly,先在不同位置间隔冒出,再一起飞向 btnCoin。
|
|
|
|
|
* 单枚显示为结算金币除以生成次数;入账仍是结算金币总额。
|
|
|
|
|
* @param gold 本次入账的金币总数
|
|
|
|
|
*/
|
|
|
|
|
private _playWaveSettleCoin(gold: number): void {
|
|
|
|
|
const layer = this.ui_fly;
|
|
|
|
|
if (!layer?.isValid) {
|
|
|
|
|
console.log('[UIBattleGame] 波次结算金币缺少 ui_fly');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const count = this._waveSettleCoinCount();
|
|
|
|
|
if (!(count > 0)) return;
|
|
|
|
|
const each = gold / count;
|
|
|
|
|
if (each !== Math.floor(each)) {
|
|
|
|
|
console.log('[UIBattleGame] 每波结算金钱不能被飘金币数量整除', gold, count);
|
|
|
|
|
}
|
|
|
|
|
const show = Math.floor(each);
|
|
|
|
|
if (!(show > 0)) {
|
|
|
|
|
console.log('[UIBattleGame] 单枚金币显示数量为 0', gold, count);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
if (!layer.active) layer.active = true;
|
|
|
|
|
this._peachCoinFly?.playWaveCoin(layer, layer.worldPosition, show, count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 击杀掉桃表现:本机飞向 ui_money;AI 归属原地弹跳(金已入 AI 槽)。
|
|
|
|
|
* @param killer 击杀者
|
|
|
|
|
* @param victim 死亡实体
|
|
|
|
|
*/
|
|
|
|
|
private _playKillPeachFly(killer: EntityId, victim: EntityId): void {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
const fly = this._peachCoinFly;
|
|
|
|
|
if (!battle || !fly) return;
|
|
|
|
|
const peach = battle.resolveKillPeach(victim);
|
|
|
|
|
if (!(peach > 0)) return;
|
|
|
|
|
const from = this._monsterDeathWorldPos(victim);
|
|
|
|
|
if (!from) {
|
|
|
|
|
console.log('[UIBattleGame] 击杀掉桃无死亡坐标', victim);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const ownerId = battle.resolveKillPeachOwnerPlayerId(killer, victim);
|
|
|
|
|
if (ownerId === battle.localPlayerId) {
|
|
|
|
|
fly.playKillDrop(from, peach);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fly.playKillDropLocal(from, peach);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 击杀时怪物世界坐标(节点优先,否则 presenter 缓存)。
|
|
|
|
|
* @param victim 死亡实体
|
|
|
|
|
*/
|
|
|
|
|
private _monsterDeathWorldPos(victim: EntityId): Vec3 | null {
|
|
|
|
|
const node = this._monsterPresenter.getEntityNodeMap().get(victim);
|
|
|
|
|
if (node?.isValid) {
|
|
|
|
|
const view = node.getComponent(BATTLE_MONSTER_CCCLASS) as {
|
|
|
|
|
getPosByAtkPosIndex?(idx: number): Vec3;
|
|
|
|
|
} | null;
|
|
|
|
|
return (view?.getPosByAtkPosIndex?.(2) ?? node.worldPosition).clone();
|
|
|
|
|
}
|
|
|
|
|
return this._monsterPresenter.getEntityWorldPos(victim);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 击杀时在怪物坐标播 prefab/死亡;五行小怪走自身「系小怪2」轨,不播外部死亡特效 */
|
|
|
|
|
private _playMonsterDeathFx(victim: EntityId): void {
|
|
|
|
|
const parent = this._deathFxParent;
|
|
|
|
|
if (!parent?.isValid) return;
|
|
|
|
|
|
|
|
|
|
const node = this._monsterPresenter.getEntityNodeMap().get(victim);
|
|
|
|
|
if (node?.isValid) {
|
|
|
|
|
const view = node.getComponent(BATTLE_MONSTER_CCCLASS) as { Data?: ITableMonster } | null;
|
|
|
|
|
if (view?.Data?.type === MonsterType.TreasureBox) return;
|
|
|
|
|
if (view?.Data && MonsterDataManager.isElementSmallMonster(view.Data)) return;
|
|
|
|
|
}
|
|
|
|
|
const worldPos = this._monsterDeathWorldPos(victim);
|
|
|
|
|
if (!worldPos) return;
|
|
|
|
|
this._monsterDeathFx.playMonsterDeathAt(parent, worldPos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 离场时回收未播完的死亡特效 */
|
|
|
|
|
private _recycleOrphanMonsterDeathFx(): void {
|
|
|
|
|
const parent = this._deathFxParent;
|
|
|
|
|
if (!parent?.isValid) return;
|
|
|
|
|
for (const child of parent.children.slice()) {
|
|
|
|
|
if (!child?.isValid || child.name !== '死亡') continue;
|
|
|
|
|
const sk = child.getComponent(sp.Skeleton) ?? child.getComponentInChildren(sp.Skeleton);
|
|
|
|
|
sk?.setCompleteListener(null);
|
|
|
|
|
gg.res.putNode(child);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定 Skill 表现层共享环境(ui_fly / 怪物坐标) */
|
|
|
|
|
private _bindSkillPresentationEnv(battle: ProjectBattleHandle, map: MapManager): void {
|
|
|
|
|
SkillPresentationEnv.bind({
|
|
|
|
|
flyLayer: this.ui_fly?.isValid ? this.ui_fly : null,
|
|
|
|
|
scoreLayer: null,
|
|
|
|
|
monsterPresenter: this._monsterPresenter,
|
|
|
|
|
map,
|
|
|
|
|
battle,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定背包槽与拖放视图 */
|
|
|
|
|
private _bindBagBoard(map: MapManager): void {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
if (!battle || !this.ui_textList?.isValid) {
|
|
|
|
|
console.log('[UIBattleGame] ui_textList missing,无法绑定背包');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._bagView?.unbind();
|
|
|
|
|
this._bagView = new CocosBagBoardView();
|
|
|
|
|
const rangeNode = this.ui_range?.isValid ? this.ui_range : find('ui_range', this.node);
|
|
|
|
|
this.ui_range = rangeNode;
|
|
|
|
|
this._resolveInkstoneSpine();
|
|
|
|
|
this._bagView.bind(
|
|
|
|
|
battle,
|
|
|
|
|
map,
|
|
|
|
|
this.ui_textList,
|
|
|
|
|
this.ui_mapParentNode,
|
|
|
|
|
this.ui_fly ?? this.ui_mapParentNode,
|
|
|
|
|
this.ui_line,
|
|
|
|
|
rangeNode,
|
|
|
|
|
this.ui_textGrids,
|
|
|
|
|
);
|
|
|
|
|
if (!this.ui_delete?.isValid) {
|
|
|
|
|
console.log('[UIBattleGame] ui_delete 缺失,无法拖字删除');
|
|
|
|
|
}
|
|
|
|
|
this._bagView.setDeleteNode(this.ui_delete);
|
|
|
|
|
// bind() 开头会 unbind,入场回调必须在 bind 之后再挂
|
|
|
|
|
this._bagView.setOtherBagAnchor(this.ui_otherBag);
|
|
|
|
|
this._bagView.setInkstoneWorldPosProvider(this._inkstoneWorldPos);
|
|
|
|
|
this._bagView.setBagEntranceHandler(this._onInkstoneEntranceStart, this._onInkstoneEntranceEnd);
|
|
|
|
|
if (this._bagView.isBagEntranceBusy()) {
|
|
|
|
|
this._onInkstoneEntranceStart();
|
|
|
|
|
}
|
|
|
|
|
this._menShenTrap?.unbind();
|
|
|
|
|
this._menShenTrap = new BattleGameMenShenTrapController();
|
|
|
|
|
this._menShenTrap.bind(battle, map, battle.localPlayerId);
|
|
|
|
|
this._itemAd?.unbind();
|
|
|
|
|
this._itemAd = new BattleGameItemAdPresenter();
|
|
|
|
|
const btnItem = find('ui_bottom/ui_ready/btnItem', this.node);
|
|
|
|
|
this._itemAd.bind(battle, btnItem, this._menShenTrap);
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
this._bindSkillPeachFly();
|
|
|
|
|
this._bindKillPeach(battle);
|
|
|
|
|
this._bindDailyShopSkillDrag();
|
|
|
|
|
this._bindDailyShopSkillClick();
|
|
|
|
|
this._passiveSkillView?.bindBagView(this._bagView);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 解析 btnIllustrated 上的砚台 Spine */
|
|
|
|
|
private _resolveInkstoneSpine(): void {
|
|
|
|
|
const btn = this.btnIllustrated;
|
|
|
|
|
if (!btn?.isValid) {
|
|
|
|
|
console.log('[UIBattleGame] btnIllustrated 缺失');
|
|
|
|
|
this._inkstoneSkel = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const spineNode = btn.getChildByName('spine');
|
|
|
|
|
const skel = spineNode?.getComponent(sp.Skeleton) ?? btn.getComponentInChildren(sp.Skeleton);
|
|
|
|
|
if (!skel?.isValid) {
|
|
|
|
|
console.log('[UIBattleGame] btnIllustrated 缺少 Spine');
|
|
|
|
|
this._inkstoneSkel = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._inkstoneSkel = skel;
|
|
|
|
|
this._playInkstoneSpine(INKSTONE_SPINE_SPAWN, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 释放砚台 Spine 播放回调 */
|
|
|
|
|
private _clearInkstoneSpine(): void {
|
|
|
|
|
if (this._inkstoneSkel?.isValid) {
|
|
|
|
|
BattleSpinePlayback.clearPendingComplete(this._inkstoneSkel);
|
|
|
|
|
}
|
|
|
|
|
this._inkstoneSkel = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 播放砚台 Spine(资源动画名与表现含义取反)
|
|
|
|
|
* @param animName 资源内动画名
|
|
|
|
|
* @param loop 是否循环
|
|
|
|
|
*/
|
|
|
|
|
private _playInkstoneSpine(animName: string, loop: boolean): void {
|
|
|
|
|
const skel = this._inkstoneSkel;
|
|
|
|
|
if (!skel?.isValid) return;
|
|
|
|
|
BattleSpinePlayback.play(skel, animName, { loop, optional: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定 AI 主动施放飞图标表现 */
|
|
|
|
|
private _bindAiActiveSkillFx(map: MapManager): void {
|
|
|
|
|
this._aiActiveSkillFx?.unbind();
|
|
|
|
|
this._aiActiveSkillFx = new BattleGameAiActiveSkillFxPresenter();
|
|
|
|
|
this._aiActiveSkillFx.bind(
|
|
|
|
|
this._otherSkillView,
|
|
|
|
|
this._bagView,
|
|
|
|
|
map,
|
|
|
|
|
this.ui_fly ?? this.ui_mapParentNode,
|
|
|
|
|
);
|
|
|
|
|
AiBattleBridge.inst.setActiveSkillCastFxHandler(this._onAiActiveSkillCastFx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** AI 主动施放 → 飞图标 */
|
|
|
|
|
private readonly _onAiActiveSkillCastFx = (info: AiActiveSkillCastFxInfo): void => {
|
|
|
|
|
this._aiActiveSkillFx?.play(info);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 绑定敌方 Boss 出场弹窗 */
|
|
|
|
|
private _bindBossIntro(battle: ProjectBattleHandle): void {
|
|
|
|
|
this._bossIntro?.unbind();
|
|
|
|
|
this._bossIntro = new BattleGameBossIntroPresenter();
|
|
|
|
|
this._bossIntro.bind(battle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定御雷珠被动表现与逻辑 Host */
|
|
|
|
|
private _bindBaseThunder(battle: ProjectBattleHandle): void {
|
|
|
|
|
this._baseThunderFx?.unbind();
|
|
|
|
|
this._baseThunderFx = new BattleGameBaseThunderPresenter();
|
|
|
|
|
this._baseThunderFx.bind(this._monsterPresenter, this.ui_mapParentNode ?? this.ui_fly);
|
|
|
|
|
getBattleBuffBaseThunderHost().bind(battle, this._baseThunderFx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定底部金币飞币(击杀 / 回收共用) */
|
|
|
|
|
private _ensurePeachCoinFly(): void {
|
|
|
|
|
if (!this._peachCoinFly) {
|
|
|
|
|
this._peachCoinFly = new BattlePeachCoinFlyPresenter();
|
|
|
|
|
}
|
|
|
|
|
const moneyTarget = this.btnCoin?.getChildByName('金币') ?? this.btnCoin;
|
|
|
|
|
this._peachCoinFly.bind(moneyTarget, this.ui_fly ?? this.ui_mapParentNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 金矿:本机被动图标掉落 coinFly 飞向 btnCoin/金币 */
|
|
|
|
|
private _bindGoldMineCoinFly(): void {
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
this._passiveSkillView?.setGoldMineCoinFlyHandler(this._onGoldMineCoinFly);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** AI 金矿飞币(须在 AiBattleBridge.start 之后,避免 bind 清掉回调) */
|
|
|
|
|
private _bindAiGoldMineCoinFly(): void {
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
AiBattleBridge.inst.setGoldMineCoinFlyHandler(this._onAiGoldMineCoinFly);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 本机金矿飞币。
|
|
|
|
|
* @param fromWorld 金矿图标世界坐标
|
|
|
|
|
*/
|
|
|
|
|
private readonly _onGoldMineCoinFly = (fromWorld: Readonly<Vec3>): void => {
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
this._peachCoinFly?.playKillDrop(fromWorld, 1);
|
|
|
|
|
this._refreshPeachUi();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** AI 金矿飞币:敌方被动金矿图标原地弹跳 */
|
|
|
|
|
private readonly _onAiGoldMineCoinFly = (): void => {
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
const icon = this._otherSkillView?.getPassiveIconNode(BattleBuffFunctionType.GoldMine);
|
|
|
|
|
const from = icon?.isValid ? icon.worldPosition.clone() : null;
|
|
|
|
|
if (!from) {
|
|
|
|
|
console.log('[UIBattleGame] AI 金矿飞币无坐标');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._peachCoinFly?.playKillDropLocal(from, 1);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 技能产金币:从格心飞向 ui_money */
|
|
|
|
|
private _bindSkillPeachFly(): void {
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
this._bagView?.setSkillPeachFlyHandler(this._onSkillPeachGain);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 寻金尺:解锁地块随机金币飞币(本机飞向 ui_money,AI 原地弹跳)。
|
|
|
|
|
* @param battle 战斗
|
|
|
|
|
*/
|
|
|
|
|
private _bindUnlockCoinFly(battle: ProjectBattleHandle): void {
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
const handler = this._onUnlockCoinFly;
|
|
|
|
|
if (battle.bagSession) {
|
|
|
|
|
battle.bagSession.onUnlockCoinFly = handler;
|
|
|
|
|
}
|
|
|
|
|
const aiBag = AiBattleBridge.inst.aiBagSession;
|
|
|
|
|
if (aiBag) {
|
|
|
|
|
aiBag.onUnlockCoinFly = handler;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 地图解锁达 Const27 次数:本机弹三选一,AI 自动选。
|
|
|
|
|
* @param battle 战斗
|
|
|
|
|
*/
|
|
|
|
|
private _bindBattleBuffSelect(battle: ProjectBattleHandle): void {
|
|
|
|
|
const handler = this._onBattleBuffSelect;
|
|
|
|
|
if (battle.bagSession) {
|
|
|
|
|
battle.bagSession.onBattleBuffSelect = handler;
|
|
|
|
|
}
|
|
|
|
|
const aiBag = AiBattleBridge.inst.aiBagSession;
|
|
|
|
|
if (aiBag) {
|
|
|
|
|
aiBag.onBattleBuffSelect = handler;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 衰弱符等改写对方地图后:刷新对端 DeploySync / 字块视图。
|
|
|
|
|
* @param battle 战斗
|
|
|
|
|
*/
|
|
|
|
|
private _bindPeerMapToolApplied(battle: ProjectBattleHandle): void {
|
|
|
|
|
const handler = this._onPeerMapToolApplied;
|
|
|
|
|
if (battle.bagSession) {
|
|
|
|
|
battle.bagSession.onPeerMapToolApplied = handler;
|
|
|
|
|
}
|
|
|
|
|
const aiBag = AiBattleBridge.inst.aiBagSession;
|
|
|
|
|
if (aiBag) {
|
|
|
|
|
aiBag.onPeerMapToolApplied = handler;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param peerOwnerPlayerId 被改写方 playerId
|
|
|
|
|
* @param gridIndex 地图格
|
|
|
|
|
*/
|
|
|
|
|
private readonly _onPeerMapToolApplied = (peerOwnerPlayerId: number, gridIndex: number): void => {
|
|
|
|
|
if (peerOwnerPlayerId === AiHeadlessBagHost.AI_PLAYER_ID) {
|
|
|
|
|
const bridge = AiBattleBridge.inst;
|
|
|
|
|
bridge.aiDeploySync?.reconcileFromMerge('bagToolWeaken');
|
|
|
|
|
bridge.notifyAiBoardChanged();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 本机盘面由 peer.merge 的 MapUpgraded 驱动 CocosBagBoardView
|
|
|
|
|
if (!(gridIndex >= 0)) {
|
|
|
|
|
console.warn('[UIBattleGame] 衰弱符对端格无效', peerOwnerPlayerId, gridIndex);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param ownerPlayerId 归属
|
|
|
|
|
* @param unlockCount 解锁次数
|
|
|
|
|
*/
|
|
|
|
|
private readonly _onBattleBuffSelect = (ownerPlayerId: number, unlockCount: number): void => {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
if (!battle) return;
|
|
|
|
|
if (ownerPlayerId === AiHeadlessBagHost.AI_PLAYER_ID) {
|
|
|
|
|
const picked = BattleBuffSelectService.autoPickOne(ownerPlayerId);
|
|
|
|
|
if (picked) {
|
|
|
|
|
BattleBuffInGameRefresh.afterPick(battle, ownerPlayerId);
|
|
|
|
|
this.refreshBattleBuffSkills();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (ownerPlayerId !== (battle.localPlayerId ?? 0)) {
|
|
|
|
|
console.warn('[UIBattleGame] 未知 player 三选一', ownerPlayerId, unlockCount);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
void gg.ui.openPanel(UIID.PopSelect);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 本局获得 buff 后刷新技能栏与基地血条 */
|
|
|
|
|
refreshBattleBuffSkills(): void {
|
|
|
|
|
this._dailyShopSkillView?.refresh();
|
|
|
|
|
this._passiveSkillView?.refresh();
|
|
|
|
|
this._otherSkillView?.refresh();
|
|
|
|
|
this._campHpBar.syncFromSlots();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 寻金尺飞币:金币已入账,仅播表现。
|
|
|
|
|
* @param gridIndex 解锁锚点格
|
|
|
|
|
* @param coin 本次金币
|
|
|
|
|
* @param ownerPlayerId 归属
|
|
|
|
|
*/
|
|
|
|
|
private readonly _onUnlockCoinFly = (gridIndex: number, coin: number, ownerPlayerId: number): void => {
|
|
|
|
|
if (!(coin > 0)) return;
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
const fly = this._peachCoinFly;
|
|
|
|
|
const from = this._bagView?.getMapGridWorldPosition(gridIndex);
|
|
|
|
|
if (ownerPlayerId === battle?.localPlayerId) {
|
|
|
|
|
this._refreshPeachUi();
|
|
|
|
|
if (!from) {
|
|
|
|
|
console.log('[UIBattleGame] 寻金尺无飞币坐标', gridIndex);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fly?.playKillDrop(from, coin);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!from) {
|
|
|
|
|
console.log('[UIBattleGame] 寻金尺 AI 无飞币坐标', gridIndex);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fly?.playKillDropLocal(from, coin);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 技能产桃:本机 coinFly 飞向 ui_money;敌方格心 coinFly 原地弹跳后回池。
|
|
|
|
|
* @param gridIndex 施法格
|
|
|
|
|
* @param peachCount 蟠桃数量
|
|
|
|
|
*/
|
|
|
|
|
private readonly _onSkillPeachGain = (gridIndex: number, peachCount: number, ownerPlayerId = 0): void => {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
const bagView = this._bagView;
|
|
|
|
|
const fly = this._peachCoinFly;
|
|
|
|
|
const from = bagView?.getMapGridWorldPosition(gridIndex);
|
|
|
|
|
if (ownerPlayerId === battle?.localPlayerId) {
|
|
|
|
|
if (!from) {
|
|
|
|
|
console.log('[UIBattleGame] 产桃坐标缺失', gridIndex, ownerPlayerId);
|
|
|
|
|
bagView?.grantBattlePeach(peachCount);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fly?.playKillDrop(from, peachCount);
|
|
|
|
|
bagView?.grantBattlePeach(peachCount);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (battle && ownerPlayerId >= 0) {
|
|
|
|
|
battle.addPlayerGold(ownerPlayerId, peachCount);
|
|
|
|
|
if (!from) {
|
|
|
|
|
console.log('[UIBattleGame] 产桃坐标缺失', gridIndex, ownerPlayerId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fly?.playKillDropLocal(from, peachCount);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 绑定赏金令击杀额外金币 */
|
|
|
|
|
private _bindKillPeach(battle: ProjectBattleHandle): void {
|
|
|
|
|
this._killPeach?.unbind();
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
this._killPeach = new BattleGameKillPeachPresenter();
|
|
|
|
|
this._killPeach.bind(
|
|
|
|
|
battle,
|
|
|
|
|
this._bagView,
|
|
|
|
|
this._peachCoinFly,
|
|
|
|
|
(victim) => this._monsterDeathWorldPos(victim),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定击杀武器碎片掉落 */
|
|
|
|
|
private _bindWeaponDrop(battle: ProjectBattleHandle): void {
|
|
|
|
|
this._weaponDropFx?.unbind();
|
|
|
|
|
this._weaponDropFx = new BattleWeaponDropPresenter();
|
|
|
|
|
this._weaponDropFx.bind(
|
|
|
|
|
battle,
|
|
|
|
|
this.ui_fly ?? this.ui_mapParentNode,
|
|
|
|
|
this.btnPause,
|
|
|
|
|
this._resolveWeaponDropPos,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定每日商店主动 buff 拖拽施放(Phase1 占位) */
|
|
|
|
|
private _bindDailyShopSkillDrag(): void {
|
|
|
|
|
this._dailyShopSkillDrag?.unbind();
|
|
|
|
|
this._dailyShopSkillDrag = new BattleGameDailyShopSkillDrag();
|
|
|
|
|
this._dailyShopSkillDrag.bind(
|
|
|
|
|
this._dailyShopSkillView,
|
|
|
|
|
this._bagView,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
this._battle,
|
|
|
|
|
this.ui_fly ?? this.ui_mapParentNode,
|
|
|
|
|
this.ui_line,
|
|
|
|
|
this._ownedDesc,
|
|
|
|
|
);
|
|
|
|
|
this._dailyShopSkillView?.bindSkillDesc();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定每日商店主动 buff 点击施放 */
|
|
|
|
|
private _bindDailyShopSkillClick(): void {
|
|
|
|
|
this._dailyShopSkillClick?.unbind();
|
|
|
|
|
this._dailyShopSkillClick = new BattleGameDailyShopSkillClick();
|
|
|
|
|
this._dailyShopSkillClick.bind(this._dailyShopSkillView, this._bagView, this._battle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 七十二变 roll / 施放后:刷新 skill 交互绑定(不重建 Drag,避免 Presenter 引用断裂) */
|
|
|
|
|
private _onDailyShopSkillCastChanged(): void {
|
|
|
|
|
if (!this._bagView) return;
|
|
|
|
|
this._dailyShopSkillDrag?.refreshSlotBindings();
|
|
|
|
|
this._dailyShopSkillClick?.refreshSlotBindings();
|
|
|
|
|
this._dailyShopSkillView?.bindSkillDesc();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定技能表现控制器(缺绑则 PlayPresentation 上锁后无人 finish,塔永不攻击) */
|
|
|
|
|
private _bindSkillController(map: MapManager): void {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
const player = battle?.player ?? gg.game.localPlayer;
|
|
|
|
|
if (!battle || !player) {
|
|
|
|
|
console.error('[UIBattleGame] SkillController 绑定失败:无 battle/player');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._skillController?.unbind();
|
|
|
|
|
let sc = this.node.getComponent(SkillController);
|
|
|
|
|
if (!sc) sc = this.node.addComponent(SkillController);
|
|
|
|
|
sc.bind(player, battle);
|
|
|
|
|
this._skillController = sc;
|
|
|
|
|
sc.refreshEntitySkillBindings(map, this._bagView?.getMapGrids());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 刷新底部金币数、召唤消耗、金币广告与按钮填充状态 */
|
|
|
|
|
private _refreshPeachUi(): void {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
if (!battle) return;
|
|
|
|
|
|
|
|
|
|
const gold = battle.localGold;
|
|
|
|
|
const cost = battle.bagSession
|
|
|
|
|
? battle.bagSession.normalRefreshCost
|
|
|
|
|
: getDiscountedBagNormalRefreshCost(0, resolveCarriedSummonCostReduceWan().reduceWan);
|
|
|
|
|
const coinAdUsed = battle.coinAdUsed;
|
|
|
|
|
if (gold === this._peachUiGold && cost === this._peachUiCost && coinAdUsed === this._peachUiCoinAdUsed) return;
|
|
|
|
|
this._peachUiGold = gold;
|
|
|
|
|
this._peachUiCost = cost;
|
|
|
|
|
this._peachUiCoinAdUsed = coinAdUsed;
|
|
|
|
|
if (this.lb_coin?.isValid) {
|
|
|
|
|
this.lb_coin.string = `${gold}`;
|
|
|
|
|
}
|
|
|
|
|
this._syncCoinAdUi(battle);
|
|
|
|
|
if (this.lb_RefreshNeed?.isValid) {
|
|
|
|
|
this.lb_RefreshNeed.string = `${cost}`;
|
|
|
|
|
}
|
|
|
|
|
const enough = cost <= 0 || gold >= cost;
|
|
|
|
|
this._showRefreshBg1(enough);
|
|
|
|
|
this._syncRefreshBounce(enough);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 同步 btnCoin 广告区:次数未满显示下一次 +数量与 used+1/max;用尽则隐藏。
|
|
|
|
|
* @param battle 当前对局
|
|
|
|
|
*/
|
|
|
|
|
private _syncCoinAdUi(battle: ProjectBattleHandle): void {
|
|
|
|
|
const canAd = battle.canUseCoinAd();
|
|
|
|
|
const amount = battle.nextCoinAdAmount();
|
|
|
|
|
const max = battle.coinAdRewards.length;
|
|
|
|
|
this._setNodeActive(this._coinAdVideoIcon, canAd);
|
|
|
|
|
this._setNodeActive(this.lb_coinAdLimit?.node ?? null, canAd);
|
|
|
|
|
this._setNodeActive(this.lb_coinAdNum?.node ?? null, canAd);
|
|
|
|
|
if (!canAd) return;
|
|
|
|
|
if (this.lb_coinAdNum?.isValid) {
|
|
|
|
|
this.lb_coinAdNum.string = `+${amount}`;
|
|
|
|
|
}
|
|
|
|
|
if (this.lb_coinAdLimit?.isValid) {
|
|
|
|
|
this.lb_coinAdLimit.string = `${battle.coinAdUsed + 1}/${max}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 安全设置节点显隐。
|
|
|
|
|
* @param node 目标节点
|
|
|
|
|
* @param active 是否显示
|
|
|
|
|
*/
|
|
|
|
|
private _setNodeActive(node: Node | null, active: boolean): void {
|
|
|
|
|
if (node?.isValid && node.active !== active) node.active = active;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 当前金币是否够付一次召唤。
|
|
|
|
|
*/
|
|
|
|
|
private _isRefreshGoldEnough(): boolean {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
if (!battle) return false;
|
|
|
|
|
const gold = battle.localGold;
|
|
|
|
|
const cost = battle.bagSession
|
|
|
|
|
? battle.bagSession.normalRefreshCost
|
|
|
|
|
: getDiscountedBagNormalRefreshCost(0, resolveCarriedSummonCostReduceWan().reduceWan);
|
|
|
|
|
return cost <= 0 || gold >= cost;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* btnRefresh 只显示 bg1,隐藏进度底 bg2 和可刷新底 bg3。金币不够时 bg1 置灰。
|
|
|
|
|
* @param enough 金币是否够付一次召唤
|
|
|
|
|
*/
|
|
|
|
|
private _showRefreshBg1(enough: boolean): void {
|
|
|
|
|
const btn = this.btnRefresh;
|
|
|
|
|
if (!btn?.isValid) return;
|
|
|
|
|
const bg1 = btn.getChildByName('bg1');
|
|
|
|
|
const bg2 = btn.getChildByName('bg2');
|
|
|
|
|
const bg3 = btn.getChildByName('bg3');
|
|
|
|
|
if (!bg1?.isValid) {
|
|
|
|
|
console.log('[UIBattleGame] btnRefresh 缺少 bg1');
|
|
|
|
|
} else {
|
|
|
|
|
this._setNodeActive(bg1, true);
|
|
|
|
|
const sp = bg1.getComponent(Sprite);
|
|
|
|
|
if (!sp) {
|
|
|
|
|
console.log('[UIBattleGame] btnRefresh bg1 缺少 Sprite');
|
|
|
|
|
} else if (sp.grayscale === enough) {
|
|
|
|
|
sp.grayscale = !enough;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (bg2?.isValid) Tween.stopAllByTarget(bg2);
|
|
|
|
|
this._setNodeActive(bg2, false);
|
|
|
|
|
this._setNodeActive(bg3, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 关掉预制体上的 TweenBreathe,避免与 Q 弹抢缩放 */
|
|
|
|
|
private _disableRefreshBreathe(): void {
|
|
|
|
|
const breathe = this.btnRefresh?.getComponent(TweenBreathe);
|
|
|
|
|
if (!breathe?.isValid) return;
|
|
|
|
|
breathe.playing = false;
|
|
|
|
|
breathe.enabled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 桃子足够时召唤按钮 Q 弹两下(第二下幅度减半),每段足够只播一次 */
|
|
|
|
|
private _syncRefreshBounce(enough: boolean): void {
|
|
|
|
|
if (!enough) {
|
|
|
|
|
this._stopRefreshBounce();
|
|
|
|
|
this._refreshBounceHinted = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this._refreshBounceHinted) return;
|
|
|
|
|
this._refreshBounceHinted = true;
|
|
|
|
|
BattleAudio.playRefreshReady();
|
|
|
|
|
this._playRefreshBounce();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 召唤按钮小幅度 Q 弹两下 */
|
|
|
|
|
private _playRefreshBounce(): void {
|
|
|
|
|
const btn = this.btnRefresh;
|
|
|
|
|
if (!btn?.isValid) return;
|
|
|
|
|
this._stopRefreshBounce();
|
|
|
|
|
const hx = this._refreshBtnScaleHome.x;
|
|
|
|
|
const hy = this._refreshBtnScaleHome.y;
|
|
|
|
|
const hz = this._refreshBtnScaleHome.z;
|
|
|
|
|
const a1 = 1 + REFRESH_BOUNCE_AMP;
|
|
|
|
|
const a2 = 1 + REFRESH_BOUNCE_AMP * 0.5;
|
|
|
|
|
tween(btn)
|
|
|
|
|
.to(REFRESH_BOUNCE_SEC, { scale: v3(hx * a1, hy * a1, hz) }, { easing: 'quadOut' })
|
|
|
|
|
.to(REFRESH_BOUNCE_SEC, { scale: v3(hx, hy, hz) }, { easing: 'quadIn' })
|
|
|
|
|
.to(REFRESH_BOUNCE_SEC, { scale: v3(hx * a2, hy * a2, hz) }, { easing: 'quadOut' })
|
|
|
|
|
.to(REFRESH_BOUNCE_SEC, { scale: v3(hx, hy, hz) }, { easing: 'quadIn' })
|
|
|
|
|
.start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 停止召唤按钮 Q 弹并恢复基准缩放 */
|
|
|
|
|
private _stopRefreshBounce(): void {
|
|
|
|
|
const btn = this.btnRefresh;
|
|
|
|
|
if (!btn?.isValid) return;
|
|
|
|
|
Tween.stopAllByTarget(btn);
|
|
|
|
|
btn.setScale(this._refreshBtnScaleHome);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 刷新顶部波次:第 N 波 */
|
|
|
|
|
private _refreshWaveUi(): void {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
if (!battle) return;
|
|
|
|
|
|
|
|
|
|
const session = battle.session;
|
|
|
|
|
const level = session.config.getLevel(session.levelId);
|
|
|
|
|
if (!level || !(level.waves.length > 0)) {
|
|
|
|
|
console.log('[UIBattleGame] 关卡波次缺失', session.levelId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._refreshNextWaveTime();
|
|
|
|
|
const idx = session.spawner.waveIndex;
|
|
|
|
|
const cur = idx < 0 ? 1 : idx + 1;
|
|
|
|
|
const text = gg.lang.get('第{1}波', [cur]);
|
|
|
|
|
if (idx === this._waveUiIdx && this.lb_wave?.isValid && this.lb_wave.string === text) return;
|
|
|
|
|
this._waveUiIdx = idx;
|
|
|
|
|
if (this.lb_wave?.isValid) {
|
|
|
|
|
this.lb_wave.node.getComponent(LangTxt)?.destroy();
|
|
|
|
|
this.lb_wave.string = text;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 下一波还没开打时显示 ui_nextWaveTime。
|
|
|
|
|
* 倒计时已开始用剩余秒数;开打前先显示该波准备时间。出怪中、没有下一波、战斗结束时隐藏。
|
|
|
|
|
*/
|
|
|
|
|
private _refreshNextWaveTime(): void {
|
|
|
|
|
const node = this.ui_nextWaveTime;
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
if (!battle || battle.IsGameOver) {
|
|
|
|
|
if (node.active) node.active = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const session = battle.session;
|
|
|
|
|
const spawner = session.spawner;
|
|
|
|
|
const waiting = !spawner.isWaveCombatActive() && spawner.hasMoreWaves();
|
|
|
|
|
if (!waiting) {
|
|
|
|
|
if (node.active) node.active = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!node.active) node.active = true;
|
|
|
|
|
const nextIndex = spawner.waveIndex + 1;
|
|
|
|
|
const tip = gg.lang.get('第{1}波怪物来袭', [nextIndex + 1]);
|
|
|
|
|
const wave = session.config.getWave(session.levelId, nextIndex);
|
|
|
|
|
if (!wave && this._nextWaveMiss !== nextIndex) {
|
|
|
|
|
this._nextWaveMiss = nextIndex;
|
|
|
|
|
console.log('[UIBattleGame] 下一波配置缺失', session.levelId, nextIndex);
|
|
|
|
|
}
|
|
|
|
|
const scheduled = session.waveIntervalRemaining;
|
|
|
|
|
this._grantWaveCountdownGold(spawner.waveIndex, scheduled);
|
|
|
|
|
const remain = scheduled > 0 ? scheduled : (wave?.prepTime ?? 0);
|
|
|
|
|
const timeText = this._formatWaveCountdown(remain);
|
|
|
|
|
const tipLabel = this._lbWaveTip;
|
|
|
|
|
if (tipLabel?.isValid && tipLabel.string !== tip) {
|
|
|
|
|
tipLabel.node.getComponent(LangTxt)?.destroy();
|
|
|
|
|
tipLabel.string = tip;
|
|
|
|
|
}
|
|
|
|
|
const timeLabel = this._lbWaveTipTime;
|
|
|
|
|
if (timeLabel?.isValid && timeLabel.string !== timeText) {
|
|
|
|
|
timeLabel.string = timeText;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 波间剩余秒数格式化为 mm:ss。向上取整,避免最后不足 1 秒变成 00:00。
|
|
|
|
|
* @param remainSec 剩余秒
|
|
|
|
|
*/
|
|
|
|
|
private _formatWaveCountdown(remainSec: number): string {
|
|
|
|
|
const total = Math.max(0, Math.ceil(remainSec));
|
|
|
|
|
const min = Math.floor(total / 60);
|
|
|
|
|
const sec = total % 60;
|
|
|
|
|
const mm = min < 10 ? `0${min}` : String(min);
|
|
|
|
|
const ss = sec < 10 ? `0${sec}` : String(sec);
|
|
|
|
|
return `${mm}:${ss}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _teardownBattleView(): void {
|
|
|
|
|
PerfSampler.endBattle();
|
|
|
|
|
DamageTextSpawner.resetBurst();
|
|
|
|
|
HitEffectResolver.resetBurst();
|
|
|
|
|
if (this._onMapGridsReady) {
|
|
|
|
|
GEvent.Ins.off(GEvent.MapGridsReady, this._onMapGridsReady, this);
|
|
|
|
|
this._onMapGridsReady = null;
|
|
|
|
|
}
|
|
|
|
|
if (this._battle && this._onBattleEventBound) {
|
|
|
|
|
this._battle.off(BATTLE_EVENT_ALL, this._onBattleEventBound);
|
|
|
|
|
}
|
|
|
|
|
this._onBattleEventBound = null;
|
|
|
|
|
this._recycleOrphanMonsterDeathFx();
|
|
|
|
|
this._deathFxParent = null;
|
|
|
|
|
SkillPresentationEnv.unbind();
|
|
|
|
|
this._combatPresenter?.unbind();
|
|
|
|
|
this._combatPresenter = null;
|
|
|
|
|
this._skillController?.unbind();
|
|
|
|
|
this._skillController = null;
|
|
|
|
|
this._dailyShopSkillDrag?.unbind();
|
|
|
|
|
this._dailyShopSkillDrag = null;
|
|
|
|
|
this._dailyShopSkillClick?.unbind();
|
|
|
|
|
this._dailyShopSkillClick = null;
|
|
|
|
|
this._bagView?.setSkillPeachFlyHandler(null);
|
|
|
|
|
this._passiveSkillView?.setGoldMineCoinFlyHandler(null);
|
|
|
|
|
AiBattleBridge.inst.setGoldMineCoinFlyHandler(null);
|
|
|
|
|
if (this._battle?.bagSession) {
|
|
|
|
|
this._battle.bagSession.onUnlockCoinFly = null;
|
|
|
|
|
this._battle.bagSession.onBattleBuffSelect = null;
|
|
|
|
|
this._battle.bagSession.onPeerMapToolApplied = null;
|
|
|
|
|
}
|
|
|
|
|
const aiBagTeardown = AiBattleBridge.inst.aiBagSession;
|
|
|
|
|
if (aiBagTeardown) {
|
|
|
|
|
aiBagTeardown.onUnlockCoinFly = null;
|
|
|
|
|
aiBagTeardown.onBattleBuffSelect = null;
|
|
|
|
|
aiBagTeardown.onPeerMapToolApplied = null;
|
|
|
|
|
}
|
|
|
|
|
this._killPeach?.unbind();
|
|
|
|
|
this._killPeach = null;
|
|
|
|
|
this._weaponDropFx?.unbind();
|
|
|
|
|
this._weaponDropFx = null;
|
|
|
|
|
AiBattleBridge.inst.setActiveSkillCastFxHandler(null);
|
|
|
|
|
this._aiActiveSkillFx?.unbind();
|
|
|
|
|
this._aiActiveSkillFx = null;
|
|
|
|
|
this._peachCoinFly?.unbind();
|
|
|
|
|
this._peachCoinFly = null;
|
|
|
|
|
this._menShenTrap?.unbind();
|
|
|
|
|
this._menShenTrap = null;
|
|
|
|
|
getBattleGameMonsterCrisisHintPresenter().unbind();
|
|
|
|
|
this._itemAd?.unbind();
|
|
|
|
|
this._itemAd = null;
|
|
|
|
|
this._baseThunderFx?.unbind();
|
|
|
|
|
this._baseThunderFx = null;
|
|
|
|
|
getBattleBuffBaseThunderHost().unbind();
|
|
|
|
|
this._bossIntro?.unbind();
|
|
|
|
|
this._bossIntro = null;
|
|
|
|
|
this._dailyShopSkillView = null;
|
|
|
|
|
this._passiveSkillView = null;
|
|
|
|
|
this._otherSkillView = null;
|
|
|
|
|
this._otherOwnedDesc = null;
|
|
|
|
|
this._unbindFirstPlaceCombat();
|
|
|
|
|
this.unschedule(this._realignFirstGuideFinger);
|
|
|
|
|
this._firstGuide.unbind();
|
|
|
|
|
this._bagView?.unbind();
|
|
|
|
|
this._bagView = null;
|
|
|
|
|
this._clearInkstoneSpine();
|
|
|
|
|
AiBattleBridge.inst.stop();
|
|
|
|
|
this._monsterPresenter.unbind();
|
|
|
|
|
this._campHpBar.unbind();
|
|
|
|
|
this._battle?.session.setBridge(undefined);
|
|
|
|
|
this._movementBridge = null;
|
|
|
|
|
this._mapHost?.unloadMap();
|
|
|
|
|
this._mapHost = null;
|
|
|
|
|
this._battle = null;
|
|
|
|
|
this._combatStarted = false;
|
|
|
|
|
this._stopRefreshBounce();
|
|
|
|
|
this._refreshBounceHinted = false;
|
|
|
|
|
this._illustratedHintShownThisBattle = false;
|
|
|
|
|
this._peachUiGold = -1;
|
|
|
|
|
this._peachUiCost = -1;
|
|
|
|
|
this._peachUiCoinAdUsed = -1;
|
|
|
|
|
this._coinAdVideoIcon = null;
|
|
|
|
|
if (this._coinAdPending) {
|
|
|
|
|
this._coinAdPending = false;
|
|
|
|
|
BattleUiPauseGate.release(COIN_AD_PAUSE_TOKEN);
|
|
|
|
|
}
|
|
|
|
|
this._waveUiIdx = -999;
|
|
|
|
|
this._nextWaveMiss = -2;
|
|
|
|
|
this._waveGoldIndex = -2;
|
|
|
|
|
this._aiSkillUiFrame = 0;
|
|
|
|
|
this._lbWaveTip = null;
|
|
|
|
|
this._lbWaveTipTime = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 第 5 波且未点击过图鉴时显示 ui_illustrated */
|
|
|
|
|
private _tryShowIllustratedHintOnWave(waveIndex: number): void {
|
|
|
|
|
if (this._illustratedHintShownThisBattle) return;
|
|
|
|
|
if (!BattleIllustratedGuideManager.shouldShowHintAtWave(waveIndex)) return;
|
|
|
|
|
this._illustratedHintShownThisBattle = true;
|
|
|
|
|
this._syncIllustratedHint(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 同步 ui_illustrated 显隐 */
|
|
|
|
|
private _syncIllustratedHint(visible: boolean): void {
|
|
|
|
|
if (!this.ui_illustrated?.isValid) return;
|
|
|
|
|
this.ui_illustrated.active = visible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 监听:隐藏图鉴引导 */
|
|
|
|
|
private _onHideIllustratedHint = (): void => {
|
|
|
|
|
this._syncIllustratedHint(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
click_btnAutoM(): void {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
click_btnBattleSpeed(): void {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 底部金币:看视频加币 */
|
|
|
|
|
click_btnCoin(): void {
|
|
|
|
|
this._tryWatchCoinAd();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 旧入口:转调看视频加币 */
|
|
|
|
|
click_btnGetMoneyAd(): void {
|
|
|
|
|
this._tryWatchCoinAd();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 观看激励视频,按 Const38 下一次数量加金币 */
|
|
|
|
|
private _tryWatchCoinAd(): void {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
if (!battle || this._coinAdPending) return;
|
|
|
|
|
if (!battle.canUseCoinAd()) return;
|
|
|
|
|
|
|
|
|
|
this._coinAdPending = true;
|
|
|
|
|
BattleUiPauseGate.acquire(COIN_AD_PAUSE_TOKEN);
|
|
|
|
|
gg.sdk.showVideoAd((res) => {
|
|
|
|
|
if (!this._coinAdPending) return;
|
|
|
|
|
this._coinAdPending = false;
|
|
|
|
|
BattleUiPauseGate.release(COIN_AD_PAUSE_TOKEN);
|
|
|
|
|
if (game.isPaused()) game.resume();
|
|
|
|
|
if (!res) {
|
|
|
|
|
gg.ui.showToast('观看视频失败');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const b = this._battle;
|
|
|
|
|
if (!b?.canUseCoinAd()) return;
|
|
|
|
|
b.IsWatchAd = true;
|
|
|
|
|
const amount = b.consumeCoinAd();
|
|
|
|
|
if (!(amount > 0)) {
|
|
|
|
|
console.log('[UIBattleGame] 金币广告入账失败');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const curWaveNum = gg.game.CurentBattle.CurentWaveNum;
|
|
|
|
|
gg.sdk.reportDY('inLevel', `${gg.game.getReportDYBattleCountLabel(curWaveNum)}-AD-广告获得金币`);
|
|
|
|
|
this._refreshPeachUi();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
click_btnIllustrated(): void {
|
|
|
|
|
BattleIllustratedGuideManager.markClicked();
|
|
|
|
|
gg.ui.openPanel(UIID.PopIllustrated);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
click_btnPause(): void {
|
|
|
|
|
gg.ui.popPanel(UIID.BattlePause);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 召唤:按当前空开启格补字,最多 5 个 */
|
|
|
|
|
click_btnRefresh(): void {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
const bag = battle?.bagSession;
|
|
|
|
|
if (!battle || !bag) {
|
|
|
|
|
console.log('[UIBattleGame] 背包会话未就绪');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this._bagView?.isBagEntranceBusy()) {
|
|
|
|
|
// 入场未正常收尾时再点:先结算待定倍增,再解锁刷新
|
|
|
|
|
this._bagView.clearBagEntranceBusy();
|
|
|
|
|
this._flushPendingDigitDouble();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 字从刷新按钮抛物线落入本次新补的空格;已有字的格子不重刷
|
|
|
|
|
const ink = this._bagView?.getInkstoneWorldPosition();
|
|
|
|
|
const fromWorld = ink
|
|
|
|
|
? new Vec3(ink.x, ink.y, ink.z)
|
|
|
|
|
: (this.btnRefresh?.isValid ? this.btnRefresh.worldPosition.clone() : null);
|
|
|
|
|
|
|
|
|
|
const result = bag.tryFillEmptyOpenSlots(battle);
|
|
|
|
|
if (result === 'ok') {
|
|
|
|
|
this._refreshPeachUi();
|
|
|
|
|
this._firstGuide.onSummonOk();
|
|
|
|
|
const filled = bag.lastFilledSlotIndices();
|
|
|
|
|
this._pendingDigitDoubleSlots = filled.slice();
|
|
|
|
|
this._bagView?.playNewSlotEntrance(filled, fromWorld);
|
|
|
|
|
// 无飞入动画时入场结束回调不会触发,立刻结算倍增
|
|
|
|
|
if (!this._bagView?.isBagEntranceBusy()) {
|
|
|
|
|
this._flushPendingDigitDouble();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._bagView?.clearBagEntranceBusy();
|
|
|
|
|
this._pendingDigitDoubleSlots = [];
|
|
|
|
|
if (result === 'no_slot') {
|
|
|
|
|
gg.ui.showToast('暂无空格');
|
|
|
|
|
} else if (result === 'not_enough') {
|
|
|
|
|
gg.ui.showToast('金币不足');
|
|
|
|
|
} else {
|
|
|
|
|
gg.ui.showToast('刷新失败');
|
|
|
|
|
console.log('[UIBattleGame] tryFillEmptyOpenSlots failed', result);
|
|
|
|
|
}
|
|
|
|
|
this._refreshPeachUi();
|
|
|
|
|
}
|
|
|
|
|
}
|