import { _decorator, Node, sp } from 'cc'; import { GBundle } from '../../game/ConfigRes'; import type { SkillPresentationContext } from '../skill/SkillPresentationTypes'; import { PRES_HIT_AT_BASE } from '../presentation/SkillVisualChannel'; import { HeroSkin } from '../view/HeroSkin'; import { BattleSpinePlayback } from '../presentation/BattleSpinePlayback'; import { HeroActor } from './HeroActor'; const { ccclass } = _decorator; /** battleHero/prefab/skill/英雄棍-攻击 */ const ATK_PREFAB = '英雄棍-攻击'; const ATK_PREFAB_PATH = 'prefab/skill/'; /** 攻效节点名 */ const NODE_ANIM = 'anim'; /** 攻效 Spine 动画 */ const SPINE_ATK_FX = '攻击2'; /** * 棍:HeroActor 壳;普攻/技能表现共用「英雄棍-攻击」(播攻击2),字形播「攻击」。 */ @ccclass('Gun') export class Gun extends HeroActor { private _atkRoot: Node | null = null; private _atkAnim: sp.Skeleton | null = null; /** 攻效「攻击2」播放中;命中结算会 onIdle,勿此时藏攻效 */ private _fxPlaying = false; /** 当前攻效对应的播控序号;>0 且 multi=true 表示多段收招判定 */ private _atkFxSeq = 0; /** 本次攻效是否多段(atk_type=16) */ private _atkFxMultiHit = false; /** 本次攻效是否环形刀光(200005):龙影由公用 type=10 host 发射,收招等攻效播完 */ private _atkFxRing = false; detachPooledFx(): void { this._disposeAtk(); super.detachPooledFx(); } onIdle(): void { if (this._fxPlaying) return; this._hideAtk(); super.onIdle(); } abortPresentation(): void { this._fxPlaying = false; this._atkFxRing = false; this._hideAtk(); super.abortPresentation(); } /** skillId Presenter:普攻与技能同一套攻效 */ presentNormAttack(ctx: SkillPresentationContext): void { this._presentGunFx(ctx); } /** atk_type=16 扇形多段:同一套攻效,命中按段间隔逐段结算 */ presentFanMultiHit(ctx: SkillPresentationContext): void { this._presentGunFx(ctx, true); } /** 200005 环形刀光攻效:龙影由公用 skill type=10 host 发射,这里只播攻效 */ presentRingAttack(ctx: SkillPresentationContext): void { this._presentGunFx(ctx, false, true); } /** 主动技能与普攻共用预制体(表暂无 act_skill,预留入口) */ presentSkill(ctx: SkillPresentationContext): void { this._presentGunFx(ctx); } private _presentGunFx(ctx: SkillPresentationContext, multiHit = false, ring = false): void { if (!ctx.isAlive || !this.isValid || !this.node?.isValid) { ctx.finish(); return; } this._bindChannel(); this._ensureAtk(); this.syncAtkFxSkin(this._atkAnim); this._showAtk(); const seq = this.beginPresentationCast(ctx); this._atkFxSeq = seq; this._atkFxMultiHit = multiHit; this._atkFxRing = ring; if (ring) { // 字形「攻击」动画先于攻效播完:挂起收招,等攻效播完再收招 this.holdPresentationUntilFxComplete(); } const aspd = this.visualChannel.playCastAnim({ caster: ctx.caster, animation: String(ctx.params.animation ?? ''), followWeapon: false, primaryTarget: ctx.targets[0] ?? null, onAnimComplete: this.onPresentationAnimCompleteBound, }); if (this._atkAnim?.isValid) { this._fxPlaying = true; this.playSpine(this._atkAnim, SPINE_ATK_FX, this._onAtkFxCompleteBound, aspd, false); } else { this._fxPlaying = false; console.log('[Gun] 攻效预制体未加载', ATK_PREFAB); if (ring) this.markStaffSmashAnimsComplete(seq); } if (ring) { // 龙影由公用 skill type=10 host 发射:不安排常规收招 hit,攻效播完收招 return; } const hitDelay = PRES_HIT_AT_BASE / Math.max(0.01, aspd); if (multiHit) this.schedulePresentationMultiHit(seq, ctx, hitDelay); else this.schedulePresentationCast(seq, hitDelay); } private readonly _onAtkFxCompleteBound = (): void => { this._fxPlaying = false; this._hideAtk(); if (!this.isValid) return; const seq = this._atkFxSeq; if (this._atkFxRing && seq > 0 && this.isPresentationCast(seq)) { // 环形刀光:龙影由公用 host 发射,攻效播完即收招 this.markStaffSmashAnimsComplete(seq); return; } if (this._atkFxMultiHit && seq > 0 && this.isPresentationCast(seq)) { // 多段:攻效播完即解锁收招,不等字形攻击动画(可能已被待机打断) this.markStaffSmashAnimsComplete(seq); return; } super.onIdle(); }; private _ensureAtk(): void { if (!this._atkRoot?.isValid) { const atk = gg.res.getNode(ATK_PREFAB_PATH, ATK_PREFAB, GBundle.BattleHero); if (!atk?.isValid) { this._atkRoot = null; this._atkAnim = null; return; } this._atkRoot = atk; } this._attachAtk(); const animNode = this._atkRoot.getChildByName(NODE_ANIM); this._atkAnim = animNode?.getComponent(sp.Skeleton) ?? null; if (!this._atkAnim?.isValid) { console.log('[Gun] 攻效缺少节点', NODE_ANIM); } } /** 挂在英雄展示根上,与「动画」同级 */ private _attachAtk(): void { const atk = this._atkRoot; if (!atk?.isValid) return; if (!this._heroSkin) { this._heroSkin = this.node.getComponent(HeroSkin); } const display = this._heroSkin?.displayNode; const parent = display?.isValid ? display : this.node; if (atk.parent !== parent) { atk.parent = parent; } atk.setPosition(0, 0, 0); } private _showAtk(): void { if (this._atkRoot?.isValid) this._atkRoot.active = true; } private _hideAtk(): void { if (this._atkAnim?.isValid) { BattleSpinePlayback.clearPendingComplete(this._atkAnim); this._atkAnim.timeScale = 1; } if (this._atkRoot?.isValid) this._atkRoot.active = false; } private _disposeAtk(): void { this._fxPlaying = false; this._hideAtk(); if (this._atkRoot?.isValid) { gg.res.putNode(this._atkRoot); } this._atkRoot = null; this._atkAnim = null; } }