You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/script/ui/BattleGame/UIBattleGame.ts

1518 lines
61 KiB

3 weeks ago
//*********************
// create by 流云
// time: Tue Aug 18 2026
// desc: 战斗界面 — 地图、出怪、背包刷新(召唤)
//*********************
1 week ago
import { _decorator, find, game, Label, Node, Sprite, Tween, tween, v3, Vec3, sp } from 'cc';
3 weeks ago
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';
3 weeks ago
import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime';
import { BattleAudio } from '../../battle/audio/BattleAudio';
1 week ago
import { BattleUiPauseGate } from '../../battle/BattleUiPauseGate';
3 weeks ago
import { SkillController } from '../../battle/skill/SkillController';
import { SkillPresentationEnv } from '../../battle/skill/SkillPresentationEnv';
import { MonsterDataManager, MonsterType } from '../../manager/MonsterDataManager';
3 weeks ago
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';
4 days ago
import { BattleBuffSelectService } from '../../manager/battleBuff/BattleBuffSelectService';
import { BattleBuffInGameRefresh } from '../../battle/combat/BattleBuffInGameRefresh';
3 weeks ago
import { BattleGameBaseThunderPresenter } from './BattleGameBaseThunderPresenter';
import { BattleGameBossIntroPresenter } from './BattleGameBossIntroPresenter';
import { AiBattleBridge } from '../../battle/ai/bridge/AiBattleBridge';
4 days ago
import { AiHeadlessBagHost } from '../../battle/ai/runtime/AiHeadlessBagHost';
import type { AiActiveSkillCastFxInfo } from '../../battle/ai/runtime/AiActiveSkillHost';
3 weeks ago
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';
3 weeks ago
import { ClientErrorReporter } from '../../../mx/tools/ClientErrorReporter';
import { PerfSampler } from '../../game/PerfSampler';
2 weeks ago
import { isElementGlyphType, MergeEventType, type MergeEvent } from 'db://assets/mx/module/merge';
import { WeaponComposeConfigProvider } from '../../battle/bag/WeaponComposeConfigProvider';
3 weeks ago
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;
1 week ago
/** 开局未放置数字则强制开怪的等待秒数 */
const FIRST_PLACE_COMBAT_TIMEOUT_SEC = 10;
1 week ago
/** 看视频加金币期间暂停 token */
const COIN_AD_PAUSE_TOKEN = 'ad:battleCoin';
3 weeks ago
/** 砚台 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 _menShenTrap: BattleGameMenShenTrapController | null = null;
2 weeks ago
/** btnItem 门神盾牌广告 */
3 weeks ago
private _itemAd: BattleGameItemAdPresenter | null = null;
/** 御雷珠被动落雷表现 */
3 weeks ago
private _baseThunderFx: BattleGameBaseThunderPresenter | null = null;
/** 敌方 Boss 出场弹窗 */
private _bossIntro: BattleGameBossIntroPresenter | null = null;
/** 点击类主动 buff(Phase1 占位) */
3 weeks ago
private _dailyShopSkillClick: BattleGameDailyShopSkillClick | null = null;
/** 回收/奖励:coinFly 飘仙桃 */
private _peachCoinFly: BattlePeachCoinFlyPresenter | null = null;
/** 赏金令:击杀小怪概率额外金币 */
3 weeks ago
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[] = [];
3 weeks ago
/** 波间提示:第 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;
1 week ago
/** 金币广告已用次数缓存 */
private _peachUiCoinAdUsed = -1;
/** btnCoin/视频标 */
private _coinAdVideoIcon: Node | null = null;
/** 看视频加金币进行中 */
private _coinAdPending = false;
3 weeks ago
/** 波次 UI 缓存 */
private _waveUiIdx = -999;
/** 已提示过配置缺失的下一波下标 */
private _nextWaveMiss = -2;
/** 已发过结算金币的已结束波次下标;首波倒计时 waveIndex 为 -1,不发 */
private _waveGoldIndex = -2;
3 weeks ago
/** AI 技能条降频 */
private _aiSkillUiFrame = 0;
2 weeks ago
/** AI 未运行告警节流(秒) */
private _aiWatchdogSec = 0;
3 weeks ago
2 weeks ago
/** 刷新按钮世界坐标,供背包出字抛物线起点 */
3 weeks ago
private readonly _inkstoneWorldPos = (): Readonly<Vec3> | null => {
2 weeks ago
const n = this.btnRefresh;
3 weeks ago
return n?.isValid ? n.worldPosition : null;
};
/** 出字开始:播资源名「待机」 */
private readonly _onInkstoneEntranceStart = (): void => {
this._playInkstoneSpine(INKSTONE_SPINE_SPAWN, true);
};
/** 出字结束:播资源名「出字」,再结算倍增卡 */
3 weeks ago
private readonly _onInkstoneEntranceEnd = (): void => {
this._playInkstoneSpine(INKSTONE_SPINE_IDLE, true);
this._flushPendingDigitDouble();
3 weeks ago
};
/** 飞入结束后(或入场未播/被清锁时)对本次刷新字掷骰 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);
}
3 weeks ago
/** 尽早关掉召唤按钮 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;
if (!this.ui_nextWaveTime?.isValid) {
console.log('[UIBattleGame] ui_nextWaveTime 节点缺失');
} else if (!this._lbWaveTip || !this._lbWaveTipTime) {
console.log('[UIBattleGame] ui_nextWaveTime 缺少 lb1 或 lb2');
}
3 weeks ago
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();
3 weeks ago
this._passiveSkillView.refresh();
this._otherSkillView = new BattleGameOtherSkillView(this.ui_otherskill1List, this.ui_otherskill2List);
this._otherSkillView.setOwnedDesc(this._otherOwnedDesc);
this._otherSkillView.refresh();
1 week ago
if (this.ui_money?.isValid) this.ui_money.active = false;
this._coinAdVideoIcon = this.btnCoin?.getChildByName('视频标') ?? null;
3 weeks ago
this._waveUiIdx = -999;
1 week ago
this._peachUiGold = -1;
this._peachUiCost = -1;
this._peachUiCoinAdUsed = -1;
3 weeks ago
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);
2 weeks ago
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;
}
3 weeks ago
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();
1 week ago
this._campHpBar.tick();
3 weeks ago
}
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);
2 weeks ago
AiBattleBridge.inst.setTextGridLayer(this.ui_textGrids);
2 weeks ago
try {
if (AiBattleBridge.inst.start(battle, map, undefined, this.node, this.ui_textList)) {
this._otherSkillView?.refresh();
this._bindAiActiveSkillFx(map);
this._bindAiGoldMineCoinFly();
2 weeks ago
} else {
console.error('[UIBattleGame] AiBattleBridge.start 失败,对手 AI 未启动');
}
} catch (e) {
console.error('[UIBattleGame] AiBattleBridge.start 异常', String(e));
3 weeks ago
}
this._bindUnlockCoinFly(battle);
4 days ago
this._bindBattleBuffSelect(battle);
3 weeks ago
this._campHpBar.bind(battle, map);
getBattleGameMonsterCrisisHintPresenter().bind(battle, map);
this._firstGuide.bind(this.ui_guide, battle.bagSession);
PerfSampler.beginBattle();
2 weeks ago
this._listenFirstPlaceCombat();
this._refreshWaveUi();
}
3 weeks ago
1 week ago
/**
* 开局不立刻出怪:本机首次放数字开怪,或超时后强制开怪。
*/
2 weeks ago
private _listenFirstPlaceCombat(): void {
this._unbindFirstPlaceCombat();
const bag = this._battle?.bagSession;
if (!bag) {
console.error('[UIBattleGame] 无背包会话,无法按首次放字开怪');
return;
3 weeks ago
}
2 weeks ago
bag.on(MergeEventType.TilePlaced, this._onFirstPlayerNumberPlaced);
1 week ago
this.scheduleOnce(this._onFirstPlaceCombatTimeout, FIRST_PLACE_COMBAT_TIMEOUT_SEC);
2 weeks ago
}
1 week ago
/** 卸掉首次放字开怪监听与超时 */
2 weeks ago
private _unbindFirstPlaceCombat(): void {
1 week ago
this.unschedule(this._onFirstPlaceCombatTimeout);
2 weeks ago
this._battle?.bagSession?.off(MergeEventType.TilePlaced, this._onFirstPlayerNumberPlaced);
3 weeks ago
}
2 weeks ago
/**
* 本机首次放下五行数字后开波。
* @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;
1 week ago
this._startCombatFromFirstPlaceGate();
};
/** 超时仍未放数字:强制开怪 */
private readonly _onFirstPlaceCombatTimeout = (): void => {
if (this._combatStarted) return;
this._startCombatFromFirstPlaceGate();
};
/** 首次放字 / 超时共用:开波并卸监听 */
private _startCombatFromFirstPlaceGate(): void {
if (this._combatStarted) return;
2 weeks ago
this._unbindFirstPlaceCombat();
this._combatStarted = true;
gg.battle.startCombat();
this._refreshWaveUi();
};
3 weeks ago
/** 绑定 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);
}
3 weeks ago
/**
* 击杀掉桃表现:本机飞向 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」轨,不播外部死亡特效 */
3 weeks ago
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;
3 weeks ago
if (view?.Data?.type === MonsterType.TreasureBox) return;
if (view?.Data && MonsterDataManager.isElementSmallMonster(view.Data)) return;
3 weeks ago
}
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,
2 weeks ago
this.ui_textGrids,
3 weeks ago
);
// 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);
2 weeks ago
this._itemAd.bind(battle, btnItem, this._menShenTrap);
this._ensurePeachCoinFly();
3 weeks ago
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_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);
}
/** 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 */
3 weeks ago
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);
}
1 week ago
/** 绑定底部金币飞币(击杀 / 回收共用) */
3 weeks ago
private _ensurePeachCoinFly(): void {
if (!this._peachCoinFly) {
this._peachCoinFly = new BattlePeachCoinFlyPresenter();
}
1 week ago
const moneyTarget = this.btnCoin?.getChildByName('金币') ?? this.btnCoin;
this._peachCoinFly.bind(moneyTarget, this.ui_fly ?? this.ui_mapParentNode);
3 weeks ago
}
/** 金矿:本机被动图标掉落 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 */
3 weeks ago
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;
}
}
4 days ago
/**
* 地图解锁达 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;
}
}
/**
* @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);
};
4 days ago
/** 本局获得 buff 后刷新技能栏与基地血条 */
4 days ago
refreshBattleBuffSkills(): void {
this._dailyShopSkillView?.refresh();
this._passiveSkillView?.refresh();
this._otherSkillView?.refresh();
4 days ago
this._campHpBar.syncFromSlots();
4 days ago
}
/**
* 寻金尺飞币:金币已入账,仅播表现。
* @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);
};
3 weeks ago
/**
* 技能产桃:本机 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);
}
};
/** 绑定赏金令击杀额外金币 */
3 weeks ago
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 占位) */
3 weeks ago
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,
3 weeks ago
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());
}
1 week ago
/** 刷新底部金币数、召唤消耗、金币广告与按钮填充状态 */
3 weeks ago
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);
1 week ago
const coinAdUsed = battle.coinAdUsed;
if (gold === this._peachUiGold && cost === this._peachUiCost && coinAdUsed === this._peachUiCoinAdUsed) return;
3 weeks ago
this._peachUiGold = gold;
this._peachUiCost = cost;
1 week ago
this._peachUiCoinAdUsed = coinAdUsed;
if (this.lb_coin?.isValid) {
this.lb_coin.string = `${gold}`;
3 weeks ago
}
1 week ago
this._syncCoinAdUi(battle);
3 weeks ago
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);
}
1 week ago
/**
* 同步 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;
}
3 weeks ago
/** 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;
}
this._refreshNextWaveTime();
3 weeks ago
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;
3 weeks ago
}
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}`;
3 weeks ago
}
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;
4 days ago
this._battle.bagSession.onBattleBuffSelect = null;
}
const aiBagTeardown = AiBattleBridge.inst.aiBagSession;
if (aiBagTeardown) {
aiBagTeardown.onUnlockCoinFly = null;
4 days ago
aiBagTeardown.onBattleBuffSelect = null;
}
3 weeks ago
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;
2 weeks ago
this._unbindFirstPlaceCombat();
3 weeks ago
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;
1 week ago
this._peachUiCoinAdUsed = -1;
this._coinAdVideoIcon = null;
if (this._coinAdPending) {
this._coinAdPending = false;
BattleUiPauseGate.release(COIN_AD_PAUSE_TOKEN);
}
3 weeks ago
this._waveUiIdx = -999;
this._nextWaveMiss = -2;
this._waveGoldIndex = -2;
3 weeks ago
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 {
}
1 week ago
/** 底部金币:看视频加币 */
click_btnCoin(): void {
this._tryWatchCoinAd();
}
/** 旧入口:转调看视频加币 */
3 weeks ago
click_btnGetMoneyAd(): void {
1 week ago
this._tryWatchCoinAd();
}
3 weeks ago
1 week ago
/** 观看激励视频,按 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();
});
3 weeks ago
}
click_btnIllustrated(): void {
BattleIllustratedGuideManager.markClicked();
gg.ui.openPanel(UIID.PopIllustrated);
}
click_btnPause(): void {
gg.ui.popPanel(UIID.BattlePause);
}
2 weeks ago
/** 召唤:按当前空开启格补字,最多 5 个 */
3 weeks ago
click_btnRefresh(): void {
const battle = this._battle;
const bag = battle?.bagSession;
if (!battle || !bag) {
console.log('[UIBattleGame] 背包会话未就绪');
return;
}
if (this._bagView?.isBagEntranceBusy()) {
// 入场未正常收尾时再点:先结算待定倍增,再解锁刷新
3 weeks ago
this._bagView.clearBagEntranceBusy();
this._flushPendingDigitDouble();
3 weeks ago
}
2 weeks ago
// 字从刷新按钮抛物线落入本次新补的空格;已有字的格子不重刷
3 weeks ago
const ink = this._bagView?.getInkstoneWorldPosition();
const fromWorld = ink
? new Vec3(ink.x, ink.y, ink.z)
2 weeks ago
: (this.btnRefresh?.isValid ? this.btnRefresh.worldPosition.clone() : null);
3 weeks ago
2 weeks ago
const result = bag.tryFillEmptyOpenSlots(battle);
3 weeks ago
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();
}
3 weeks ago
return;
}
this._bagView?.clearBagEntranceBusy();
this._pendingDigitDoubleSlots = [];
2 weeks ago
if (result === 'no_slot') {
gg.ui.showToast('暂无空格');
} else if (result === 'not_enough') {
1 week ago
gg.ui.showToast('金币不足');
3 weeks ago
} else {
gg.ui.showToast('刷新失败');
2 weeks ago
console.log('[UIBattleGame] tryFillEmptyOpenSlots failed', result);
3 weeks ago
}
this._refreshPeachUi();
}
}