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