import type { BattleServices } from 'db://assets/mx/module/battle/combat/BattleServices'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { MapCellKind } from 'db://assets/mx/module/merge'; import { BattleTeamId, TeamComp } from 'db://assets/mx/module/battle'; import type { ProjectBattleHandle } from '../runtime/BattleRuntime'; import { AI_PLAYER_ID } from '../ai/runtime/AiCampArea'; import { findEntityIdByGridIndex, findGridIndexForEntity } from '../runtime/DeployEntityQuery'; import { isMonsterDefaultEnemyTarget, resolveTowerHeroId } from '../config/BattleHeroTable'; import type { Player } from '../../game/Player'; import type { BattleBagSession } from '../bag/BattleBagSession'; import { getTowerLevelDownHost } from './TowerLevelDownHost'; import { getTowerRuinHost } from './TowerRuinHost'; /** 技能改棋盘时的本机表现(由 CocosBagBoardView 开战绑定) */ export interface ISkillAtkBuffBoardView { /** 拆格后清本机格子表现 */ vacateDestroyedMapCell(gridIndex: number): void; /** * 施法格产桃演出;未接 UI 返回 false。 * @param gridIndex 施法格 * @param peachCount 蟠桃数量 * @param ownerPlayerId 所属玩家 */ notifySkillPeachGain(gridIndex: number, peachCount: number, ownerPlayerId?: number): boolean; } /** 技能拆格时的英雄演员销毁口 */ export interface ISkillAtkBuffActorHost { /** 按实体销毁演员 */ destroy(entity: EntityId): void; /** 按格销毁演员 */ destroyAtGrid(gridIndex: number): void; } /** 棋盘表现口,避免本文件回引 View / AiBattleBridge / HeroActorHost */ export class SkillAtkBuffBoardViewBind { /** 本机棋盘表现 */ private static _view: ISkillAtkBuffBoardView | null = null; /** 敌方棋盘变更通知 */ private static _notifyEnemy: (() => void) | null = null; /** 英雄演员销毁 */ private static _actors: ISkillAtkBuffActorHost | null = null; /** * 绑定本机棋盘表现。 * @param view 棋盘;传 null 清除 */ static bindView(view: ISkillAtkBuffBoardView | null): void { this._view = view; } /** * 绑定敌方棋盘变更通知。 * @param fn 通知;传 null 清除 */ static bindEnemyNotify(fn: (() => void) | null): void { this._notifyEnemy = fn; } /** 当前本机棋盘表现 */ static get view(): ISkillAtkBuffBoardView | null { return this._view; } /** 通知 AI 侧棋盘已变 */ static notifyEnemyChanged(): void { this._notifyEnemy?.(); } /** * 绑定英雄演员销毁。 * @param host 演员宿主;传 null 清除 */ static bindActors(host: ISkillAtkBuffActorHost | null): void { this._actors = host; } /** * 按实体销毁演员。 * @param entity 实体 */ static destroyActor(entity: EntityId): void { if (!this._actors) { console.warn('[SkillAtkBuffBoard] 英雄演员未绑定', entity); return; } this._actors.destroy(entity); } /** * 按格销毁演员。 * @param gridIndex 格 */ static destroyActorAtGrid(gridIndex: number): void { if (!this._actors) { console.warn('[SkillAtkBuffBoard] 英雄演员未绑定 grid', gridIndex); return; } this._actors.destroyAtGrid(gridIndex); } } /** 需访问 Cocos/背包 的 atk_buff 效果 */ export interface SkillAtkBuffBoardRequest { /** atk_buff 头类型 */ kind: number; /** 施法者所属玩家 */ ownerPlayerId: number; /** 目标实体 */ target?: EntityId; /** 等级变化(负为降) */ levelDelta?: number; /** 降级持续秒(类型7) */ durationSec?: number; /** 桃子数量 */ peachCount?: number; } /** 战斗内棋盘/经济类 atk_buff */ export function applySkillAtkBuffBoardEffect(services: BattleServices, req: SkillAtkBuffBoardRequest): void { const battle = resolveBattleHandle(); if (!battle) { console.log('[SkillAtkBuffBoard] 无战斗句柄', req.kind); return; } if (req.kind === 5) { const count = Math.max(0, Math.floor(Number(req.peachCount) || 0)); const ownerId = Math.floor(Number(req.ownerPlayerId) || 0); if (count > 0 && (ownerId === 0 || ownerId === AI_PLAYER_ID)) { if (!tryFlySkillPeach(battle, req.target, count, ownerId)) { if (ownerId === 0) battle.addCoinNum(count); else battle.addPlayerGold(ownerId, count); } } return; } if (req.kind === 7) { if (req.target == null) return; getTowerLevelDownHost().apply( battle, req.target, Math.abs(req.levelDelta ?? 1), Number(req.durationSec) || 0, ); return; } if (req.kind === 9) { console.log('[SkillAtkBuffBoard] atk_buff 类型9由怪物 Host 封格,不走此路径', req.ownerPlayerId); return; } if (req.kind === 12) { if (req.target == null) { console.log('[SkillAtkBuffBoard] 损毁缺少目标'); return; } getTowerRuinHost().apply(battle, req.target); } } /** * 从施法格触发产桃演出;本机飘桃到 ui_money,敌方格心 coinFly 原地弹跳;无视图或找不到格时返回 false。 * @param battle 战斗句柄 * @param caster 产桃施法者 * @param count 蟠桃数量 */ function tryFlySkillPeach(battle: ProjectBattleHandle, caster: EntityId | undefined, count: number, ownerPlayerId = 0): boolean { const view = SkillAtkBuffBoardViewBind.view; if (!view) return false; if (caster == null) { console.log('[SkillAtkBuffBoard] 产桃缺少施法者'); return false; } const player = battle.getPlayer(ownerPlayerId); const map = battle.Map; if (!player || !map) { console.log('[SkillAtkBuffBoard] 产桃缺少玩家或棋盘', caster, ownerPlayerId); return false; } const grid = findGridIndexForEntity(caster, player, battle.session, map); if (grid == null) { console.log('[SkillAtkBuffBoard] 产桃找不到格', caster, ownerPlayerId); return false; } return view.notifySkillPeachGain(grid, count, ownerPlayerId); } function resolveBattleHandle(): ProjectBattleHandle | null { const battle = gg.game.CurentBattle as ProjectBattleHandle | null | undefined; return battle?.session ? battle : null; } /** * 随机选取被进攻玩家可封的可放置格(空 Put 可选;组合武将与组合字跳过)。 * @param battle 战斗句柄 * @param caster 施法怪物 * @param count 封格数量 */ export function pickSealablePutTiles(battle: ProjectBattleHandle, caster: EntityId, count: number): number[] { const picked: number[] = []; const n = Math.floor(count); if (!(n > 0)) { console.log('[SkillAtkBuffBoard] 封格数量无效', count); return picked; } const bag = resolveSealTargetBag(battle, caster); if (!bag) return picked; for (let i = 0; i < n; i++) { const pick = pickSealablePutTile(battle, bag, picked); if (pick == null) { if (picked.length <= 0) { const team = battle.session.world.getComponent(caster, TeamComp); const ownerId = Math.floor(Number(team?.ownerId) || 0); const targetPlayerId = team && BattleTeamId.isEnemyTeam(team.team) ? ownerId : 1 - ownerId; console.log('[SkillAtkBuffBoard] 无可封可放置格', targetPlayerId); } break; } picked.push(pick); } return picked; } /** * atk_buff 类型 9:把已选可放置格改为可开挖,并播封格 fill 渐显。 * @param battle 战斗句柄 * @param caster 施法怪物 * @param gridIndices 目标格 */ export function sealPlaceableTiles(battle: ProjectBattleHandle, caster: EntityId, gridIndices: readonly number[]): void { const session = battle.session; const team = session.world.getComponent(caster, TeamComp); if (!team) { console.log('[SkillAtkBuffBoard] 封格缺少 TeamComp', caster); return; } const ownerId = Math.floor(Number(team.ownerId) || 0); const targetPlayerId = BattleTeamId.isEnemyTeam(team.team) ? ownerId : 1 - ownerId; const player = battle.getPlayer(targetPlayerId); const bag = player?.bagSession; const map = battle.Map; if (!player || !bag || !map) { console.log('[SkillAtkBuffBoard] 封格缺少玩家或棋盘', caster, targetPlayerId); return; } for (let i = 0; i < gridIndices.length; i++) { const idx = gridIndices[i]; if (!sealOnePutTile(battle, player, bag, idx)) continue; const mg = map.getMapGridByIndex(idx); if (mg?.node?.isValid) mg.playSealDigFillCover(); else console.log('[SkillAtkBuffBoard] 封格无 MapGrid,跳过 fill', idx); } } /** * 解析封格目标玩家背包。 * @param battle 战斗句柄 * @param caster 施法怪物 */ function resolveSealTargetBag(battle: ProjectBattleHandle, caster: EntityId): BattleBagSession | null { const team = battle.session.world.getComponent(caster, TeamComp); if (!team) { console.log('[SkillAtkBuffBoard] 封格缺少 TeamComp', caster); return null; } const ownerId = Math.floor(Number(team.ownerId) || 0); const targetPlayerId = BattleTeamId.isEnemyTeam(team.team) ? ownerId : 1 - ownerId; const bag = battle.getPlayer(targetPlayerId)?.bagSession; if (!bag) { console.log('[SkillAtkBuffBoard] 封格缺少玩家或棋盘', caster, targetPlayerId); return null; } return bag; } /** * 随机一格己方可放置且非多格合成的格。 * @param battle 战斗句柄 * @param bag 目标玩家背包 * @param except 已选格 */ function pickSealablePutTile(battle: ProjectBattleHandle, bag: BattleBagSession, except: readonly number[]): number | null { const candidates: number[] = []; const indices = bag.merge.map.gridIndices; for (let i = 0; i < indices.length; i++) { const idx = indices[i]; if (isPickedGrid(except, idx)) continue; if (!bag.validator.isAllowedCampForPlace(idx)) continue; if (bag.merge.map.getCellKind(idx) !== MapCellKind.Put) continue; if (bag.merge.isMultiCellCompositionAt(idx)) continue; candidates.push(idx); } if (!candidates.length) return null; return candidates[battle.session.rng.int(0, candidates.length - 1)]; } /** * 已选格是否含该格。 * @param list 已选 * @param value 格 */ function isPickedGrid(list: readonly number[], value: number): boolean { for (let i = 0; i < list.length; i++) { if (list[i] === value) return true; } return false; } /** * 封一格:有单字则卖掉单位,格改为可开挖。 * @param battle 战斗句柄 * @param player 被封格的玩家 * @param bag 该玩家背包 * @param gridIndex 目标格 * @returns 是否改格成功 */ function sealOnePutTile(battle: ProjectBattleHandle, player: Player, bag: BattleBagSession, gridIndex: number): boolean { const session = battle.session; const map = battle.Map; const hadTile = bag.merge.map.hasTile(gridIndex); const entity = map ? findEntityIdByGridIndex(gridIndex, session, map, player) : null; if (!bag.skillSealPutToDig(gridIndex)) return false; if (hadTile && entity != null && session.world.isAlive(entity)) { const sold = session.deploy.sell(player.playerId, entity); if (sold) { SkillAtkBuffBoardViewBind.destroyActor(entity); } else { console.log('[SkillAtkBuffBoard] 封格卖出失败', entity, player.playerId, gridIndex); } } SkillAtkBuffBoardViewBind.destroyActorAtGrid(gridIndex); if (player.isLocal) { SkillAtkBuffBoardViewBind.view?.vacateDestroyedMapCell(gridIndex); battle.deploySync?.reconcileFromMerge('skillSeal'); } else { SkillAtkBuffBoardViewBind.notifyEnemyChanged(); } return true; } /** * atk_buff 类型 12:强制拆掉单格字并卖掉 ECS 单位。 * @param battle 战斗句柄 * @param target 锁定的非组合字实体 */ export function destroyBasicHeroTile(battle: ProjectBattleHandle, target: EntityId): void { const session = battle.session; if (session.world.isAlive(target) && !isMonsterDefaultEnemyTarget(session.world, target)) { console.log('[SkillAtkBuffBoard] 损毁目标不是非组合字', target); return; } const team = session.world.getComponent(target, TeamComp); const hintPlayerId = Math.floor(Number(team?.ownerId) || 0); const map = battle.Map; if (!map) { console.log('[SkillAtkBuffBoard] 损毁找不到地图', target); return; } const located = locateDestroyTile(battle, target, hintPlayerId); if (!located) { console.log('[SkillAtkBuffBoard] 损毁找不到字块', target, hintPlayerId); return; } const reason = located.bag.merge.applySkillDestroyMap(located.gridIndex); if (reason != null) { console.log('[SkillAtkBuffBoard] 损毁拆格失败', target, located.gridIndex, reason); return; } const ownerId = located.player.playerId; if (session.world.isAlive(target)) { const sold = session.deploy.sell(ownerId, target); if (sold) { SkillAtkBuffBoardViewBind.destroyActor(target); } else { console.log('[SkillAtkBuffBoard] 损毁卖出失败', target, ownerId); } } else { SkillAtkBuffBoardViewBind.destroyActor(target); } SkillAtkBuffBoardViewBind.destroyActorAtGrid(located.gridIndex); if (located.player.isLocal) { SkillAtkBuffBoardViewBind.view?.vacateDestroyedMapCell(located.gridIndex); battle.deploySync?.reconcileFromMerge('skillDestroy'); } else { SkillAtkBuffBoardViewBind.notifyEnemyChanged(); } } /** 损毁定位结果 */ interface DestroyTileHit { /** 持有该格的玩家 */ player: Player; /** 该玩家背包 */ bag: BattleBagSession; /** 合成格 */ gridIndex: number; } /** * 在本机/对手背包中定位目标字块格。 * @param battle 战斗句柄 * @param target 实体 * @param hintPlayerId TeamComp.ownerId */ function locateDestroyTile(battle: ProjectBattleHandle, target: EntityId, hintPlayerId: number): DestroyTileHit | null { const order: Player[] = []; const hinted = battle.getPlayer(hintPlayerId); if (hinted) order.push(hinted); const otherId = hintPlayerId === 0 ? 1 : 0; const other = battle.getPlayer(otherId); if (other && other !== hinted) order.push(other); for (let i = 0; i < order.length; i++) { const hit = locateDestroyTileOnPlayer(battle, target, order[i]); if (hit) return hit; } return null; } /** * 在指定玩家合成体中定位目标格。 * @param battle 战斗句柄 * @param target 实体 * @param player 玩家 */ function locateDestroyTileOnPlayer(battle: ProjectBattleHandle, target: EntityId, player: Player): DestroyTileHit | null { const bag = player.bagSession; const map = battle.Map; if (!bag || !map) return null; const session = battle.session; const comps = player.compositions; for (let i = 0; i < comps.length; i++) { const indices = comps[i].gridIndices; for (let j = 0; j < indices.length; j++) { const idx = indices[j]; if (findEntityIdByGridIndex(idx, session, map, player) === target) { return { player, bag, gridIndex: idx }; } } } const heroId = resolveTowerHeroId(session.world, target); if (heroId > 0) { for (let i = 0; i < comps.length; i++) { const comp = comps[i]; if (comp.weapon1Id !== heroId || comp.gridIndices.length !== 1) continue; const idx = comp.gridIndices[0]; if (bag.merge.map.hasTile(idx)) { return { player, bag, gridIndex: idx }; } } } const fallback = findGridIndexForEntity(target, player, session, map); if (fallback != null && bag.merge.map.hasTile(fallback)) { return { player, bag, gridIndex: fallback }; } return null; }