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/BattleGameMonsterCrisisHint...

324 lines
12 KiB

3 weeks ago
import { Node, sp } from 'cc';
import type { EntityId } from 'db://assets/mx/module/ecs';
import {
BattleTeamId,
MovementComp,
TeamComp,
TransformComp,
UnitMetaComp,
isBajiaoBlowRetreating,
isYanSoulClaimed,
type BattleSession,
type Cell,
} from 'db://assets/mx/module/battle';
import { GBundle } from '../../game/ConfigRes';
import { BATTLE_HERO_BUFF_PATH } from '../../battle/res/BattleHeroResCollector';
import { isMechanismUnit } from '../../battle/mechanism/MechanismServices';
import { MonsterType } from '../../manager/MonsterDataManager';
import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime';
import type { MapManager } from '../../battle/map/legacy/MapManager';
import type { MapGrid } from '../../battle/map/legacy/MapGrid';
import { resolveEnemyCampAreaIdForDefender, resolveEnemySpawnPortForDefender, resolveSpawnGridByPort } from '../../battle/map/MapPathQueries';
import { resolveAttackStandGridForCamp } from '../../battle/view/tangSengMap';
import { BattleAudio } from '../../battle/audio/BattleAudio';
import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback';
import { schedulePresentation } from '../../battle/skill/BattlePresentationClock';
/** 怪物危机提示预制体(battleHero/prefab/buff/) */
const CRISIS_HINT_PREFAB_NAME = '怪物危机提示';
/** 危机提示 Spine 循环动画 */
const CRISIS_HINT_ANIM = 'animation';
/** 终点前监测格数 */
const WATCH_CELLS_BEFORE_GOAL = 3;
/** 本机玩家 id */
const LOCAL_PLAYER_ID = 0;
/** AI 玩家 id */
const AI_PLAYER_ID = 1;
/** 单侧危机提示运行时 */
type CrisisHintSide = {
/** 被进攻防守方 playerId */
defenderPlayerId: number;
/** 进路(含终点) */
routePath: Cell[];
/** 终点前一格 */
anchorGrid: MapGrid;
/** 终点前 WATCH 格(最多 3 格) */
watchCells: Cell[];
/** 表现节点 */
fxNode: Node | null;
/** Spine 组件 */
skeleton: sp.Skeleton | null;
/** 循环音效调度世代(递增取消) */
loopSoundGen: number;
};
/**
* 怪物危机提示:终点前一格挂载预制体,终点前 3 格有进攻怪则显示,否则隐藏。
* 我方提示在敌方终点前一格;敌方提示在我方终点前一格。
*/
export class BattleGameMonsterCrisisHintPresenter {
private _battle: ProjectBattleHandle | null = null;
private _map: MapManager | null = null;
private readonly _sides: CrisisHintSide[] = [];
/** 全场扫格降频 */
private _scanAcc = 0;
/** 绑定地图并在双方终点前一格挂载提示(初始隐藏) */
bind(battle: ProjectBattleHandle, map: MapManager): void {
this.unbind();
if (!battle || !map?.gridsInitialized) {
console.log('[MonsterCrisisHint] 地图未就绪,跳过绑定');
return;
}
this._battle = battle;
this._map = map;
const session = battle.session;
session.pathfinder.rebuild(session.topology);
this._trySetupSide(session, map, AI_PLAYER_ID);
this._trySetupSide(session, map, LOCAL_PLAYER_ID);
if (this._sides.length <= 0) {
console.log('[MonsterCrisisHint] 无有效进路,跳过');
this.unbind();
}
}
/** 帧刷新可见性(战斗进行中) */
tick(): void {
const battle = this._battle;
if (!battle || battle.session.ended || this._sides.length <= 0) return;
this._scanAcc++;
if (this._scanAcc % 8 !== 1) return;
for (let i = 0; i < this._sides.length; i++) {
this._refreshSideVisible(battle, this._sides[i]);
}
}
/** 解除绑定并回收节点 */
unbind(): void {
for (let i = 0; i < this._sides.length; i++) {
this._releaseSideFx(this._sides[i]);
}
this._sides.length = 0;
this._scanAcc = 0;
this._battle = null;
this._map = null;
}
/** 初始化单侧:我方→敌方终点前一格;敌方→我方终点前一格 */
private _trySetupSide(session: BattleSession, map: MapManager, defenderPlayerId: number): void {
const enemyCamp = resolveEnemyCampAreaIdForDefender(session, defenderPlayerId);
if (enemyCamp == null) {
console.log('[MonsterCrisisHint] 进攻阵营缺失', defenderPlayerId);
return;
}
const stand = resolveAttackStandGridForCamp(map, session, enemyCamp);
if (!stand?.node?.isValid) {
console.log('[MonsterCrisisHint] 攻击终点缺失', defenderPlayerId);
return;
}
const port = resolveEnemySpawnPortForDefender(defenderPlayerId);
const spawnGrid = resolveSpawnGridByPort(map, session, port);
if (!spawnGrid) {
console.log('[MonsterCrisisHint] 出怪口格缺失', port, defenderPlayerId);
return;
}
const routePath = this._buildMarchRoute(
session,
{ row: spawnGrid.Row, col: spawnGrid.Col },
{ row: stand.Row, col: stand.Col },
enemyCamp,
);
if (routePath.length < 2) {
console.log('[MonsterCrisisHint] 进路不足', defenderPlayerId, routePath.length);
return;
}
const anchorCell = routePath[routePath.length - 2];
const anchorGrid = map.getMapGrid(anchorCell.row, anchorCell.col);
if (!anchorGrid?.node?.isValid) {
console.log('[MonsterCrisisHint] 锚点格缺失', defenderPlayerId, anchorCell);
return;
}
const watchCells = this._collectWatchCellsBeforeGoal(routePath);
if (watchCells.length <= 0) {
console.log('[MonsterCrisisHint] 监测格为空', defenderPlayerId);
return;
}
const fxNode = this._spawnHintNode(anchorGrid);
if (!fxNode) return;
this._sides.push({
defenderPlayerId,
routePath,
anchorGrid,
watchCells,
fxNode,
skeleton: null,
loopSoundGen: 0,
});
}
/** 出怪口 → 攻击终点完整进路 */
private _buildMarchRoute(session: BattleSession, spawn: Cell, goal: Cell, campAreaId: number): Cell[] {
let path = session.pathfinder.buildPathToTarget(spawn, goal, campAreaId);
if (path.length < 2) {
path = session.pathfinder.findPath(spawn, goal, true, campAreaId);
}
return path;
}
/** 终点前最多 3 格(不含终点) */
private _collectWatchCellsBeforeGoal(path: readonly Cell[]): Cell[] {
const out: Cell[] = [];
const endIdx = path.length - 1;
for (let i = 1; i <= WATCH_CELLS_BEFORE_GOAL && endIdx - i >= 0; i++) {
out.push(path[endIdx - i]);
}
return out;
}
/** 在锚点格挂载危机提示 */
private _spawnHintNode(anchorGrid: MapGrid): Node | null {
if (!anchorGrid?.node?.isValid) return null;
let node: Node | null = null;
try {
node = gg.res.getNode(BATTLE_HERO_BUFF_PATH, CRISIS_HINT_PREFAB_NAME, GBundle.BattleHero);
} catch (err) {
console.log('[MonsterCrisisHint] 预制体加载失败', String(err));
return null;
}
if (!node?.isValid) {
console.log('[MonsterCrisisHint] 预制体缺失', CRISIS_HINT_PREFAB_NAME);
return null;
}
node.parent = anchorGrid.node;
node.setPosition(0, 0, 0);
node.active = false;
return node;
}
/** 按终点前 3 格是否有进攻怪刷新显隐 */
private _refreshSideVisible(battle: ProjectBattleHandle, side: CrisisHintSide): void {
const fxNode = side.fxNode;
if (!fxNode?.isValid) return;
const shouldShow = this._hasMonsterOnWatchCells(battle, side);
const wasShowing = fxNode.active;
if (shouldShow === wasShowing) return;
if (shouldShow) {
fxNode.active = true;
this._startSidePresentation(side);
return;
}
this._stopSidePresentation(side);
fxNode.active = false;
}
/** 显示时:循环播动画,并按每轮时长播危险警报 */
private _startSidePresentation(side: CrisisHintSide): void {
const fxNode = side.fxNode;
if (!fxNode?.isValid) return;
const skel = fxNode.getComponent(sp.Skeleton) ?? fxNode.getComponentInChildren(sp.Skeleton);
if (!skel?.isValid) {
console.log('[MonsterCrisisHint] Spine 缺失');
return;
}
side.skeleton = skel;
BattleSpinePlayback.resetTracks(skel);
BattleSpinePlayback.play(skel, CRISIS_HINT_ANIM, { loop: true, optional: true });
side.loopSoundGen++;
this._scheduleLoopAlert(side, side.loopSoundGen);
}
/** 隐藏时:停止循环音效调度 */
private _stopSidePresentation(side: CrisisHintSide): void {
side.loopSoundGen++;
const skel = side.skeleton;
if (skel?.isValid) BattleSpinePlayback.clearPendingComplete(skel);
side.skeleton = null;
}
/** 每播完一轮 animation 播一次危险警报(随战斗时钟) */
private _scheduleLoopAlert(side: CrisisHintSide, gen: number): void {
if (gen !== side.loopSoundGen) return;
if (!side.fxNode?.active) return;
const skel = side.skeleton;
if (!skel?.isValid) return;
const delay = BattleSpinePlayback.resolveLogicalCompleteDelay(skel, CRISIS_HINT_ANIM);
if (!delay || delay <= 0) {
console.log('[MonsterCrisisHint] 动画时长缺失', CRISIS_HINT_ANIM);
BattleAudio.playMonsterCrisisAlert();
return;
}
BattleAudio.playMonsterCrisisAlert();
schedulePresentation(delay, () => {
if (gen !== side.loopSoundGen) return;
if (!side.fxNode?.active) return;
this._scheduleLoopAlert(side, gen);
});
}
/** 终点前监测格上是否存在进攻该防守方的存活怪 */
private _hasMonsterOnWatchCells(battle: ProjectBattleHandle, side: CrisisHintSide): boolean {
const session = battle.session;
let found = false;
session.world.forEachAlive((entityId) => {
if (found) return;
if (!this._isMarchMonsterCandidate(battle, side.defenderPlayerId, entityId)) return;
const tf = session.world.getComponent(entityId, TransformComp);
if (!tf) return;
if (!this._isWatchCell(side.watchCells, tf.row, tf.col)) return;
found = true;
});
return found;
}
/** 是否为进攻指定防守方的行进怪 */
private _isMarchMonsterCandidate(battle: ProjectBattleHandle, defenderPlayerId: number, entityId: EntityId): boolean {
const session = battle.session;
const world = session.world;
if (!world.isAlive(entityId)) return false;
if (isMechanismUnit(session, entityId)) return false;
const team = world.getComponent(entityId, TeamComp);
if (!team || !BattleTeamId.isEnemyTeam(team.team)) return false;
if (Math.max(0, Math.floor(Number(team.ownerId) || 0)) !== defenderPlayerId) return false;
const row = battle.resolveMonsterRow(entityId);
if (!row) return false;
const type = Math.floor(Number(row.type));
if (type !== MonsterType.SmallMonster && type !== MonsterType.Boss && type !== MonsterType.SummonMonster) {
return false;
}
const move = world.getComponent(entityId, MovementComp);
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) {
return false;
}
return true;
}
/** 格是否在监测列表中 */
private _isWatchCell(watchCells: readonly Cell[], row: number, col: number): boolean {
for (let i = 0; i < watchCells.length; i++) {
const c = watchCells[i];
if (c.row === row && c.col === col) return true;
}
return false;
}
/** 回收单侧表现节点 */
private _releaseSideFx(side: CrisisHintSide): void {
this._stopSidePresentation(side);
const node = side.fxNode;
side.fxNode = null;
if (!node?.isValid) return;
node.removeFromParent();
gg.res.putNode(node);
}
}
let _inst: BattleGameMonsterCrisisHintPresenter | null = null;
/** 怪物危机提示单例 */
export function getBattleGameMonsterCrisisHintPresenter(): BattleGameMonsterCrisisHintPresenter {
if (!_inst) _inst = new BattleGameMonsterCrisisHintPresenter();
return _inst;
}