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

330 lines
12 KiB

import { Node, Sorting2D, sp } from 'cc';
import type { BattleEvent, EntityId } from 'db://assets/mx/module/battle';
import {
BATTLE_EVENT_ALL, BattleEventType, BattleTeamId, BuffContainerComp, MovementComp, TeamComp, TransformComp,
} from 'db://assets/mx/module/battle';
import { GBundle } from '../../game/ConfigRes';
import { BATTLE_HERO_BUFF_PATH } from '../../battle/res/BattleHeroResCollector';
import { isEnemyMonsterWalkGrid } from '../../battle/map/MapPathQueries';
import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime';
import type { MapManager } from '../../battle/map/legacy/MapManager';
/** 缚仙网陷阱预制体(battleHero/prefab/buff/缚仙网) */
const BIND_NET_TRAP_PREFAB_DIR = BATTLE_HERO_BUFF_PATH;
const BIND_NET_TRAP_PREFAB_NAME = '缚仙网';
/** 缚仙网捕获表现(battleHero/prefab/buff/缚仙网捕获) */
const BIND_NET_CAPTURE_PREFAB_NAME = '缚仙网捕获';
/** 缚仙网捕获 Sorting2D(须高于怪物 skin 的 order=2) */
const BIND_NET_CAPTURE_SORT_LAYER = 3;
const BIND_NET_CAPTURE_SORT_ORDER = 10;
/** 缚仙网捕获相对怪物根节点的 Y 偏移(本地坐标,向上为正) */
const BIND_NET_CAPTURE_OFFSET_Y = 30;
/** 缚仙网定身 debuff(MovementSystem 移速归零,不触发 debuff_4 眩晕特效) */
const BIND_NET_IMMOBILIZE_BUFF_ID = 'debuff_bind_net';
/** 单格缚仙网陷阱 */
type BindNetTrapEntry = {
/** 地图格索引 */
gridIndex: number;
/** 行 */
row: number;
/** 列 */
col: number;
/** 网住时长(秒) */
bindDurationSec: number;
/** 表现节点 */
node: Node;
};
/** 怪物身上的缚仙网捕获表现 */
type BindNetCaptureEntry = {
/** 被网住的实体 */
entityId: EntityId;
/** 捕获预制体节点 */
node: Node;
};
/**
* 缚仙网:地图行走格布置陷阱,首个触碰的敌人定身并挂捕获预制体。
*/
export class BattleGameBindNetTrapController {
private _battle: ProjectBattleHandle | null = null;
private _map: MapManager | null = null;
/** 防守方 playerId(陷阱布置在进攻该方的行军路上) */
private _defenderPlayerId = 0;
private readonly _traps: BindNetTrapEntry[] = [];
private readonly _captures: BindNetCaptureEntry[] = [];
private _battleListener: ((e: BattleEvent) => void) | null = null;
/**
* @param battle 战斗句柄
* @param map 地图管理器
* @param defenderPlayerId 防守方;默认本机 0
*/
bind(battle: ProjectBattleHandle | null, map: MapManager | null, defenderPlayerId: number = 0): void {
this.unbind();
this._battle = battle;
this._map = map;
this._defenderPlayerId = Math.max(0, Math.floor(Number(defenderPlayerId) || 0));
if (battle) {
this._battleListener = (e) => this._onBattleEvent(e);
battle.on(BATTLE_EVENT_ALL, this._battleListener);
}
}
/** 释放全部陷阱与捕获表现 */
unbind(): void {
if (this._battle && this._battleListener) {
this._battle.off(BATTLE_EVENT_ALL, this._battleListener);
}
this._battleListener = null;
for (let i = 0; i < this._traps.length; i++) {
this._releaseTrapNode(this._traps[i].node);
}
this._traps.length = 0;
for (let i = 0; i < this._captures.length; i++) {
this._releaseCaptureNode(this._captures[i].node);
}
this._captures.length = 0;
this._battle = null;
this._map = null;
}
/** 帧更新:检测触碰 */
tick(_dt: number): void {
if (!this._battle || this._battle.session.ended || this._traps.length === 0) return;
for (let i = this._traps.length - 1; i >= 0; i--) {
const trap = this._traps[i];
if (!trap.node?.isValid) {
this._traps.splice(i, 1);
continue;
}
const victim = this._findEnemyOnCell(trap.row, trap.col);
if (victim == null) continue;
this._triggerTrap(trap, victim);
this._traps.splice(i, 1);
}
}
/** 格上是否已有陷阱 */
hasTrapAt(gridIndex: number): boolean {
const idx = Number(gridIndex) || 0;
for (let i = 0; i < this._traps.length; i++) {
if (this._traps[i].gridIndex === idx) return true;
}
return false;
}
/**
* 布置陷阱
* @param gridIndex 地图格索引
* @param bindDurationSec 网住时长(functionpar 第二段)
* @returns 是否成功
*/
tryPlace(gridIndex: number, bindDurationSec: number): boolean {
const map = this._map;
if (!map?.gridsInitialized || bindDurationSec <= 0) return false;
if (this.hasTrapAt(gridIndex)) return false;
const mg = map.getMapGridByIndex(gridIndex);
if (!mg?.node?.isValid || !mg.isCanWalk) return false;
const battle = this._battle;
if (!battle || !isEnemyMonsterWalkGrid(map, battle.session, mg, null, this._defenderPlayerId)) return false;
const node = this._spawnTrapNode(mg.node);
if (!node) return false;
this._traps.push({
gridIndex,
row: mg.Row,
col: mg.Col,
bindDurationSec,
node,
});
return true;
}
private _onBattleEvent(event: BattleEvent): void {
switch (event.type) {
case BattleEventType.BuffRemoved:
if (event.buffId === BIND_NET_IMMOBILIZE_BUFF_ID) {
this._releaseCapture(event.target);
}
break;
case BattleEventType.EntityKilled:
this._releaseCapture(event.victim);
break;
case BattleEventType.UnitDespawned:
this._releaseCapture(event.entity);
break;
default:
break;
}
}
/** 加载并挂到地图格 */
private _spawnTrapNode(parent: Node): Node | null {
if (!parent?.isValid) return null;
let node: Node | null = null;
try {
node = gg.res.getNode(BIND_NET_TRAP_PREFAB_DIR, BIND_NET_TRAP_PREFAB_NAME, GBundle.BattleHero);
} catch (e) {
console.log('[BindNetTrap] 缚仙网预制体加载失败', String(e));
return null;
}
if (!node?.isValid) {
console.log('[BindNetTrap] 缚仙网预制体缺失', BIND_NET_TRAP_PREFAB_NAME);
return null;
}
node.parent = parent;
node.setPosition(0, 0, 0);
node.active = true;
return node;
}
/** 在怪物身上挂捕获预制体 */
private _spawnCaptureOnMonster(entityId: EntityId): Node | null {
const parent = this._resolveMonsterFxParent(entityId);
if (!parent?.isValid) {
console.log('[BindNetTrap] 捕获表现挂载点缺失 entity=', entityId);
return null;
}
let node: Node | null = null;
try {
node = gg.res.getNode(BIND_NET_TRAP_PREFAB_DIR, BIND_NET_CAPTURE_PREFAB_NAME, GBundle.BattleHero);
} catch (e) {
console.log('[BindNetTrap] 缚仙网捕获预制体加载失败', String(e));
return null;
}
if (!node?.isValid) {
console.log('[BindNetTrap] 缚仙网捕获预制体缺失', BIND_NET_CAPTURE_PREFAB_NAME);
return null;
}
this._applyCaptureFxLayer(node, parent);
return node;
}
/**
* 缚仙网捕获层级:Sorting2D 须在 Spine 节点上(与怪_晕眩一致),且 order 高于怪物 skin。
*/
private _applyCaptureFxLayer(captureRoot: Node, monsterRoot: Node): void {
captureRoot.layer = monsterRoot.layer;
captureRoot.setParent(monsterRoot);
captureRoot.setPosition(0, BIND_NET_CAPTURE_OFFSET_Y, 0);
captureRoot.active = true;
const skin = monsterRoot.getChildByName('skin');
if (skin?.isValid) {
captureRoot.setSiblingIndex(skin.getSiblingIndex() + 1);
} else {
captureRoot.setSiblingIndex(monsterRoot.children.length - 1);
}
for (const sk of captureRoot.getComponentsInChildren(sp.Skeleton)) {
if (!sk?.node?.isValid) continue;
sk.node.layer = monsterRoot.layer;
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME);
sk.setAnimation(0, '捕获', true);
let sorting = sk.node.getComponent(Sorting2D);
if (!sorting) sorting = sk.node.addComponent(Sorting2D);
sorting.sortingLayer = BIND_NET_CAPTURE_SORT_LAYER;
sorting.sortingOrder = BIND_NET_CAPTURE_SORT_ORDER;
}
for (const sort of captureRoot.getComponents(Sorting2D)) {
sort.sortingLayer = BIND_NET_CAPTURE_SORT_LAYER;
sort.sortingOrder = BIND_NET_CAPTURE_SORT_ORDER;
}
}
/** 怪物根节点(捕获网包住 body,挂在 skin 之上) */
private _resolveMonsterFxParent(entityId: EntityId): Node | null {
const battle = this._battle;
if (!battle) return null;
const root = battle.getMonsterPresenter()?.getEntityNodeMap().get(entityId);
return root?.isValid ? root : null;
}
/** 释放陷阱节点 */
private _releaseTrapNode(node: Node | null | undefined): void {
if (!node?.isValid) return;
if (node['resKey']) {
gg.res.putNode(node);
} else {
node.destroy();
}
}
/** 释放捕获节点 */
private _releaseCaptureNode(node: Node | null | undefined): void {
if (!node?.isValid) return;
if (node['resKey']) {
gg.res.putNode(node);
} else {
node.destroy();
}
}
/** 移除指定实体的捕获表现 */
private _releaseCapture(entityId: EntityId): void {
for (let i = this._captures.length - 1; i >= 0; i--) {
if (this._captures[i].entityId !== entityId) continue;
this._releaseCaptureNode(this._captures[i].node);
this._captures.splice(i, 1);
}
}
/** 查找已到达陷阱格中心的存活敌人(MovementSystem 抵达 path 格心才更新 TransformComp) */
private _findEnemyOnCell(row: number, col: number): EntityId | null {
const battle = this._battle;
if (!battle) return null;
const world = battle.session.world;
let found: EntityId | null = null;
world.forEachAlive((id) => {
if (found != null) return;
const team = world.getComponent(id, TeamComp);
if (!team || !BattleTeamId.isEnemyTeam(team.team)) return;
if (Math.max(0, Math.floor(Number(team.ownerId) || 0)) !== this._defenderPlayerId) return;
const move = world.getComponent(id, MovementComp);
if (move?.spawnProtected) return;
const tf = world.getComponent(id, TransformComp);
if (!tf || tf.row !== row || tf.col !== col) return;
found = id;
});
return found;
}
/** 触发:定身 + 捕获表现 + 移除地面陷阱 */
private _triggerTrap(trap: BindNetTrapEntry, victim: EntityId): void {
const battle = this._battle;
if (!battle) return;
const world = battle.session.world;
if (!world.isAlive(victim)) return;
this._releaseCapture(victim);
const duration = Math.max(0.05, trap.bindDurationSec);
battle.session.buffs.add(
victim,
BIND_NET_IMMOBILIZE_BUFF_ID,
0 as EntityId,
1,
{ duration },
);
const buffs = world.getComponent(victim, BuffContainerComp);
if (!buffs?.buffs.some((b) => b.buffId === BIND_NET_IMMOBILIZE_BUFF_ID)) {
console.log('[BindNetTrap] 定身 buff 施加失败 entity=', victim, 'grid=', trap.gridIndex);
this._releaseTrapNode(trap.node);
return;
}
const captureNode = this._spawnCaptureOnMonster(victim);
if (captureNode) {
this._captures.push({ entityId: victim, node: captureNode });
}
this._releaseTrapNode(trap.node);
}
}