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.
181 lines
5.7 KiB
181 lines
5.7 KiB
|
3 weeks ago
|
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);
|
||
|
|
}
|
||
|
|
}
|