|
|
|
|
|
|
|
|
|
//*********************
|
|
|
|
|
// create by 流云
|
|
|
|
|
// time: Tue Aug 18 2026
|
|
|
|
|
// desc: 战斗界面 — 地图、出怪、背包刷新(召唤)
|
|
|
|
|
//*********************
|
|
|
|
|
import { _decorator, find, 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 type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime';
|
|
|
|
|
import { BattleAudio } from '../../battle/audio/BattleAudio';
|
|
|
|
|
import { SkillController } from '../../battle/skill/SkillController';
|
|
|
|
|
import { SkillPresentationEnv } from '../../battle/skill/SkillPresentationEnv';
|
|
|
|
|
import { 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 { BattleGameBindNetTrapController } from './BattleGameBindNetTrapController';
|
|
|
|
|
import { BattleGameFireTrapController } from './BattleGameFireTrapController';
|
|
|
|
|
import { BattleGameRecycleToPeachPresenter } from './BattleGameRecycleToPeachPresenter';
|
|
|
|
|
import { BattleGameRewriteGlyphPresenter } from './BattleGameRewriteGlyphPresenter';
|
|
|
|
|
import { BattleGameAttackSpeedFxPresenter } from './BattleGameAttackSpeedFxPresenter';
|
|
|
|
|
import { BattleGameRandomLevelFxPresenter } from './BattleGameRandomLevelFxPresenter';
|
|
|
|
|
import { BattleGameUnitLevelUpFxPresenter } from './BattleGameUnitLevelUpFxPresenter';
|
|
|
|
|
import { BattleGameAttackRangeFxPresenter } from './BattleGameAttackRangeFxPresenter';
|
|
|
|
|
import { BattleGameExtraBossFxPresenter } from './BattleGameExtraBossFxPresenter';
|
|
|
|
|
import { BattleGameConfuseHeroFxPresenter } from './BattleGameConfuseHeroFxPresenter';
|
|
|
|
|
import { BattleGameStealGlyphFxPresenter } from './BattleGameStealGlyphFxPresenter';
|
|
|
|
|
import { BattleGameDailyShopSkillClick } from './BattleGameDailyShopSkillClick';
|
|
|
|
|
import { BattlePeachCoinFlyPresenter } from './BattlePeachCoinFlyPresenter';
|
|
|
|
|
import { BattleWeaponDropPresenter } from './BattleWeaponDropPresenter';
|
|
|
|
|
import { BattleGameTreasureShovelPresenter } from './BattleGameTreasureShovelPresenter';
|
|
|
|
|
import { BattleGameItemAdPresenter } from './BattleGameItemAdPresenter';
|
|
|
|
|
import { BattleGameMenShenTrapController } from './BattleGameMenShenTrapController';
|
|
|
|
|
import { getBattleGameRuoshuiBuffPresenter } from './BattleGameRuoshuiBuffPresenter';
|
|
|
|
|
import { getBattleGameMonsterCrisisHintPresenter } from './BattleGameMonsterCrisisHintPresenter';
|
|
|
|
|
import { BattleGameKillPeachPresenter } from './BattleGameKillPeachPresenter';
|
|
|
|
|
import { getBattleBuffUnitReviveHost } from '../../battle/combat/BattleBuffUnitReviveHost';
|
|
|
|
|
import { getBattleBuffBaseThunderHost } from '../../battle/combat/BattleBuffBaseThunderHost';
|
|
|
|
|
import { getBattleBuffBossInterruptHost } from '../../battle/combat/BattleBuffBossInterruptHost';
|
|
|
|
|
import { BattleGameBaseThunderPresenter } from './BattleGameBaseThunderPresenter';
|
|
|
|
|
import { BattleGameBossInterruptFxPresenter } from './BattleGameBossInterruptFxPresenter';
|
|
|
|
|
import { BattleGameBossIntroPresenter } from './BattleGameBossIntroPresenter';
|
|
|
|
|
import { AiBattleBridge } from '../../battle/ai/bridge/AiBattleBridge';
|
|
|
|
|
import type { AiActiveSkillCastFxInfo, AiConfuseHeroFxHandler } 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 { ClientErrorReporter } from '../../../mx/tools/ClientErrorReporter';
|
|
|
|
|
import { PerfSampler } from '../../game/PerfSampler';
|
|
|
|
|
|
|
|
|
|
const { ccclass } = _decorator;
|
|
|
|
|
|
|
|
|
|
/** 召唤按钮 bg fillRange 变化 tween 时长(秒) */
|
|
|
|
|
const REFRESH_FILL_TWEEN_SEC = 0.2;
|
|
|
|
|
/** btnRefresh/bg2 fillRange 下限(负值,接近凑够时不再小于 -0.1) */
|
|
|
|
|
const REFRESH_FILL_RANGE_MIN = -0.1;
|
|
|
|
|
/** 召唤按钮进度满后 Q 弹第一下相对缩放增量 */
|
|
|
|
|
const REFRESH_BOUNCE_AMP = 0.1;
|
|
|
|
|
/** 单段弹起/回落时长(秒) */
|
|
|
|
|
const REFRESH_BOUNCE_SEC = 0.22;
|
|
|
|
|
|
|
|
|
|
/** 砚台 Spine:资源里名字取反,出字过程播「待机」 */
|
|
|
|
|
const INKSTONE_SPINE_SPAWN = '待机';
|
|
|
|
|
/** 砚台 Spine:资源里名字取反,出完播「出字」待机 */
|
|
|
|
|
const INKSTONE_SPINE_IDLE = '出字';
|
|
|
|
|
|
|
|
|
|
@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;
|
|
|
|
|
/** 召唤按钮填充进度(btnRefresh/bg2,水平从右到左) */
|
|
|
|
|
private _refreshFill: Sprite | null = null;
|
|
|
|
|
/** 召唤按钮 bg2 目标 fillRange(= -(1 - 桃子进度)) */
|
|
|
|
|
private _refreshFillTarget = 0;
|
|
|
|
|
/** 召唤按钮可刷新态(bg3) */
|
|
|
|
|
private _refreshBg3: Node | 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 _bindNetTrap: BattleGameBindNetTrapController | null = null;
|
|
|
|
|
private _fireTrap: BattleGameFireTrapController | null = null;
|
|
|
|
|
/** 门神盾牌陷阱 */
|
|
|
|
|
private _menShenTrap: BattleGameMenShenTrapController | null = null;
|
|
|
|
|
/** btnItem 铲子 / 盾牌广告 */
|
|
|
|
|
private _itemAd: BattleGameItemAdPresenter | null = null;
|
|
|
|
|
/** 九霄神雷被动落雷表现 */
|
|
|
|
|
private _baseThunderFx: BattleGameBaseThunderPresenter | null = null;
|
|
|
|
|
/** 碎法帝钟 Boss 打断表现 */
|
|
|
|
|
private _bossInterruptFx: BattleGameBossInterruptFxPresenter | null = null;
|
|
|
|
|
/** 敌方 Boss 出场弹窗 */
|
|
|
|
|
private _bossIntro: BattleGameBossIntroPresenter | null = null;
|
|
|
|
|
/** 乾坤袋:skill 拖到字块回收 */
|
|
|
|
|
private _recycleToPeach: BattleGameRecycleToPeachPresenter | null = null;
|
|
|
|
|
/** 文昌笔:skill 拖到字块改字前置动画 */
|
|
|
|
|
private _rewriteGlyph: BattleGameRewriteGlyphPresenter | null = null;
|
|
|
|
|
/** 攻速符:skill 拖到字块施法表现 */
|
|
|
|
|
private _attackSpeedFx: BattleGameAttackSpeedFxPresenter | null = null;
|
|
|
|
|
/** 授仙箓:skill 拖到字块升/降级表现 */
|
|
|
|
|
private _randomLevelFx: BattleGameRandomLevelFxPresenter | null = null;
|
|
|
|
|
/** 封神榜:skill 拖到字块升级双层表现 */
|
|
|
|
|
private _unitLevelUpFx: BattleGameUnitLevelUpFxPresenter | null = null;
|
|
|
|
|
/** 千里眼:skill 拖到字块施法表现 */
|
|
|
|
|
private _attackRangeFx: BattleGameAttackRangeFxPresenter | null = null;
|
|
|
|
|
/** 万神幡:skill 拖到己方出怪口召唤 Boss */
|
|
|
|
|
private _extraBossFx: BattleGameExtraBossFxPresenter | null = null;
|
|
|
|
|
/** 幻魂镜:skill 拖到敌方神将迷惑停攻 */
|
|
|
|
|
private _confuseHeroFx: BattleGameConfuseHeroFxPresenter | null = null;
|
|
|
|
|
/** 妙手神偷:skill 拖到敌方单字偷取 */
|
|
|
|
|
private _stealGlyphFx: BattleGameStealGlyphFxPresenter | null = null;
|
|
|
|
|
/** 点击类主动 buff(万神幡等) */
|
|
|
|
|
private _dailyShopSkillClick: BattleGameDailyShopSkillClick | null = null;
|
|
|
|
|
/** 回收/奖励:coinFly 飘仙桃 */
|
|
|
|
|
private _peachCoinFly: BattlePeachCoinFlyPresenter | null = null;
|
|
|
|
|
/** 掘宝仙铲:铲挖格产桃 */
|
|
|
|
|
private _treasureShovel: BattleGameTreasureShovelPresenter | 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();
|
|
|
|
|
/** 波间提示:第 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;
|
|
|
|
|
/** 波次 UI 缓存 */
|
|
|
|
|
private _waveUiIdx = -999;
|
|
|
|
|
/** AI 技能条降频 */
|
|
|
|
|
private _aiSkillUiFrame = 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_IDLE, true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 尽早关掉召唤按钮 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._refreshFill = this.btnRefresh?.getChildByName('bg2')?.getComponent(Sprite)
|
|
|
|
|
?? this.btnRefresh?.getChildByName('bg')?.getComponent(Sprite)
|
|
|
|
|
?? null;
|
|
|
|
|
this._refreshBg3 = this.btnRefresh?.getChildByName('bg3') ?? null;
|
|
|
|
|
this._disableRefreshBreathe();
|
|
|
|
|
this.btnRefresh?.getScale(this._refreshBtnScaleHome);
|
|
|
|
|
this._setupRefreshFillSprite();
|
|
|
|
|
this._lbWaveTip = this.ui_nextWaveTime?.getChildByName('lb1')?.getComponent(Label) ?? null;
|
|
|
|
|
this._lbWaveTipTime = this.ui_nextWaveTime?.getChildByName('lb2')?.getComponent(Label) ?? null;
|
|
|
|
|
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._passiveSkillView.refresh();
|
|
|
|
|
this._otherSkillView = new BattleGameOtherSkillView(this.ui_otherskill1List, this.ui_otherskill2List);
|
|
|
|
|
this._otherSkillView.setOwnedDesc(this._otherOwnedDesc);
|
|
|
|
|
this._otherSkillView.refresh();
|
|
|
|
|
this._waveUiIdx = -999;
|
|
|
|
|
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?.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._bindNetTrap?.tick(dt);
|
|
|
|
|
this._fireTrap?.tick(dt);
|
|
|
|
|
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._bindBossInterruptFx(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);
|
|
|
|
|
if (AiBattleBridge.inst.start(battle, map, undefined, this.node)) {
|
|
|
|
|
this._otherSkillView?.refresh();
|
|
|
|
|
this._bindAiActiveSkillFx(map);
|
|
|
|
|
}
|
|
|
|
|
this._campHpBar.bind(battle, map);
|
|
|
|
|
getBattleGameRuoshuiBuffPresenter().refresh();
|
|
|
|
|
getBattleGameMonsterCrisisHintPresenter().bind(battle, map);
|
|
|
|
|
this._firstGuide.bind(this.ui_guide, battle.bagSession);
|
|
|
|
|
PerfSampler.beginBattle();
|
|
|
|
|
|
|
|
|
|
if (!this._combatStarted) {
|
|
|
|
|
this._combatStarted = true;
|
|
|
|
|
gg.battle.startCombat();
|
|
|
|
|
this._refreshWaveUi();
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 击杀掉桃表现:本机飞向 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/死亡(优先用节点位,否则用缓存世界坐标) */
|
|
|
|
|
private _playMonsterDeathFx(victim: EntityId): void {
|
|
|
|
|
if (getBattleBuffUnitReviveHost().shouldSkipDeathFxOnKill(victim)) {
|
|
|
|
|
// 还魂暂缓期间已播过死亡特效,真死时跳过重复播放
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
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?: { type?: number } } | null;
|
|
|
|
|
if (view?.Data?.type === MonsterType.TreasureBox) 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,
|
|
|
|
|
);
|
|
|
|
|
// 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._bindNetTrap?.unbind();
|
|
|
|
|
this._bindNetTrap = new BattleGameBindNetTrapController();
|
|
|
|
|
this._bindNetTrap.bind(battle, map);
|
|
|
|
|
this._fireTrap?.unbind();
|
|
|
|
|
this._fireTrap = new BattleGameFireTrapController();
|
|
|
|
|
this._fireTrap.bind(battle, map);
|
|
|
|
|
this._menShenTrap?.unbind();
|
|
|
|
|
this._menShenTrap = new BattleGameMenShenTrapController();
|
|
|
|
|
this._menShenTrap.bind(battle, map, battle.localPlayerId);
|
|
|
|
|
getBattleGameRuoshuiBuffPresenter().bind(battle, map, this._monsterPresenter, {
|
|
|
|
|
flyLayer: this.ui_fly ?? this.ui_mapParentNode ?? null,
|
|
|
|
|
passiveSkillView: this._passiveSkillView,
|
|
|
|
|
otherSkillView: this._otherSkillView,
|
|
|
|
|
});
|
|
|
|
|
this._itemAd?.unbind();
|
|
|
|
|
this._itemAd = new BattleGameItemAdPresenter();
|
|
|
|
|
const btnItem = find('ui_bottom/ui_ready/btnItem', this.node);
|
|
|
|
|
this._itemAd.bind(battle, map, this._bagView, btnItem, this._menShenTrap);
|
|
|
|
|
this._bindRecycleToPeach();
|
|
|
|
|
this._bindTreasureShovel();
|
|
|
|
|
this._bindSkillPeachFly();
|
|
|
|
|
this._bindKillPeach(battle);
|
|
|
|
|
this._bindRewriteGlyph();
|
|
|
|
|
this._bindAttackSpeedFx();
|
|
|
|
|
this._bindRandomLevelFx();
|
|
|
|
|
this._bindUnitLevelUpFx();
|
|
|
|
|
this._bindAttackRangeFx();
|
|
|
|
|
this._bindExtraBossFx();
|
|
|
|
|
this._bindConfuseHeroFx();
|
|
|
|
|
this._bindStealGlyphFx();
|
|
|
|
|
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_IDLE, 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 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定 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);
|
|
|
|
|
// start→bind 会重建 ActiveSkillHost,须在 AI 启动后再挂神偷表现
|
|
|
|
|
AiBattleBridge.inst.setStealGlyphFxHandler(this._onAiStealGlyphFx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** AI 主动施放 → 飞图标 */
|
|
|
|
|
private readonly _onAiActiveSkillCastFx = (info: AiActiveSkillCastFxInfo): void => {
|
|
|
|
|
this._aiActiveSkillFx?.play(info);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 绑定碎法帝钟被动打断表现 */
|
|
|
|
|
private _bindBossInterruptFx(battle: ProjectBattleHandle): void {
|
|
|
|
|
this._bossInterruptFx?.unbind();
|
|
|
|
|
this._bossInterruptFx = new BattleGameBossInterruptFxPresenter();
|
|
|
|
|
this._bossInterruptFx.bind(battle, this._monsterPresenter);
|
|
|
|
|
getBattleBuffBossInterruptHost().bind(battle, this._bossInterruptFx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定敌方 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定 ui_money 飞桃(击杀 / 回收共用) */
|
|
|
|
|
private _ensurePeachCoinFly(): void {
|
|
|
|
|
if (!this._peachCoinFly) {
|
|
|
|
|
this._peachCoinFly = new BattlePeachCoinFlyPresenter();
|
|
|
|
|
}
|
|
|
|
|
this._peachCoinFly.bind(this.ui_money, this.ui_fly ?? this.ui_mapParentNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定乾坤袋回收演出 */
|
|
|
|
|
private _bindRecycleToPeach(): void {
|
|
|
|
|
this._recycleToPeach?.unbind();
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
this._recycleToPeach = new BattleGameRecycleToPeachPresenter();
|
|
|
|
|
this._recycleToPeach.bind(
|
|
|
|
|
this._bagView,
|
|
|
|
|
this.ui_fly ?? this.ui_mapParentNode,
|
|
|
|
|
this._peachCoinFly,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定掘宝仙铲挖格产桃 */
|
|
|
|
|
private _bindTreasureShovel(): void {
|
|
|
|
|
this._treasureShovel?.unbind();
|
|
|
|
|
this._treasureShovel = new BattleGameTreasureShovelPresenter();
|
|
|
|
|
this._treasureShovel.bind(this._bagView, this._peachCoinFly);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 桃树/技能产桃:从格心飞向 ui_money */
|
|
|
|
|
private _bindSkillPeachFly(): void {
|
|
|
|
|
this._ensurePeachCoinFly();
|
|
|
|
|
this._bagView?.setSkillPeachFlyHandler(this._onSkillPeachGain);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 技能产桃:本机 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,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定文昌笔改字前置动画 */
|
|
|
|
|
private _bindRewriteGlyph(): void {
|
|
|
|
|
this._rewriteGlyph?.unbind();
|
|
|
|
|
this._rewriteGlyph = new BattleGameRewriteGlyphPresenter();
|
|
|
|
|
this._rewriteGlyph.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定攻速符施法表现 */
|
|
|
|
|
private _bindAttackSpeedFx(): void {
|
|
|
|
|
this._attackSpeedFx?.unbind();
|
|
|
|
|
this._attackSpeedFx = new BattleGameAttackSpeedFxPresenter();
|
|
|
|
|
this._attackSpeedFx.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定授仙箓升/降级表现 */
|
|
|
|
|
private _bindRandomLevelFx(): void {
|
|
|
|
|
this._randomLevelFx?.unbind();
|
|
|
|
|
this._randomLevelFx = new BattleGameRandomLevelFxPresenter();
|
|
|
|
|
this._randomLevelFx.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定封神榜升级双层表现 */
|
|
|
|
|
private _bindUnitLevelUpFx(): void {
|
|
|
|
|
this._unitLevelUpFx?.unbind();
|
|
|
|
|
this._unitLevelUpFx = new BattleGameUnitLevelUpFxPresenter();
|
|
|
|
|
this._unitLevelUpFx.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定千里眼施法表现 */
|
|
|
|
|
private _bindAttackRangeFx(): void {
|
|
|
|
|
this._attackRangeFx?.unbind();
|
|
|
|
|
this._attackRangeFx = new BattleGameAttackRangeFxPresenter();
|
|
|
|
|
this._attackRangeFx.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定万神幡施法表现 */
|
|
|
|
|
private _bindExtraBossFx(): void {
|
|
|
|
|
this._extraBossFx?.unbind();
|
|
|
|
|
this._extraBossFx = new BattleGameExtraBossFxPresenter();
|
|
|
|
|
this._extraBossFx.bind(this._battle, this._bagView);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定幻魂镜施法表现 */
|
|
|
|
|
private _bindConfuseHeroFx(): void {
|
|
|
|
|
this._confuseHeroFx?.unbind();
|
|
|
|
|
this._confuseHeroFx = new BattleGameConfuseHeroFxPresenter();
|
|
|
|
|
this._confuseHeroFx.bind(this._battle, this._bagView, this.ui_fly ?? this.ui_mapParentNode);
|
|
|
|
|
this._battle?.charmController.setEntryReleasedHandler((composeKey) => {
|
|
|
|
|
this._confuseHeroFx?.abortForComposeKey(composeKey);
|
|
|
|
|
});
|
|
|
|
|
AiBattleBridge.inst.setConfuseHeroFxHandler(this._onAiConfuseHeroFx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 绑定妙手神偷施法表现 */
|
|
|
|
|
private _bindStealGlyphFx(): void {
|
|
|
|
|
this._stealGlyphFx?.unbind();
|
|
|
|
|
this._stealGlyphFx = new BattleGameStealGlyphFxPresenter();
|
|
|
|
|
this._stealGlyphFx.bind(this._bagView, this.ui_fly ?? this.ui_mapParentNode);
|
|
|
|
|
AiBattleBridge.inst.setStealGlyphFxHandler(this._onAiStealGlyphFx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** AI 幻魂镜 → 对玩家神将播施法与上下层循环 */
|
|
|
|
|
private readonly _onAiConfuseHeroFx: AiConfuseHeroFxHandler = (target, durationSec) => {
|
|
|
|
|
const presenter = this._confuseHeroFx;
|
|
|
|
|
if (!presenter) return false;
|
|
|
|
|
return presenter.tryPlayFromAi(target, durationSec);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* AI 妙手神偷 → 神偷手锁定玩家字后抓取飞入。
|
|
|
|
|
* @param playerGridIndex 玩家源格
|
|
|
|
|
* @param placement AI 落点
|
|
|
|
|
* @param onExtracted 提取成功
|
|
|
|
|
*/
|
|
|
|
|
private readonly _onAiStealGlyphFx = (
|
|
|
|
|
playerGridIndex: number,
|
|
|
|
|
placement: { kind: 'map'; gridIndex: number } | { kind: 'bag'; slotIndex: number },
|
|
|
|
|
onExtracted: (tile: import('db://assets/mx/module/merge').MapTileRecord) => void,
|
|
|
|
|
): boolean => {
|
|
|
|
|
const presenter = this._stealGlyphFx;
|
|
|
|
|
if (!presenter) return false;
|
|
|
|
|
return presenter.tryPlayAiSteal(playerGridIndex, placement, onExtracted);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 绑定每日商店主动 buff 拖拽施放 */
|
|
|
|
|
private _bindDailyShopSkillDrag(): void {
|
|
|
|
|
this._dailyShopSkillDrag?.unbind();
|
|
|
|
|
this._dailyShopSkillDrag = new BattleGameDailyShopSkillDrag();
|
|
|
|
|
this._dailyShopSkillDrag.bind(
|
|
|
|
|
this._dailyShopSkillView,
|
|
|
|
|
this._bagView,
|
|
|
|
|
this._bindNetTrap,
|
|
|
|
|
this._fireTrap,
|
|
|
|
|
this._recycleToPeach,
|
|
|
|
|
this._rewriteGlyph,
|
|
|
|
|
this._attackSpeedFx,
|
|
|
|
|
this._randomLevelFx,
|
|
|
|
|
this._unitLevelUpFx,
|
|
|
|
|
this._attackRangeFx,
|
|
|
|
|
this._extraBossFx,
|
|
|
|
|
this._confuseHeroFx,
|
|
|
|
|
this._stealGlyphFx,
|
|
|
|
|
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);
|
|
|
|
|
if (gold === this._peachUiGold && cost === this._peachUiCost) return;
|
|
|
|
|
this._peachUiGold = gold;
|
|
|
|
|
this._peachUiCost = cost;
|
|
|
|
|
if (this.lb_money?.isValid) {
|
|
|
|
|
this.lb_money.string = `${gold}`;
|
|
|
|
|
}
|
|
|
|
|
if (this.lb_RefreshNeed?.isValid) {
|
|
|
|
|
this.lb_RefreshNeed.string = `${cost}`;
|
|
|
|
|
}
|
|
|
|
|
const enough = cost <= 0 || gold >= cost;
|
|
|
|
|
if (this._refreshBg3?.isValid) {
|
|
|
|
|
this._refreshBg3.active = enough;
|
|
|
|
|
}
|
|
|
|
|
if (this._refreshFill?.isValid) {
|
|
|
|
|
const fillNode = this._refreshFill.node;
|
|
|
|
|
if (enough) {
|
|
|
|
|
if (fillNode.active) {
|
|
|
|
|
this._tweenRefreshFillRange(0, this._hideRefreshFill.bind(this));
|
|
|
|
|
} else {
|
|
|
|
|
this._stopRefreshFillTween();
|
|
|
|
|
this._refreshFillTarget = 0;
|
|
|
|
|
this._refreshFill.fillRange = 0;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
fillNode.active = true;
|
|
|
|
|
const progress = cost > 0 ? Math.min(1, Math.max(0, gold / cost)) : 1;
|
|
|
|
|
const fillRange = Math.min(REFRESH_FILL_RANGE_MIN, -(1 - progress));
|
|
|
|
|
this._tweenRefreshFillRange(fillRange);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this._syncRefreshBounce(enough);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** btnRefresh/bg2:水平从右到左;fillRange = min(-0.1, -(1 - 桃子/消耗)),凑够为 0 */
|
|
|
|
|
private _setupRefreshFillSprite(): void {
|
|
|
|
|
const sprite = this._refreshFill;
|
|
|
|
|
if (!sprite?.isValid) return;
|
|
|
|
|
sprite.type = Sprite.Type.FILLED;
|
|
|
|
|
sprite.fillType = Sprite.FillType.HORIZONTAL;
|
|
|
|
|
sprite.fillStart = 1;
|
|
|
|
|
sprite.fillRange = -1;
|
|
|
|
|
this._refreshFillTarget = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** btnRefresh/bg2:fillRange 0.2s tween 到目标(不足时为负,凑够为 0) */
|
|
|
|
|
private _tweenRefreshFillRange(target: number, onComplete?: () => void): void {
|
|
|
|
|
const sprite = this._refreshFill;
|
|
|
|
|
if (!sprite?.isValid) return;
|
|
|
|
|
if (Math.abs(target - this._refreshFillTarget) < 1e-4) {
|
|
|
|
|
onComplete?.();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._refreshFillTarget = target;
|
|
|
|
|
Tween.stopAllByTarget(sprite);
|
|
|
|
|
const tw = tween(sprite).to(REFRESH_FILL_TWEEN_SEC, { fillRange: target });
|
|
|
|
|
if (onComplete) {
|
|
|
|
|
tw.call(onComplete);
|
|
|
|
|
}
|
|
|
|
|
tw.start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 桃子已够:fillRange 到 0 后隐藏 bg2 */
|
|
|
|
|
private _hideRefreshFill(): void {
|
|
|
|
|
const fillNode = this._refreshFill?.node;
|
|
|
|
|
if (fillNode?.isValid) {
|
|
|
|
|
fillNode.active = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 停止召唤按钮 bg fillRange tween */
|
|
|
|
|
private _stopRefreshFillTween(): void {
|
|
|
|
|
if (this._refreshFill?.isValid) {
|
|
|
|
|
Tween.stopAllByTarget(this._refreshFill);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 关掉预制体上的 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
if (this.ui_nextWaveTime?.isValid) {
|
|
|
|
|
this.ui_nextWaveTime.active = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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._recycleToPeach?.unbind();
|
|
|
|
|
this._recycleToPeach = null;
|
|
|
|
|
this._treasureShovel?.unbind();
|
|
|
|
|
this._treasureShovel = null;
|
|
|
|
|
this._bagView?.setSkillPeachFlyHandler(null);
|
|
|
|
|
this._killPeach?.unbind();
|
|
|
|
|
this._killPeach = null;
|
|
|
|
|
this._weaponDropFx?.unbind();
|
|
|
|
|
this._weaponDropFx = null;
|
|
|
|
|
this._rewriteGlyph?.unbind();
|
|
|
|
|
this._rewriteGlyph = null;
|
|
|
|
|
this._attackSpeedFx?.unbind();
|
|
|
|
|
this._attackSpeedFx = null;
|
|
|
|
|
this._randomLevelFx?.unbind();
|
|
|
|
|
this._randomLevelFx = null;
|
|
|
|
|
this._unitLevelUpFx?.unbind();
|
|
|
|
|
this._unitLevelUpFx = null;
|
|
|
|
|
this._attackRangeFx?.unbind();
|
|
|
|
|
this._attackRangeFx = null;
|
|
|
|
|
this._extraBossFx?.unbind();
|
|
|
|
|
this._extraBossFx = null;
|
|
|
|
|
this._confuseHeroFx?.unbind();
|
|
|
|
|
this._confuseHeroFx = null;
|
|
|
|
|
this._stealGlyphFx?.unbind();
|
|
|
|
|
this._stealGlyphFx = null;
|
|
|
|
|
AiBattleBridge.inst.setStealGlyphFxHandler(null);
|
|
|
|
|
AiBattleBridge.inst.setConfuseHeroFxHandler(null);
|
|
|
|
|
AiBattleBridge.inst.setActiveSkillCastFxHandler(null);
|
|
|
|
|
this._aiActiveSkillFx?.unbind();
|
|
|
|
|
this._aiActiveSkillFx = null;
|
|
|
|
|
this._peachCoinFly?.unbind();
|
|
|
|
|
this._peachCoinFly = null;
|
|
|
|
|
this._bindNetTrap?.unbind();
|
|
|
|
|
this._bindNetTrap = null;
|
|
|
|
|
this._fireTrap?.unbind();
|
|
|
|
|
this._fireTrap = null;
|
|
|
|
|
this._menShenTrap?.unbind();
|
|
|
|
|
this._menShenTrap = null;
|
|
|
|
|
getBattleGameRuoshuiBuffPresenter().unbind();
|
|
|
|
|
getBattleGameMonsterCrisisHintPresenter().unbind();
|
|
|
|
|
this._itemAd?.unbind();
|
|
|
|
|
this._itemAd = null;
|
|
|
|
|
this._baseThunderFx?.unbind();
|
|
|
|
|
this._baseThunderFx = null;
|
|
|
|
|
getBattleBuffBaseThunderHost().unbind();
|
|
|
|
|
this._bossInterruptFx?.unbind();
|
|
|
|
|
this._bossInterruptFx = null;
|
|
|
|
|
getBattleBuffBossInterruptHost().unbind();
|
|
|
|
|
this._bossIntro?.unbind();
|
|
|
|
|
this._bossIntro = null;
|
|
|
|
|
this._dailyShopSkillView = null;
|
|
|
|
|
this._passiveSkillView = null;
|
|
|
|
|
this._otherSkillView = null;
|
|
|
|
|
this._otherOwnedDesc = null;
|
|
|
|
|
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._stopRefreshFillTween();
|
|
|
|
|
this._refreshFill = null;
|
|
|
|
|
this._refreshBg3 = null;
|
|
|
|
|
this._refreshFillTarget = 0;
|
|
|
|
|
this._stopRefreshBounce();
|
|
|
|
|
this._refreshBounceHinted = false;
|
|
|
|
|
this._illustratedHintShownThisBattle = false;
|
|
|
|
|
this._peachUiGold = -1;
|
|
|
|
|
this._peachUiCost = -1;
|
|
|
|
|
this._waveUiIdx = -999;
|
|
|
|
|
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_btnGetMoneyAd(): void {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()) {
|
|
|
|
|
// 入场未正常收尾时再点:先解锁再刷新(根因在 Animator.stop 须 settle)
|
|
|
|
|
this._bagView.clearBagEntranceBusy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 字从刷新按钮抛物线落入本次新补的空格;已有字的格子不重刷
|
|
|
|
|
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();
|
|
|
|
|
this._bagView?.playNewSlotEntrance(bag.lastFilledSlotIndices(), fromWorld);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._bagView?.clearBagEntranceBusy();
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|