import { Node, sp } from 'cc'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { BattleEventFactory, BattleTeamId, BuffContainerComp, HealthComp, MovementComp, TeamComp, TransformComp, } from 'db://assets/mx/module/battle'; import { syncMonsterNode } from '../../battle/bridge/CocosBattleMovementBridge'; import { GBundle } from '../../game/ConfigRes'; import { BATTLE_HERO_BUFF_PATH } from '../../battle/res/BattleHeroResCollector'; import { isEnemyMonsterWalkGrid } from '../../battle/map/MapPathQueries'; import { BattleSpinePlayback } from '../../battle/presentation/BattleSpinePlayback'; import { schedulePresentation } from '../../battle/skill/BattlePresentationClock'; import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime'; import type { MapManager } from '../../battle/map/legacy/MapManager'; /** 陷阱预制体(battleHero/prefab/buff/三味真火,资源名与表 icon「三昧真火」不同) */ const FIRE_TRAP_PREFAB_DIR = BATTLE_HERO_BUFF_PATH; const FIRE_TRAP_PREFAB_NAME = '三味真火'; /** 布置后循环播放 */ const FIRE_TRAP_ANIM_IDLE = '持续'; /** 兵将到达格心触发 */ const FIRE_TRAP_ANIM_BURST = '持续3'; /** 局内伤害类型 */ const FIRE_TRAP_DAMAGE_TYPE = 'fire_trap'; /** 无施法者时的占位 caster */ const FIRE_TRAP_CASTER = 0 as EntityId; /** 三昧真火定身 debuff(MovementSystem 移速归零) */ const FIRE_TRAP_IMMOBILIZE_BUFF_ID = 'debuff_fire_trap'; /** 播「持续3」后等待再飘血(逻辑秒) */ const FIRE_TRAP_DAMAGE_DELAY_SEC = 1; /** 飘字后再击杀,避免死亡特效抢在伤害数字前(逻辑秒) */ const FIRE_TRAP_KILL_DELAY_SEC = 0.4; /** 「持续3」时长读不到时的回收兜底(逻辑秒) */ const FIRE_TRAP_BURST_FALLBACK_SEC = 1.2; /** 单格火焰陷阱 */ type FireTrapEntry = { /** 地图格索引 */ gridIndex: number; /** 行 */ row: number; /** 列 */ col: number; /** 伤害上限 */ maxDamage: number; /** 剩余持续时间(秒) */ remainSec: number; /** 表现节点 */ node: Node; /** 陷阱 Spine */ skel: sp.Skeleton | null; /** 是否已进入触发序列(播持续3 / 等待伤害) */ triggering: boolean; /** 是否已调度回收(防重复) */ releaseScheduled: boolean; }; /** * 三昧真火(functiontype=24):怪物行走格布置陷阱,循环「持续」; * 兵将到达格心播「持续3」并定身,1s 后飘血;致死延迟击杀以播死亡特效。 */ export class BattleGameFireTrapController { private _battle: ProjectBattleHandle | null = null; private _map: MapManager | null = null; /** 防守方 playerId(陷阱布置在进攻该方的行军路上) */ private _defenderPlayerId = 0; private readonly _traps: FireTrapEntry[] = []; /** 打断 pending 伤害/击杀调度 */ private _triggerGen = 0; /** * @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)); } /** 释放全部陷阱 */ unbind(): void { this._triggerGen++; this._releaseAllVictims(); for (let i = 0; i < this._traps.length; i++) { this._releaseTrapNode(this._traps[i].node, this._traps[i].skel); } this._traps.length = 0; this._battle = null; this._map = null; this._defenderPlayerId = 0; } /** 帧更新:超时移除 / 检测格心触碰 */ tick(dt: number): void { if (!this._battle || this._battle.session.ended) return; const delta = Math.max(0, dt); 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; } if (trap.triggering) continue; trap.remainSec -= delta; if (trap.remainSec <= 0) { this._releaseTrapNode(trap.node, trap.skel); this._traps.splice(i, 1); continue; } const victim = this._findEnemyOnCell(trap.row, trap.col); if (victim == null) continue; this._beginTrigger(trap, victim); } } /** 格上是否已有陷阱 */ 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 maxDamage 伤害上限(functionpar 第二段) * @param durationSec 陷阱持续时间(functionpar 第三段) * @returns 是否成功 */ tryPlace(gridIndex: number, maxDamage: number, durationSec: number): boolean { const map = this._map; if (!map?.gridsInitialized || maxDamage <= 0 || durationSec <= 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 spawned = this._spawnTrapNode(mg.node); if (!spawned) return false; this._traps.push({ gridIndex, row: mg.Row, col: mg.Col, maxDamage, remainSec: durationSec, node: spawned.node, skel: spawned.skel, triggering: false, releaseScheduled: false, }); return true; } /** 加载并挂到地图格,循环播「持续」 */ private _spawnTrapNode(parent: Node): { node: Node; skel: sp.Skeleton | null } | null { if (!parent?.isValid) return null; let node: Node | null = null; try { node = gg.res.getNode(FIRE_TRAP_PREFAB_DIR, FIRE_TRAP_PREFAB_NAME, GBundle.BattleHero); } catch (e) { console.log('[FireTrap] 三昧真火预制体加载失败', String(e)); return null; } if (!node?.isValid) { console.log('[FireTrap] 三昧真火预制体缺失', FIRE_TRAP_PREFAB_NAME); return null; } node.parent = parent; node.setPosition(0, 0, 0); node.active = true; const skel = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); if (skel?.isValid) { skel.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME); const ok = BattleSpinePlayback.play(skel, FIRE_TRAP_ANIM_IDLE, { loop: true }); if (!ok) { console.log('[FireTrap] 陷阱待机动画缺失', FIRE_TRAP_ANIM_IDLE); } } else { console.log('[FireTrap] 陷阱节点缺少 sp.Skeleton'); } return { node, skel: skel?.isValid ? skel : null }; } /** 释放陷阱节点 */ private _releaseTrapNode(node: Node | null | undefined, skel: sp.Skeleton | null | undefined): void { if (skel?.isValid) { BattleSpinePlayback.clearPendingComplete(skel); } if (!node?.isValid) return; if (node['resKey']) { gg.res.putNode(node); } else { node.destroy(); } } /** 查找已到达陷阱格中心的存活敌人(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; } /** 格心触碰:定身 + 播「持续3」,1s 后结算伤害;陷阱等「持续3」播完再回收 */ private _beginTrigger(trap: FireTrapEntry, victim: EntityId): void { const battle = this._battle; if (!battle || trap.triggering) return; trap.triggering = true; if (!this._lockVictimOnTrap(victim, trap.row, trap.col)) { console.log('[FireTrap] 定身失败 entity=', victim, 'grid=', trap.gridIndex); trap.triggering = false; return; } const skel = trap.skel; if (skel?.isValid) { const ok = BattleSpinePlayback.play(skel, FIRE_TRAP_ANIM_BURST, { loop: false, onComplete: () => this._scheduleTrapRelease(trap), }); if (!ok) { console.log('[FireTrap] 陷阱爆发动画缺失', FIRE_TRAP_ANIM_BURST); } this._scheduleTrapReleaseFallback(trap, skel); } else { this._scheduleTrapRelease(trap); } const scheduleGen = this._triggerGen; schedulePresentation(FIRE_TRAP_DAMAGE_DELAY_SEC, () => { if (scheduleGen !== this._triggerGen) return; this._applyTrapDamage(trap, victim, scheduleGen); }); } /** * 定身并钉在格心:presentLocked + 定身 buff 覆盖至飘血/击杀全流程。 * @returns 是否成功 */ private _lockVictimOnTrap(victim: EntityId, row: number, col: number): boolean { const battle = this._battle; const map = this._map; if (!battle || !map) return false; const session = battle.session; const world = session.world; if (!world.isAlive(victim)) return false; const move = world.getComponent(victim, MovementComp); if (!move) return false; const center = session.grid.cellToWorld({ row, col }); move.worldX = center.x; move.worldY = center.y; move.speed = 0; move.forcePresentSync = true; const lockDur = FIRE_TRAP_DAMAGE_DELAY_SEC + FIRE_TRAP_KILL_DELAY_SEC + 0.05; session.buffs.add( victim, FIRE_TRAP_IMMOBILIZE_BUFF_ID, FIRE_TRAP_CASTER, 1, { duration: lockDur }, ); const buffs = world.getComponent(victim, BuffContainerComp); if (!buffs?.buffs.some((b) => b.buffId === FIRE_TRAP_IMMOBILIZE_BUFF_ID)) { return false; } const node = battle.getMonsterPresenter()?.getEntityNodeMap().get(victim); if (node?.isValid) { syncMonsterNode(session, map, victim, node); } move.presentLocked = true; return true; } /** 解除定身(幸存者飘血后恢复移动) */ private _unlockVictim(victim: EntityId): void { const battle = this._battle; if (!battle) return; const world = battle.session.world; if (!world.isAlive(victim)) return; const move = world.getComponent(victim, MovementComp); if (move) { move.presentLocked = false; move.forcePresentSync = false; } const buffs = world.getComponent(victim, BuffContainerComp); if (!buffs) return; buffs.buffs = buffs.buffs.filter((b) => b.buffId !== FIRE_TRAP_IMMOBILIZE_BUFF_ID); buffs.refreshDebuff4Flag(); } /** unbind 时解除仍被锁定的怪物 */ private _releaseAllVictims(): void { const battle = this._battle; if (!battle) return; const world = battle.session.world; world.forEachAlive((id) => { const move = world.getComponent(id, MovementComp); if (!move?.presentLocked) return; const buffs = world.getComponent(id, BuffContainerComp); if (!buffs?.buffs.some((b) => b.buffId === FIRE_TRAP_IMMOBILIZE_BUFF_ID)) return; this._unlockVictim(id); }); } /** 「持续3」播完后回收陷阱 */ private _scheduleTrapRelease(trap: FireTrapEntry): void { if (trap.releaseScheduled) return; trap.releaseScheduled = true; this._removeTrapEntry(trap); } /** 动画时长异常时的兜底回收 */ private _scheduleTrapReleaseFallback(trap: FireTrapEntry, skel: sp.Skeleton): void { const duration = BattleSpinePlayback.getAnimDuration(skel, FIRE_TRAP_ANIM_BURST); const delay = duration != null && duration > 0 ? duration : FIRE_TRAP_BURST_FALLBACK_SEC; const scheduleGen = this._triggerGen; schedulePresentation(delay, () => { if (scheduleGen !== this._triggerGen) return; this._scheduleTrapRelease(trap); }); } /** 飘血 + 可选延迟击杀(陷阱节点由「持续3」播完回收) */ private _applyTrapDamage(trap: FireTrapEntry, victim: EntityId, scheduleGen: number): void { const battle = this._battle; if (!battle || scheduleGen !== this._triggerGen) { return; } const session = battle.session; const world = session.world; if (!world.isAlive(victim)) { return; } const tf = world.getComponent(victim, TransformComp); if (!tf || tf.row !== trap.row || tf.col !== trap.col) { this._unlockVictim(victim); return; } const hp = world.getComponent(victim, HealthComp); if (!hp || hp.maxHp <= 0 || !(hp.hp > 0)) { this._unlockVictim(victim); return; } const maxDmg = Math.max(1, Math.floor(trap.maxDamage)); /** 当前血量不超过伤害上限 → 按最大血量结算(即死) */ const amount = hp.hp <= maxDmg ? hp.maxHp : maxDmg; const hpBefore = hp.hp; session.damage.apply(FIRE_TRAP_CASTER, victim, amount, FIRE_TRAP_DAMAGE_TYPE, undefined, false); session.events.flush(); session.events.emit( BattleEventFactory.hitAfter( FIRE_TRAP_CASTER, victim, amount, false, undefined, FIRE_TRAP_DAMAGE_TYPE, ), true, ); if (hp.hp <= 0) { schedulePresentation(FIRE_TRAP_KILL_DELAY_SEC, () => { if (scheduleGen !== this._triggerGen || !this._battle) return; const sess = this._battle.session; const w = sess.world; if (!w.isAlive(victim)) return; const hpNow = w.getComponent(victim, HealthComp); if (!hpNow || hpNow.hp > 0) return; sess.lifecycle.kill(victim, FIRE_TRAP_CASTER, hpBefore); this._unlockVictim(victim); }); return; } this._unlockVictim(victim); } /** 从列表移除并回收陷阱节点 */ private _removeTrapEntry(trap: FireTrapEntry): void { const idx = this._traps.indexOf(trap); if (idx >= 0) { this._traps.splice(idx, 1); } this._releaseTrapNode(trap.node, trap.skel); } }