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); } }