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

428 lines
16 KiB

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