import { Node, UITransform, Vec3, sp } from 'cc'; import { AttrId } from 'db://assets/mx/module/battle/ecs/Components'; import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch'; import { GBundle, GPath } from '../../game/ConfigRes'; import { BattleAudio } from '../audio/BattleAudio'; import { BOW_ATK_ARROW_SPEED, SCATTER_RANGE_MUL, getBowAtkHost } from '../combat/BowAtkRuntime'; import type { Skill } from '../skill/Skill'; import { SkillPresentationEnv } from '../skill/SkillPresentationEnv'; import { SkillTransformDriver, computeAimAngleDeg } from '../skill/SkillTransformDriver'; import type { SkillPresentationContext } from '../skill/SkillPresentationTypes'; import { BattleSpinePlayback } from './BattleSpinePlayback'; import type { ISkillPresenter } from './SkillPresenter'; /** 穿云击 Skill.id(Battlebuff 4062) */ export const PIERCING_GIANT_SWORD_SKILL_ID = 406201; /** battleHero/prefab/skill/穿透巨剑 */ const SWORD_PREFAB = '穿透巨剑'; /** Spine 动画 */ const SWORD_ANIM = 'animation'; /** 体盒半长兜底(预制体约 513) */ const HALF_LEN_FALLBACK = 256; /** 体盒半宽兜底(预制体约 161) */ const HALF_WID_FALLBACK = 80; /** * 是否穿云击 Skill.id。 * @param skillId Skill.id */ export function isPiercingGiantSwordSkillId(skillId: number): boolean { return Math.floor(Number(skillId) || 0) === PIERCING_GIANT_SWORD_SKILL_ID; } let _host: PiercingGiantSwordHost | null = null; /** 穿透巨剑表现宿主 */ export function getPiercingGiantSwordHost(): PiercingGiantSwordHost { if (!_host) _host = new PiercingGiantSwordHost(); return _host; } /** * Skill 406201:发射「穿透巨剑」直线穿透(配表 stepDeg=0 时通用散射会失败)。 */ export class PiercingGiantSwordAtkPresenter implements ISkillPresenter { present(ctx: SkillPresentationContext, host: Skill): void { if (!ctx.isAlive) { ctx.finish(); return; } const seq = host.beginPresentationCast(ctx); if (!(seq > 0)) return; host.disarmAnimComplete(); getPiercingGiantSwordHost().launch(ctx); ctx.releasePresentationLock(); host.finishPresentationCast(seq, false); } } /** 穿透巨剑:取预制体飞全程,伤害由 BowAtkHost 穿透结算 */ export class PiercingGiantSwordHost { private readonly _nodes: Node[] = []; private readonly _from = new Vec3(); private readonly _to = new Vec3(); private readonly _degs: number[] = []; /** * 按 atk_para 向主目标方向发射穿透巨剑。 * @param ctx 表现上下文 */ launch(ctx: SkillPresentationContext): void { const parts = String(ctx.params.atkPara ?? '').split(',').map(Number); const count = Math.floor(parts[0] || 0); const stepDeg = Number(parts[1]); const wan = Number(parts[2]); const target = ctx.targets[0] ?? 0; const rangePx = SkillPresentationEnv.getCasterAttr(ctx.caster, AttrId.Range); if (!(count > 0) || !Number.isFinite(stepDeg) || !(stepDeg >= 0) || !(wan > 0) || !(rangePx > 0) || !(target > 0)) { console.log('[PiercingGiantSword] atk_para/射程无效', ctx.params.skillId, ctx.params.atkPara, rangePx, target); return; } const from = SkillPresentationEnv.getCompositionWorldCenter(ctx.caster) ?? SkillPresentationEnv.getCasterGridCenter(ctx.caster); const to = SkillPresentationEnv.getTargetWorldPos(target); if (!from || !to) { console.log('[PiercingGiantSword] 无起终点', ctx.params.skillId, target); return; } this._from.set(from.x, from.y, from.z); const aimDeg = computeAimAngleDeg(this._from, to); if (aimDeg == null) { console.log('[PiercingGiantSword] 朝向为零', ctx.params.skillId); return; } this._fillAimDegs(aimDeg, count, stepDeg); if (this._degs.length <= 0) { console.log('[PiercingGiantSword] 无发射角', ctx.params.skillId); return; } const parent = SkillPresentationEnv.flyLayer; if (!parent?.isValid) { console.log('[PiercingGiantSword] 无 flyLayer'); return; } const travel = rangePx * SCATTER_RANGE_MUL; if (!(travel > 0)) { console.log('[PiercingGiantSword] 飞行距离无效', rangePx); return; } const duration = Math.max(0.12, travel / BOW_ATK_ARROW_SPEED); const scale = wan / 10000; const buff1 = String(ctx.params.buff1 ?? ''); const buff2 = String(ctx.params.buff2 ?? ''); const bow = getBowAtkHost(); parent.active = true; const flyUt = parent.getComponent(UITransform); let launched = 0; for (let i = 0; i < this._degs.length; i++) { const deg = this._degs[i]; const rad = (deg * Math.PI) / 180; const ux = Math.cos(rad); const uy = Math.sin(rad); this._to.set(this._from.x + ux * travel, this._from.y + uy * travel, this._from.z); const node = this._takeSword(); if (!node) continue; const box = this._resolveHalfBox(node); node.active = true; node.layer = parent.layer; node.setScale(1, 1, 1); node.parent = parent; const startLocal = flyUt ? flyUt.convertToNodeSpaceAR(this._from) : this._from.clone(); const endLocal = flyUt ? flyUt.convertToNodeSpaceAR(this._to) : this._to.clone(); node.setPosition(startLocal); node.angle = deg; node.setSiblingIndex(parent.children.length - 1); this._playAnim(node); this._nodes.push(node); SkillTransformDriver.cancelNode(node); SkillTransformDriver.animate({ node, duration, fromPos: startLocal, toPos: endLocal, fromAngle: deg, toAngle: deg, easing: SkillTransformDriver.easeLinear, onComplete: this._onSwordArrived, }); bow.spawnRay({ caster: ctx.caster, abilityId: ctx.abilityId, x: this._from.x, y: this._from.y, ux, uy, speed: BOW_ATK_ARROW_SPEED, left: duration, scale, halfLen: box.halfLen, halfWid: box.halfWid, buff1, buff2, hitIds: [], pierce: true, }); launched++; } if (launched > 0) { BattleAudio.playWeaponAttack(ctx.caster, 40); } else { console.log('[PiercingGiantSword] 未发射', ctx.params.skillId); } } /** 战斗结束清空 */ abortAll(): void { for (let i = this._nodes.length - 1; i >= 0; i--) { this._recycle(this._nodes[i]); } this._nodes.length = 0; } /** * 填充发射角:stepDeg≤0 时全部同向(穿云击 1,0)。 * @param aimDeg 瞄准角 * @param count 数量 * @param stepDeg 邻箭夹角 */ private _fillAimDegs(aimDeg: number, count: number, stepDeg: number): void { this._degs.length = 0; const n = Math.floor(count); if (!(n > 0)) return; if (!(stepDeg > 0)) { for (let i = 0; i < n; i++) this._degs.push(aimDeg); return; } getBowAtkHost().fillScatterDegs(aimDeg, n, stepDeg, this._degs); } private readonly _onSwordArrived = (node?: Node): void => { if (node) this._recycle(node); }; /** * @param node 巨剑节点 */ private _recycle(node: Node): void { for (let i = this._nodes.length - 1; i >= 0; i--) { if (this._nodes[i] === node) this._nodes.splice(i, 1); } if (!node?.isValid) return; SkillTransformDriver.cancelNode(node); const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); BattleSpinePlayback.clearPendingComplete(sk); BattleSpinePlayback.resetTracks(sk); node.angle = 0; node.setScale(1, 1, 1); gg.res.putNode(node); } private _takeSword(): Node | null { let node: Node | null = null; try { node = gg.res.getNode(GPath.HeroSkillPrefab(), SWORD_PREFAB, GBundle.BattleHero); } catch (err) { console.log('[PiercingGiantSword] getNode 失败', String(err)); return null; } if (!node?.isValid) { console.log('[PiercingGiantSword] 预制体未加载', SWORD_PREFAB); return null; } return node; } /** * @param node 巨剑根 */ private _playAnim(node: Node): void { const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); if (!sk?.isValid) { console.log('[PiercingGiantSword] 缺 Skeleton'); return; } if (sk.skeletonData) ensureSpineSkeletonDataRuntime(sk.skeletonData); const typed = sk as sp.Skeleton & { defaultAnimation?: string }; const name = String(typed.defaultAnimation || SWORD_ANIM).trim() || SWORD_ANIM; BattleSpinePlayback.play(sk, name, { loop: true }); } /** * @param node 巨剑根 */ private _resolveHalfBox(node: Node): { halfLen: number; halfWid: number } { const ut = node.getComponent(UITransform) ?? node.getChildByName(SWORD_PREFAB)?.getComponent(UITransform) ?? node.getComponentInChildren(UITransform); const w = ut?.width ?? 0; const h = ut?.height ?? 0; return { halfLen: w > 0 ? w * 0.5 : HALF_LEN_FALLBACK, halfWid: h > 0 ? h * 0.5 : HALF_WID_FALLBACK, }; } }