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/battle/combat/SkillAtkBuffBoardBridge.ts

457 lines
16 KiB

3 weeks ago
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;
}
/**
2 weeks ago
* 随机一格己方可放置且非多格合成的格。
3 weeks ago
* @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;
}