import { _decorator, instantiate, Node, sp, UITransform, Vec3 } from 'cc'; import { GBundle } from '../../game/ConfigRes'; import type { SkillPresentationContext } from '../skill/SkillPresentationTypes'; import { SkillPresentationEnv } from '../skill/SkillPresentationEnv'; import { PRES_HIT_AT_BASE } from '../presentation/SkillVisualChannel'; import { SkillTransformDriver } from '../skill/SkillTransformDriver'; import { getBowAtkHost } from '../combat/BowAtkRuntime'; 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/'; /** 龙影攻击预制体(棍 200005 环形刀光,保存名「棍」) */ const RING_PREFAB = '棍'; /** 攻效节点名 */ const NODE_ANIM = 'anim'; /** 攻效 Spine 动画 */ const SPINE_ATK_FX = '攻击2'; /** 龙影链式发射间距(像素):上一支飞出该距离后才发下一支 */ const RING_CHAIN_GAP_PX = 100; /** 龙影起始发射延迟(秒):攻效开播后延迟发起,配合棍的攻击动作 */ const RING_START_DELAY_SEC = 0.5; /** 龙影起始角(度):从水平向右起逆时针;atk_para 不含角度参数,取代码约定值 */ const RING_START_DEG = 30; /** 龙影邻支角度间隔(度):数量 = 360/间隔 均分整圆 */ const RING_STEP_DEG = 20; /** 龙影飞行像素/秒 */ const RING_FLY_SPEED = 500; /** 龙影飞行距离(像素):足够飞出屏幕 */ const RING_FLY_DIST = 1200; /** 龙影出生外扩(像素):从字的攻击边缘发出 */ const RING_SPAWN_OFFSET = 200; /** 龙影数量上限(角度间隔最小 18°) */ const RING_MAX_COUNT = 18; /** 龙影 Spine 默认动画(预制体未勾 playOnLoad,需显式播放) */ const RING_ANIM = '剑气红'; /** 龙影首中被消耗的保底可视飞行时长(秒):贴脸命中不凭空消失 */ const RING_CONSUME_MIN_FLY_SEC = 0.08; /** * 棍: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):发射与攻效同播,收招等攻效播完 */ private _atkFxRing = false; /** 200005 环形刀光:待发射的展示上下文(攻效播完置入) */ private _ringCtx: SkillPresentationContext | null = null; /** 龙影模板(仅用于 instantiate),来自 battleHero/prefab/skill/棍 */ private _ringTemplate: Node | null = null; /** 在飞龙影克隆 */ private readonly _ringClones: Node[] = []; detachPooledFx(): void { this._disposeAtk(); this._disposeRing(); super.detachPooledFx(); } onIdle(): void { if (this._fxPlaying) return; this._hideAtk(); super.onIdle(); } abortPresentation(): void { this._fxPlaying = false; this._atkFxRing = false; this._ringCtx = null; 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 环形刀光:播完攻效后从字的位置环形发出龙影 */ 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; this._ringCtx = ring ? ctx : null; 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); // 跟攻击动作一起播:攻效开播后延迟 RING_START_DELAY_SEC 发起 if (ring) this.schedulePresentation(() => this._startRing(), RING_START_DELAY_SEC); } else { this._fxPlaying = false; console.log('[Gun] 攻效预制体未加载', ATK_PREFAB); if (ring) { this.markStaffSmashAnimsComplete(seq); this._startRing(); } } if (ring) { // 伤害全在龙影弹道结算:不安排常规收招 hit,攻效播完由 _startRing 发射并收招 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)) { // 环形刀光:龙影已随攻效发射,攻效播完即收招 this.markStaffSmashAnimsComplete(seq); return; } if (this._atkFxMultiHit && seq > 0 && this.isPresentationCast(seq)) { // 多段:攻效播完即解锁收招,不等字形攻击动画(可能已被待机打断) this.markStaffSmashAnimsComplete(seq); return; } super.onIdle(); }; /** * 200005 环形刀光:起始角/数量用代码约定(RING_START_DEG/RING_STEP_DEG), * 伤害系数取 atk_para 第 3 段(与 14 多段语义的第 3 段一致)。 */ private _startRing(): void { const ctx = this._ringCtx; this._ringCtx = null; if (!ctx?.isAlive || !this.isValid || !this.node?.isValid) return; const parts = String(ctx.params.atkPara ?? '').split(',').map(Number); const wan = parts[2]; if (!(wan > 0)) { console.log('[Gun] 200005 atk_para 伤害系数非法', ctx.params.atkPara); return; } let count = Math.round(360 / RING_STEP_DEG); if (count > RING_MAX_COUNT) count = RING_MAX_COUNT; if (count < 1) count = 1; const step = 360 / count; const origin = this.node.worldPosition.clone(); const template = this._ensureRingTemplate(); if (!template?.isValid) { console.log('[Gun] 龙影预制体未加载', RING_PREFAB); return; } for (let i = 0; i < count; i++) { const deg = RING_START_DEG + i * step; // 链式发射:上一支飞出 RING_CHAIN_GAP_PX 后才发下一支 const delaySec = (i * RING_CHAIN_GAP_PX) / RING_FLY_SPEED; if (i === 0) this._spawnRingArrow(ctx, origin, deg, wan); else this.schedulePresentation(() => this._spawnRingArrow(ctx, origin, deg, wan), delaySec); } } /** 发一支龙影:直线飞出,首中即结算消耗(无怪飞满距离消失) */ private _spawnRingArrow(ctx: SkillPresentationContext, origin: Vec3, deg: number, wan: number): void { if (!ctx.isAlive || !this.isValid || !this.node?.isValid) return; const rad = deg * Math.PI / 180; const ux = Math.cos(rad); const uy = Math.sin(rad); const from = new Vec3(origin.x + ux * RING_SPAWN_OFFSET, origin.y + uy * RING_SPAWN_OFFSET, origin.z); const to = new Vec3(origin.x + ux * (RING_SPAWN_OFFSET + RING_FLY_DIST), origin.y + uy * (RING_SPAWN_OFFSET + RING_FLY_DIST), origin.z); const ut = this._ringTemplate?.isValid ? this._ringTemplate.getComponent(UITransform) : null; if (!ut) { console.log('[Gun] 龙影预制体缺少 UITransform,跳过', RING_PREFAB); return; } const ws = this._ringTemplate!.worldScale; const duration = RING_FLY_DIST / RING_FLY_SPEED; const flyLayer = SkillPresentationEnv.flyLayer; let visual: Node | null = null; if (flyLayer?.isValid) { visual = this._cloneRingArrow(); if (!visual) console.log('[Gun] 龙影克隆失败,仅逻辑结算'); } const spawnAtMs = Date.now(); if (visual) { const wsClone = visual.worldScale.clone(); const flyUt = flyLayer.getComponent(UITransform); flyLayer.active = true; visual.parent = flyLayer; visual.setWorldScale(wsClone); visual.angle = deg; visual.setPosition(flyUt ? flyUt.convertToNodeSpaceAR(from) : from.clone()); const toLocal = flyUt ? flyUt.convertToNodeSpaceAR(to) : to.clone(); SkillTransformDriver.animate({ node: visual, duration, fromPos: visual.position.clone(), toPos: toLocal, easing: SkillTransformDriver.easeLinear, onComplete: () => this._recycleRingClone(visual as Node), }); } getBowAtkHost().spawnRay({ caster: ctx.caster, abilityId: ctx.abilityId, x: from.x, y: from.y, ux, uy, speed: RING_FLY_SPEED, left: duration, scale: wan / 10000, halfLen: ut.width * Math.abs(ws.x) * 0.5, halfWid: ut.height * Math.abs(ws.y) * 0.5, buff1: String(ctx.params.buff1 ?? ''), buff2: String(ctx.params.buff2 ?? ''), hitIds: [], pierce: false, onConsume: visual ? () => this._consumeRingClone(visual as Node, spawnAtMs) : undefined, }); } /** * 龙影首中被消耗:贴脸命中时还没飞出来就回收会像没发射, * 保底再飞 RING_CONSUME_MIN_FLY_SEC 再回收;已飞够时长则当场回收。 */ private _consumeRingClone(visual: Node, spawnAtMs: number): void { const elapsedSec = (Date.now() - spawnAtMs) / 1000; const waitSec = RING_CONSUME_MIN_FLY_SEC - elapsedSec; if (waitSec <= 0) { this._recycleRingClone(visual); return; } this.schedulePresentation(() => this._recycleRingClone(visual), waitSec); } /** 取龙影模板(懒加载自 battleHero/prefab/skill/棍)并克隆一支;克隆需显式播 Spine(未勾 playOnLoad) */ private _cloneRingArrow(): Node | null { const tpl = this._ensureRingTemplate(); if (!tpl?.isValid) return null; const clone = instantiate(tpl); clone.active = true; const skel = clone.getComponentInChildren(sp.Skeleton); if (skel?.isValid) { skel.timeScale = 1; skel.setAnimation(0, String(skel.defaultAnimation || RING_ANIM), true); } else { console.log('[Gun] 龙影缺少 Spine 动画节点', RING_PREFAB); } this._ringClones.push(clone); return clone; } private _ensureRingTemplate(): Node | null { if (!this._ringTemplate?.isValid) { const tpl = gg.res.getNode(ATK_PREFAB_PATH, RING_PREFAB, GBundle.BattleHero); if (!tpl?.isValid) return null; tpl.active = false; this._ringTemplate = tpl; } return this._ringTemplate; } /** 回收龙影(首中消耗或飞满距离) */ private _recycleRingClone(visual: Node): void { if (visual?.isValid) { SkillTransformDriver.cancelNode(visual); visual.destroy(); } const idx = this._ringClones.indexOf(visual); if (idx >= 0) this._ringClones.splice(idx, 1); } /** 回池前清龙影:销毁克隆、归还模板 */ private _disposeRing(): void { this._ringCtx = null; for (let i = 0; i < this._ringClones.length; i++) { const c = this._ringClones[i]; if (c?.isValid) { SkillTransformDriver.cancelNode(c); c.destroy(); } } this._ringClones.length = 0; if (this._ringTemplate?.isValid) gg.res.putNode(this._ringTemplate); this._ringTemplate = null; } 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; } }