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.
330 lines
12 KiB
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);
|
|
}
|
|
}
|
|
|