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.
539 lines
18 KiB
539 lines
18 KiB
|
1 week ago
|
import { Node, Vec3, sp } from 'cc';
|
||
|
|
import type { BattleSession, Cell, EntityId } from 'db://assets/mx/module/battle';
|
||
|
|
import { BattleTeamId } from 'db://assets/mx/module/battle';
|
||
|
|
import {
|
||
|
|
HealthComp,
|
||
|
|
MovementComp,
|
||
|
|
TeamComp,
|
||
|
|
TransformComp,
|
||
|
|
isBajiaoBlowRetreating,
|
||
|
|
isYanSoulClaimed,
|
||
|
|
} from 'db://assets/mx/module/battle/ecs/Components';
|
||
|
|
import { GBundle, GPath } from '../../game/ConfigRes';
|
||
|
|
import { BattleMapSpec } from '../map/BattleMapSpec';
|
||
|
|
import type { MapManager } from '../map/legacy/MapManager';
|
||
|
|
import type { Skill } from '../skill/Skill';
|
||
|
|
import type { SkillPresentationContext } from '../skill/SkillPresentationTypes';
|
||
|
|
import { SkillPresentationEnv } from '../skill/SkillPresentationEnv';
|
||
|
|
import {
|
||
|
|
computeAimAngleDeg,
|
||
|
|
setNodeWorldPos,
|
||
|
|
shortestAngleDeltaDeg,
|
||
|
|
} from '../skill/SkillTransformDriver';
|
||
|
|
import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch';
|
||
|
|
import { BattleSpinePlayback } from './BattleSpinePlayback';
|
||
|
|
import { buildShadowDealContext } from './SkillShadowContext';
|
||
|
|
import type { ISkillPresenter } from './SkillPresenter';
|
||
|
|
|
||
|
|
/** battleHero/prefab/skill/冲锋 */
|
||
|
|
const CHARGE_PREFAB = '冲锋';
|
||
|
|
/** 朝向转速(度/秒) */
|
||
|
|
const TURN_DEG = 540;
|
||
|
|
/** 沿路径前瞻(像素) */
|
||
|
|
const LOOKAHEAD = 28;
|
||
|
|
/** 沿途命中半径(像素,对齐预制体半边) */
|
||
|
|
const HIT_RADIUS = 100;
|
||
|
|
/** 冲锋直伤类型 */
|
||
|
|
const DMG_TYPE = 'earth_charge';
|
||
|
|
|
||
|
|
/** Skill.skill type=1 土冲锋参数 */
|
||
|
|
interface EarthChargeParams {
|
||
|
|
/** 冲锋速度(像素/秒) */
|
||
|
|
speed: number;
|
||
|
|
/** 持续秒 */
|
||
|
|
duration: number;
|
||
|
|
/** 伤害系数 */
|
||
|
|
scale: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 一次土冲锋运行态 */
|
||
|
|
interface EarthChargeRun {
|
||
|
|
/** 施法者 */
|
||
|
|
caster: EntityId;
|
||
|
|
/** 影子伤害上下文 */
|
||
|
|
ctx: SkillPresentationContext;
|
||
|
|
/** 冲锋根 */
|
||
|
|
node: Node;
|
||
|
|
/** 路径世界坐标 */
|
||
|
|
waypoints: Vec3[];
|
||
|
|
/** 像素/秒 */
|
||
|
|
speed: number;
|
||
|
|
/** 持续秒 */
|
||
|
|
duration: number;
|
||
|
|
/** 伤害系数 */
|
||
|
|
scale: number;
|
||
|
|
/** 已过秒 */
|
||
|
|
elapsed: number;
|
||
|
|
/** 节点朝向角 */
|
||
|
|
angle: number;
|
||
|
|
/** 已命中 */
|
||
|
|
hitIds: EntityId[];
|
||
|
|
/** 上一帧世界 X */
|
||
|
|
lastX: number;
|
||
|
|
/** 上一帧世界 Y */
|
||
|
|
lastY: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
let _host: EarthChargeHost | null = null;
|
||
|
|
|
||
|
|
/** 土冲锋运行时 */
|
||
|
|
export function getEarthChargeHost(): EarthChargeHost {
|
||
|
|
if (!_host) _host = new EarthChargeHost();
|
||
|
|
return _host;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 土属性冲锋:自枪格吸附进路格生成「冲锋」预制体,沿哪吒同款进路反冲,沿途伤+击退;飞出后独立。
|
||
|
|
*/
|
||
|
|
export class EarthChargeHost {
|
||
|
|
private readonly _runs: EarthChargeRun[] = [];
|
||
|
|
private readonly _tmp = new Vec3();
|
||
|
|
private readonly _aim = new Vec3();
|
||
|
|
private readonly _from = new Vec3();
|
||
|
|
private _hitRun: EarthChargeRun | null = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 解析 `1,速度,时长,伤害万分比`。
|
||
|
|
* @param raw Skill.skill
|
||
|
|
*/
|
||
|
|
static parse(raw: string): EarthChargeParams | null {
|
||
|
|
const parts = String(raw ?? '').split(',').map((s) => Number(String(s).trim()));
|
||
|
|
if (parts[0] !== 1) return null;
|
||
|
|
const speed = parts[1];
|
||
|
|
const duration = parts[2];
|
||
|
|
const wan = parts[3];
|
||
|
|
if (!(speed > 0) || !(duration > 0) || !(wan > 0)) {
|
||
|
|
console.log('[EarthCharge] skill 参数无效');
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return { speed, duration, scale: wan / 10000 };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 开战:自施法者格吸附进路生成冲锋体并立刻放锁。
|
||
|
|
* @param ctx 表现上下文
|
||
|
|
* @param host Skill 宿主
|
||
|
|
*/
|
||
|
|
start(ctx: SkillPresentationContext, host: Skill): void {
|
||
|
|
this._abortCaster(ctx.caster);
|
||
|
|
const raw = String(ctx.params.extraSkill ?? ctx.params.skillRaw ?? '');
|
||
|
|
const params = EarthChargeHost.parse(raw);
|
||
|
|
if (!params) {
|
||
|
|
host.drainLogicPresentation(ctx);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const session = SkillPresentationEnv.getBattleSession();
|
||
|
|
const map = SkillPresentationEnv.getMap();
|
||
|
|
if (!session || !map) {
|
||
|
|
console.log('[EarthCharge] 无 session/map');
|
||
|
|
host.drainLogicPresentation(ctx);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const target = ctx.targets[0] ?? 0;
|
||
|
|
if (!(target > 0) || !session.world.isAlive(target)) {
|
||
|
|
console.log('[EarthCharge] 无附近怪物', target);
|
||
|
|
host.drainLogicPresentation(ctx);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const fromCell = this._casterCell(session, map, ctx.caster);
|
||
|
|
if (!fromCell) {
|
||
|
|
console.log('[EarthCharge] 无施法者格', ctx.caster);
|
||
|
|
host.drainLogicPresentation(ctx);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const snap = this._snapApproachCell(session, ctx.caster, fromCell);
|
||
|
|
if (!snap) {
|
||
|
|
console.log('[EarthCharge] 进路上无吸附格', fromCell);
|
||
|
|
host.drainLogicPresentation(ctx);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const waypoints = this._buildRetreatWorlds(session, ctx.caster, snap, params);
|
||
|
|
if (waypoints.length < 1) {
|
||
|
|
console.log('[EarthCharge] 反向路径为空');
|
||
|
|
host.drainLogicPresentation(ctx);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const parent = SkillPresentationEnv.flyLayer ?? map.ensureHeroParent();
|
||
|
|
if (!parent?.isValid) {
|
||
|
|
console.log('[EarthCharge] 无父节点');
|
||
|
|
host.drainLogicPresentation(ctx);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let node: Node | null = null;
|
||
|
|
try {
|
||
|
|
node = gg.res.getNode(GPath.HeroSkillPrefab(), CHARGE_PREFAB, GBundle.BattleHero);
|
||
|
|
} catch (err) {
|
||
|
|
console.log('[EarthCharge] getNode 失败', String(err));
|
||
|
|
host.drainLogicPresentation(ctx);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!node?.isValid) {
|
||
|
|
console.log('[EarthCharge] 预制体未加载', CHARGE_PREFAB);
|
||
|
|
host.drainLogicPresentation(ctx);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const seq = host.beginPresentationCast(ctx);
|
||
|
|
if (!(seq > 0)) {
|
||
|
|
gg.res.putNode(node);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
host.disarmAnimComplete();
|
||
|
|
parent.active = true;
|
||
|
|
node.active = true;
|
||
|
|
node.layer = parent.layer;
|
||
|
|
node.parent = parent;
|
||
|
|
node.setSiblingIndex(parent.children.length - 1);
|
||
|
|
node.setScale(1, 1, 1);
|
||
|
|
const start = waypoints[0];
|
||
|
|
setNodeWorldPos(node, start);
|
||
|
|
const angle = this._chargeAimDeg(start.x, start.y, waypoints, 0) ?? 0;
|
||
|
|
node.angle = angle;
|
||
|
|
this._playPrefabAnim(node);
|
||
|
|
|
||
|
|
const shadow = buildShadowDealContext(
|
||
|
|
ctx,
|
||
|
|
String(ctx.params.buff1 ?? ''),
|
||
|
|
String(ctx.params.buff2 ?? ''),
|
||
|
|
);
|
||
|
|
this._runs.push({
|
||
|
|
caster: ctx.caster,
|
||
|
|
ctx: shadow,
|
||
|
|
node,
|
||
|
|
waypoints,
|
||
|
|
speed: params.speed,
|
||
|
|
duration: params.duration,
|
||
|
|
scale: params.scale,
|
||
|
|
elapsed: 0,
|
||
|
|
angle,
|
||
|
|
hitIds: [],
|
||
|
|
lastX: start.x,
|
||
|
|
lastY: start.y,
|
||
|
|
});
|
||
|
|
ctx.releasePresentationLock();
|
||
|
|
host.finishPresentationCast(seq, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 推进冲锋。
|
||
|
|
* @param dt 逻辑秒
|
||
|
|
*/
|
||
|
|
tick(dt: number): void {
|
||
|
|
if (!(dt > 0)) return;
|
||
|
|
for (let i = this._runs.length - 1; i >= 0; i--) {
|
||
|
|
if (!this._step(this._runs[i], dt)) {
|
||
|
|
this._finish(this._runs[i]);
|
||
|
|
this._runs.splice(i, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 结束全部冲锋 */
|
||
|
|
abortAll(): void {
|
||
|
|
for (let i = this._runs.length - 1; i >= 0; i--) {
|
||
|
|
this._finish(this._runs[i]);
|
||
|
|
}
|
||
|
|
this._runs.length = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 结束该施法者未完成的冲锋。
|
||
|
|
* @param caster 施法者
|
||
|
|
*/
|
||
|
|
private _abortCaster(caster: EntityId): void {
|
||
|
|
for (let i = this._runs.length - 1; i >= 0; i--) {
|
||
|
|
if (this._runs[i].caster !== caster) continue;
|
||
|
|
this._finish(this._runs[i]);
|
||
|
|
this._runs.splice(i, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 单帧位移与命中。false=结束。
|
||
|
|
* @param run 运行态
|
||
|
|
* @param dt 逻辑秒
|
||
|
|
*/
|
||
|
|
private _step(run: EarthChargeRun, dt: number): boolean {
|
||
|
|
if (!run.ctx.isAlive || !run.node.isValid) return false;
|
||
|
|
run.elapsed += dt;
|
||
|
|
const cap = run.speed * Math.min(run.duration, run.elapsed);
|
||
|
|
this._pointAlong(run.waypoints, cap, this._tmp);
|
||
|
|
setNodeWorldPos(run.node, this._tmp);
|
||
|
|
run.lastX = this._tmp.x;
|
||
|
|
run.lastY = this._tmp.y;
|
||
|
|
this._turn(run, dt, this._chargeAimDeg(run.lastX, run.lastY, run.waypoints, cap));
|
||
|
|
this._hitNear(run);
|
||
|
|
const pathLen = this._pathLen(run.waypoints);
|
||
|
|
if (run.elapsed >= run.duration || cap >= pathLen - 0.5) return false;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 冲锋体附近敌对怪各伤一次(含 atk_buff 击退)。
|
||
|
|
* @param run 运行态
|
||
|
|
*/
|
||
|
|
private _hitNear(run: EarthChargeRun): void {
|
||
|
|
const session = SkillPresentationEnv.getBattleSession();
|
||
|
|
if (!session || !session.world.isAlive(run.caster)) return;
|
||
|
|
this._hitRun = run;
|
||
|
|
session.world.view(HealthComp).forEach(this._hitOne);
|
||
|
|
this._hitRun = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 单体命中。
|
||
|
|
* @param id 实体
|
||
|
|
* @param hp 生命
|
||
|
|
*/
|
||
|
|
private readonly _hitOne = (id: EntityId, hp: HealthComp): void => {
|
||
|
|
const run = this._hitRun;
|
||
|
|
const session = SkillPresentationEnv.getBattleSession();
|
||
|
|
if (!run || !session) return;
|
||
|
|
if (id === run.caster) return;
|
||
|
|
if (this._alreadyHit(run, id)) return;
|
||
|
|
if (!session.world.isAlive(id) || !(hp.hp > 0)) return;
|
||
|
|
const move = session.world.getComponent(id, MovementComp);
|
||
|
|
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) return;
|
||
|
|
const casterTeam = session.world.getComponent(run.caster, TeamComp);
|
||
|
|
if (!BattleTeamId.isHostile(casterTeam, session.world.getComponent(id, TeamComp))) return;
|
||
|
|
const wp = SkillPresentationEnv.getTargetWorldPos(id);
|
||
|
|
if (!wp) return;
|
||
|
|
const dx = wp.x - run.lastX;
|
||
|
|
const dy = wp.y - run.lastY;
|
||
|
|
if (dx * dx + dy * dy > HIT_RADIUS * HIT_RADIUS) return;
|
||
|
|
run.hitIds.push(id);
|
||
|
|
run.ctx.dealDamage(id, { scale: run.scale, damageType: DMG_TYPE });
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 是否已命中。
|
||
|
|
* @param run 运行态
|
||
|
|
* @param id 实体
|
||
|
|
*/
|
||
|
|
private _alreadyHit(run: EarthChargeRun, id: EntityId): boolean {
|
||
|
|
const ids = run.hitIds;
|
||
|
|
for (let i = 0; i < ids.length; i++) {
|
||
|
|
if (ids[i] === id) return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 回收冲锋体。
|
||
|
|
* @param run 运行态
|
||
|
|
*/
|
||
|
|
private _finish(run: EarthChargeRun): void {
|
||
|
|
if (!run.node?.isValid) return;
|
||
|
|
const list = run.node.getComponentsInChildren(sp.Skeleton);
|
||
|
|
for (let i = 0; i < list.length; i++) {
|
||
|
|
BattleSpinePlayback.clearPendingComplete(list[i]);
|
||
|
|
}
|
||
|
|
run.node.angle = 0;
|
||
|
|
run.node.setScale(1, 1, 1);
|
||
|
|
gg.res.putNode(run.node);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 施法者落点格(Transform / 地图格,禁 session.grid.worldToCell——世界坐标原点与逻辑格不一致)。
|
||
|
|
* @param session 会话
|
||
|
|
* @param map 地图
|
||
|
|
* @param caster 施法者
|
||
|
|
*/
|
||
|
|
private _casterCell(session: BattleSession, map: MapManager, caster: EntityId): Cell | null {
|
||
|
|
const tf = session.world.getComponent(caster, TransformComp);
|
||
|
|
if (tf && session.grid.inBounds({ row: tf.row, col: tf.col })) {
|
||
|
|
return { row: tf.row, col: tf.col };
|
||
|
|
}
|
||
|
|
const center = SkillPresentationEnv.getCasterGridCenter(caster);
|
||
|
|
if (!center) return null;
|
||
|
|
const mg = map.getMapGridByWorldPos(center);
|
||
|
|
if (!mg) return null;
|
||
|
|
return { row: mg.Row, col: mg.Col };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 攻我方进路上离枪格最近的格;同距优先枪格正下方(更大 row)。
|
||
|
|
* @param session 会话
|
||
|
|
* @param caster 施法者
|
||
|
|
* @param from 施法者格
|
||
|
|
*/
|
||
|
|
private _snapApproachCell(session: BattleSession, caster: EntityId, from: Cell): { Row: number; Col: number } | null {
|
||
|
|
const enemyPort = this._enemyPort(session, caster);
|
||
|
|
const spawn = session.topology.getSpawnCell(enemyPort);
|
||
|
|
if (!spawn) {
|
||
|
|
console.log('[EarthCharge] 敌方出怪口缺失', enemyPort);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const forward = session.pathfinder.getApproachPath(spawn, enemyPort);
|
||
|
|
if (forward.length < 1) return null;
|
||
|
|
// 枪格正下方若在进路上,优先落该格
|
||
|
|
const belowRow = from.row + 1;
|
||
|
|
for (let i = 0; i < forward.length; i++) {
|
||
|
|
if (forward[i].row === belowRow && forward[i].col === from.col) {
|
||
|
|
return { Row: forward[i].row, Col: forward[i].col };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let best = 0;
|
||
|
|
let bestD = Number.POSITIVE_INFINITY;
|
||
|
|
let bestRow = Number.NEGATIVE_INFINITY;
|
||
|
|
for (let i = 0; i < forward.length; i++) {
|
||
|
|
const d = Math.abs(forward[i].row - from.row) + Math.abs(forward[i].col - from.col);
|
||
|
|
if (d < bestD || (d === bestD && forward[i].row > bestRow)) {
|
||
|
|
bestD = d;
|
||
|
|
bestRow = forward[i].row;
|
||
|
|
best = i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return { Row: forward[best].row, Col: forward[best].col };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 攻向施法者阵营的出怪口。
|
||
|
|
* @param session 会话
|
||
|
|
* @param caster 施法者
|
||
|
|
*/
|
||
|
|
private _enemyPort(session: BattleSession, caster: EntityId): number {
|
||
|
|
const team = session.world.getComponent(caster, TeamComp);
|
||
|
|
const owner = team?.ownerId ?? team?.team ?? 0;
|
||
|
|
return owner === 0 ? 1 : 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 从吸附格沿进路退向敌方出怪口。
|
||
|
|
* @param session 会话
|
||
|
|
* @param caster 施法者
|
||
|
|
* @param snap 吸附格
|
||
|
|
* @param params 配表
|
||
|
|
*/
|
||
|
|
private _buildRetreatWorlds(session: BattleSession, caster: EntityId, snap: { Row: number; Col: number }, params: EarthChargeParams): Vec3[] {
|
||
|
|
const enemyPort = this._enemyPort(session, caster);
|
||
|
|
const spawn = session.topology.getSpawnCell(enemyPort);
|
||
|
|
if (!spawn) {
|
||
|
|
console.log('[EarthCharge] 敌方出怪口缺失', enemyPort);
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
const cellSize = Math.max(1, BattleMapSpec.CELL_SIZE);
|
||
|
|
const maxSteps = Math.ceil(params.speed * params.duration / cellSize) + 8;
|
||
|
|
const from: Cell = { row: snap.Row, col: snap.Col };
|
||
|
|
const cells = session.pathfinder.buildRetreatPath(from, maxSteps, spawn, enemyPort);
|
||
|
|
const out: Vec3[] = [];
|
||
|
|
for (let i = 0; i < cells.length; i++) {
|
||
|
|
const w = SkillPresentationEnv.getGridWorldCenterByRowCol(cells[i].row, cells[i].col);
|
||
|
|
if (w) out.push(w.clone());
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 沿折线取点。
|
||
|
|
* @param points 路点
|
||
|
|
* @param dist 路程
|
||
|
|
* @param out 输出
|
||
|
|
*/
|
||
|
|
private _pointAlong(points: readonly Vec3[], dist: number, out: Vec3): void {
|
||
|
|
if (points.length === 0) {
|
||
|
|
out.set(0, 0, 0);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (points.length === 1 || dist <= 0) {
|
||
|
|
out.set(points[0].x, points[0].y, points[0].z);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let left = dist;
|
||
|
|
for (let i = 0; i < points.length - 1; i++) {
|
||
|
|
const a = points[i];
|
||
|
|
const b = points[i + 1];
|
||
|
|
const dx = b.x - a.x;
|
||
|
|
const dy = b.y - a.y;
|
||
|
|
const seg = Math.sqrt(dx * dx + dy * dy);
|
||
|
|
if (seg <= 1e-4) continue;
|
||
|
|
if (left <= seg) {
|
||
|
|
const t = left / seg;
|
||
|
|
out.set(a.x + dx * t, a.y + dy * t, a.z);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
left -= seg;
|
||
|
|
}
|
||
|
|
const last = points[points.length - 1];
|
||
|
|
out.set(last.x, last.y, last.z);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 折线总长。
|
||
|
|
* @param points 路点
|
||
|
|
*/
|
||
|
|
private _pathLen(points: readonly Vec3[]): number {
|
||
|
|
let len = 0;
|
||
|
|
for (let i = 0; i < points.length - 1; i++) {
|
||
|
|
const dx = points[i + 1].x - points[i].x;
|
||
|
|
const dy = points[i + 1].y - points[i].y;
|
||
|
|
len += Math.sqrt(dx * dx + dy * dy);
|
||
|
|
}
|
||
|
|
return len;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 播预制体默认循环动画。
|
||
|
|
* @param root 冲锋根
|
||
|
|
*/
|
||
|
|
private _playPrefabAnim(root: Node): void {
|
||
|
|
const sk = root.getComponent(sp.Skeleton) ?? root.getComponentInChildren(sp.Skeleton);
|
||
|
|
if (!sk?.isValid) {
|
||
|
|
console.warn('[EarthCharge] 缺 Spine');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (sk.skeletonData) ensureSpineSkeletonDataRuntime(sk.skeletonData);
|
||
|
|
const typed = sk as sp.Skeleton & { defaultAnimation?: string };
|
||
|
|
const name = String(typed.defaultAnimation || sk.animation || '').trim();
|
||
|
|
if (!name) {
|
||
|
|
console.warn('[EarthCharge] 无默认动画');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
BattleSpinePlayback.play(sk, name, { loop: true, optional: true });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 节点对准冲锋前方。
|
||
|
|
* @param run 运行态
|
||
|
|
* @param dt 逻辑秒
|
||
|
|
* @param desired 目标角
|
||
|
|
*/
|
||
|
|
private _turn(run: EarthChargeRun, dt: number, desired: number | null): void {
|
||
|
|
if (desired == null || !run.node.isValid) return;
|
||
|
|
const delta = shortestAngleDeltaDeg(run.angle, desired);
|
||
|
|
const maxStep = TURN_DEG * dt;
|
||
|
|
const step = delta > maxStep ? maxStep : (delta < -maxStep ? -maxStep : delta);
|
||
|
|
run.angle += step;
|
||
|
|
run.node.angle = run.angle;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 沿路径前瞻朝向(度,0=右)。
|
||
|
|
* @param fromX 当前 X
|
||
|
|
* @param fromY 当前 Y
|
||
|
|
* @param waypoints 路点
|
||
|
|
* @param traveled 已走路程
|
||
|
|
*/
|
||
|
|
private _chargeAimDeg(fromX: number, fromY: number, waypoints: readonly Vec3[], traveled: number): number | null {
|
||
|
|
this._from.set(fromX, fromY, 0);
|
||
|
|
this._pointAlong(waypoints, traveled + LOOKAHEAD, this._aim);
|
||
|
|
return computeAimAngleDeg(this._from, this._aim);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Skill.skill type=1 土冲锋入口 */
|
||
|
|
export class EarthChargePresenter implements ISkillPresenter {
|
||
|
|
/**
|
||
|
|
* 播放土冲锋。
|
||
|
|
* @param ctx 表现上下文
|
||
|
|
* @param host Skill 宿主
|
||
|
|
*/
|
||
|
|
present(ctx: SkillPresentationContext, host: Skill): void {
|
||
|
|
if (!ctx.isAlive) {
|
||
|
|
ctx.finish();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
getEarthChargeHost().start(ctx, host);
|
||
|
|
}
|
||
|
|
}
|