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.
AdLike2048/assets/script/battle/presentation/PiercingGiantSwordAtkPresen...

266 lines
9.9 KiB

17 hours ago
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,
};
}
}