import { Node, Vec3, sp } from 'cc'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { GBundle, GPath } from '../../game/ConfigRes'; import type { Skill } from '../skill/Skill'; import type { SkillPresentationContext } from '../skill/SkillPresentationTypes'; import { SkillPresentationEnv } from '../skill/SkillPresentationEnv'; import { InstantAtkPresenter } from './AtkTypePresenters'; import { BattleSpinePlayback } from './BattleSpinePlayback'; import type { ISkillPresenter } from './SkillPresenter'; /** battleHero/prefab/skill/宝塔 */ const PREFAB = '宝塔'; /** 预制体皮肤 */ const SKIN = '宝塔'; /** 砸地动画 */ const SMASH_ANIM = '宝塔砸地技能2'; /** 一次砸地特效 */ interface PagodaFx { /** 宝塔节点 */ node: Node; /** 回池剩余秒 */ wait: number; } let _host: PagodaSmashHost | null = null; /** 玲珑宝塔砸地特效运行时 */ export function getPagodaSmashHost(): PagodaSmashHost { if (!_host) _host = new PagodaSmashHost(); return _host; } /** * 玲珑宝塔(Skill.id=20509)落地特效:在主目标处播砸地,动画结束回池。 * 伤害与施法锁仍由 InstantAtkPresenter(atk_type=3)负责。 */ export class PagodaSmashHost { private readonly _fx: PagodaFx[] = []; /** * 在索敌目标处生成砸地。 * @param target 主目标 * @param skillId 仅用于 warn */ spawn(target: EntityId, skillId: number): void { if (!(target > 0)) { console.log('[PagodaSmash] 无索敌目标', skillId); return; } const world = SkillPresentationEnv.getTargetWorldPos(target); if (!world) { console.log('[PagodaSmash] 目标无世界坐标', target, skillId); return; } const node = this._spawnNode(world); if (!node) return; const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); if (!sk?.isValid) { console.log('[PagodaSmash] 缺少 Skeleton', PREFAB); this._recycle(node); return; } try { sk.setSkin(SKIN); } catch { console.log('[PagodaSmash] setSkin 失败', SKIN); this._recycle(node); return; } const wait = BattleSpinePlayback.resolveLogicalCompleteDelay(sk, SMASH_ANIM); if (wait == null) { console.log('[PagodaSmash] 动画不存在', SMASH_ANIM); this._recycle(node); return; } if (!BattleSpinePlayback.play(sk, SMASH_ANIM, { loop: false })) { this._recycle(node); return; } this._fx.push({ node, wait }); } /** * 推进回池。 * @param dt 逻辑秒 */ tick(dt: number): void { if (!(dt > 0) || this._fx.length <= 0) return; for (let i = this._fx.length - 1; i >= 0; i--) { const fx = this._fx[i]; if (!fx.node?.isValid) { this._fx.splice(i, 1); continue; } fx.wait -= dt; if (fx.wait <= 0) this._end(i); } } /** 取消全部砸地并回池 */ abortAll(): void { for (let i = this._fx.length - 1; i >= 0; i--) { this._end(i); } } /** * 生成宝塔并放到目标世界坐标。 * @param world 目标世界坐标 */ private _spawnNode(world: Vec3): Node | null { const parent = SkillPresentationEnv.flyLayer ?? SkillPresentationEnv.getMap()?.ensureHeroParent(); if (!parent?.isValid) { console.log('[PagodaSmash] 无父节点'); return null; } let node: Node | null = null; try { node = gg.res.getNode(GPath.HeroSkillPrefab(), PREFAB, GBundle.BattleHero); } catch (e) { console.log('[PagodaSmash] 加载异常', String(e)); return null; } if (!node?.isValid) { console.log('[PagodaSmash] 预制体未加载', PREFAB); return null; } if (!parent.active) parent.active = true; parent.addChild(node); node.active = true; node.layer = parent.layer; node.setWorldPosition(world.x, world.y, world.z); return node; } /** * 结束并回池。 * @param index 下标 */ private _end(index: number): void { const fx = this._fx[index]; this._fx.splice(index, 1); this._recycle(fx.node); } /** * 回池宝塔。 * @param node 节点 */ private _recycle(node: Node | null): void { if (!node?.isValid) return; const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); BattleSpinePlayback.clearPendingComplete(sk); BattleSpinePlayback.resetTracks(sk); gg.res.putNode(node); const flyLayer = SkillPresentationEnv.flyLayer; if (flyLayer?.isValid && flyLayer.children.length === 0) flyLayer.active = false; } } /** Skill.id=20509 玲珑宝塔:目标砸地 + 武器通道瞬时施法 */ export class PagodaSmashPresenter implements ISkillPresenter { /** atk_type=3 命中与施法锁 */ private readonly _instant = new InstantAtkPresenter(); /** * 在主目标处砸宝塔,并走范围瞬时表现。 * @param ctx 表现上下文 * @param host Skill 宿主 */ present(ctx: SkillPresentationContext, host: Skill): void { if (!ctx.isAlive) { ctx.finish(); return; } getPagodaSmashHost().spawn(ctx.targets[0] ?? 0, Number(ctx.params.skillId ?? 0) || 0); this._instant.present(ctx, host); } }