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.
265 lines
9.9 KiB
265 lines
9.9 KiB
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,
|
|
};
|
|
}
|
|
}
|
|
|