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.
757 lines
25 KiB
757 lines
25 KiB
import { Node, UITransform, Vec3, sp } from 'cc';
|
|
import type { BattleSession, 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 { applyMonsterFootOffset } from '../map/BattleMapSpec';
|
|
import { findNearestCurrentlyWalkableCell } from '../map/MapPathQueries';
|
|
import type { Skill } from '../skill/Skill';
|
|
import type { SkillPresentationContext } from '../skill/SkillPresentationTypes';
|
|
import { getBattleTimeScale } from '../skill/BattlePresentationClock';
|
|
import { SkillPresentationEnv } from '../skill/SkillPresentationEnv';
|
|
import { setNodeWorldPos } from '../skill/SkillTransformDriver';
|
|
import { BATTLE_MONSTER_CCCLASS, type IBattleMonsterView } from '../view/IBattleMonsterView';
|
|
import { BattleSpinePlayback } from './BattleSpinePlayback';
|
|
import type { ISkillPresenter } from './SkillPresenter';
|
|
|
|
/** 枪尖骨骼(枪类皮肤 bone3 在杆头) */
|
|
const TIP_BONE = 'bone3';
|
|
/** 吸入结束(动画 0.4 突刺前) */
|
|
const SUCK_UNTIL = 0.28;
|
|
/** 砸地关键帧 */
|
|
const SMASH_AT = 1.0;
|
|
/** 砸向格子时长 */
|
|
const SLAM_DUR = 0.16;
|
|
/** 落地压扁恢复 */
|
|
const SETTLE_SEC = 0.08;
|
|
/** 动画未到砸地关键帧时的最长等待(逻辑秒) */
|
|
const TOSS_MAX = 3;
|
|
/** battleHero/prefab/skill/挑飞砸地效果 */
|
|
const SMASH_PREFAB = '挑飞砸地效果';
|
|
/** 砸地 Spine 动画 */
|
|
const SMASH_ANIM = '三尖两面刀技能2';
|
|
/** 直伤类型 */
|
|
const DMG_TYPE = 'spear_toss';
|
|
/** 眩晕 buff */
|
|
const STUN_BUFF = 'debuff_4';
|
|
/** 吸入缓出 */
|
|
const SUCK_EASE = 3;
|
|
/** 砸地加速 */
|
|
const SLAM_EASE = 3;
|
|
|
|
/** Skill.skill type=7 解析结果 */
|
|
export interface SpearTossParams {
|
|
/** 圆形半径(像素) */
|
|
radius: number;
|
|
/** 伤害系数 */
|
|
scale: number;
|
|
/** 眩晕秒 */
|
|
stunSec: number;
|
|
}
|
|
|
|
/** 砸地特效 */
|
|
interface TossFx {
|
|
/** 特效节点 */
|
|
node: Node;
|
|
/** 剩余秒 */
|
|
wait: number;
|
|
}
|
|
|
|
/** 一次挑飞 */
|
|
interface SpearTossRun {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** 被挑飞怪 */
|
|
victim: EntityId;
|
|
/** 表现上下文 */
|
|
ctx: SkillPresentationContext;
|
|
/** Skill 宿主 */
|
|
host: Skill;
|
|
/** 播控序号 */
|
|
seq: number;
|
|
/** 圆形半径 */
|
|
radius: number;
|
|
/** 伤害系数 */
|
|
scale: number;
|
|
/** 眩晕秒 */
|
|
stunSec: number;
|
|
/** 动画累计秒(无 trackTime 时) */
|
|
elapsed: number;
|
|
/** 0 吸入 / 1 跟随枪尖 / 2 砸地 / 3 特效 */
|
|
phase: number;
|
|
/** 吸入起点 X */
|
|
fromX: number;
|
|
/** 吸入起点 Y */
|
|
fromY: number;
|
|
/** 砸地起点 X */
|
|
slamFromX: number;
|
|
/** 砸地起点 Y */
|
|
slamFromY: number;
|
|
/** 落点 X */
|
|
landX: number;
|
|
/** 落点 Y */
|
|
landY: number;
|
|
/** 落点行 */
|
|
landRow: number;
|
|
/** 落点列 */
|
|
landCol: number;
|
|
/** 砸地已过秒(动画时间) */
|
|
slamT: number;
|
|
/** 落地后特效剩余秒 */
|
|
fxWait: number;
|
|
/** 落地等待初值 */
|
|
fxMax: number;
|
|
/** 默认缩放 X */
|
|
scaleX: number;
|
|
/** 默认缩放 Y */
|
|
scaleY: number;
|
|
}
|
|
|
|
let _host: SpearTossHost | null = null;
|
|
|
|
/** 挑飞砸地运行时 */
|
|
export function getSpearTossHost(): SpearTossHost {
|
|
if (!_host) _host = new SpearTossHost();
|
|
return _host;
|
|
}
|
|
|
|
/** 挑飞砸地:吸到枪尖、随动画挑起、砸到最近可行走格 */
|
|
export class SpearTossHost {
|
|
private readonly _runs: SpearTossRun[] = [];
|
|
private readonly _fx: TossFx[] = [];
|
|
private readonly _tmp = new Vec3();
|
|
private readonly _tip = new Vec3();
|
|
private readonly _boneLocal = new Vec3();
|
|
private readonly _land = new Vec3();
|
|
/** 当前范围扫描所属施法 */
|
|
private _hitRun: SpearTossRun | null = null;
|
|
private _hitX = 0;
|
|
private _hitY = 0;
|
|
private _tipWarned = false;
|
|
|
|
/**
|
|
* 解析 `7,半径,伤害万分比,眩晕秒`。
|
|
* @param raw Skill.skill
|
|
*/
|
|
static parse(raw: string): SpearTossParams | null {
|
|
const parts = String(raw ?? '').split(',').map((s) => Number(String(s).trim()));
|
|
if (parts[0] !== 7) return null;
|
|
const radius = parts[1];
|
|
const wan = parts[2];
|
|
const stunSec = parts[3];
|
|
if (!(radius > 0) || !(wan > 0) || !(stunSec >= 0)) {
|
|
console.log('[SpearToss] skill 参数无效');
|
|
return null;
|
|
}
|
|
return { radius, scale: wan / 10000, stunSec };
|
|
}
|
|
|
|
/**
|
|
* 开战挑飞。
|
|
* @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 = SpearTossHost.parse(raw);
|
|
if (!params) {
|
|
host.drainLogicPresentation(ctx);
|
|
return;
|
|
}
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
const map = SkillPresentationEnv.getMap();
|
|
if (!session || !map) {
|
|
console.log('[SpearToss] 无 session/map');
|
|
host.drainLogicPresentation(ctx);
|
|
return;
|
|
}
|
|
const victim = ctx.targets[0];
|
|
if (victim == null) {
|
|
console.log('[SpearToss] 无目标', ctx.params.skillId);
|
|
host.drainLogicPresentation(ctx);
|
|
return;
|
|
}
|
|
const node = SkillPresentationEnv.getMonsterNode(victim);
|
|
if (!node?.isValid) {
|
|
console.log('[SpearToss] 目标无节点', victim);
|
|
host.drainLogicPresentation(ctx);
|
|
return;
|
|
}
|
|
const move = session.world.getComponent(victim, MovementComp);
|
|
if (!move || move.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) {
|
|
console.log('[SpearToss] 目标不可挑飞', victim);
|
|
host.drainLogicPresentation(ctx);
|
|
return;
|
|
}
|
|
const animation = String(ctx.params.animation ?? '').trim();
|
|
if (!animation) {
|
|
console.log('[SpearToss] 缺 animation', ctx.params.skillId);
|
|
host.drainLogicPresentation(ctx);
|
|
return;
|
|
}
|
|
|
|
const follow = String(ctx.params.skillKind ?? '') === 'weapon' || host.visualChannel.useWeapon;
|
|
if (!host.visualChannel.ensureWeapon()) {
|
|
console.log('[SpearToss] 无武器', ctx.params.skillId);
|
|
host.drainLogicPresentation(ctx);
|
|
return;
|
|
}
|
|
const seq = host.beginPresentationCast(ctx);
|
|
if (!(seq > 0)) return;
|
|
host.visualChannel.playCastAnim({
|
|
caster: ctx.caster,
|
|
animation,
|
|
followWeapon: follow,
|
|
skillKind: String(ctx.params.skillKind ?? ''),
|
|
primaryTarget: victim,
|
|
holdCasting: true,
|
|
animTimeScale: 1,
|
|
onAnimComplete: this._noopAnim,
|
|
});
|
|
|
|
move.presentLocked = true;
|
|
SkillPresentationEnv.holdMonsterVisual(victim);
|
|
this._runs.push({
|
|
caster: ctx.caster,
|
|
victim,
|
|
ctx,
|
|
host,
|
|
seq,
|
|
radius: params.radius,
|
|
scale: params.scale,
|
|
stunSec: params.stunSec,
|
|
elapsed: 0,
|
|
phase: 0,
|
|
fromX: node.worldPosition.x,
|
|
fromY: node.worldPosition.y,
|
|
slamFromX: 0,
|
|
slamFromY: 0,
|
|
landX: 0,
|
|
landY: 0,
|
|
landRow: -1,
|
|
landCol: -1,
|
|
slamT: 0,
|
|
fxWait: 0,
|
|
fxMax: SETTLE_SEC,
|
|
scaleX: node.scale.x,
|
|
scaleY: node.scale.y,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 推进挑飞。
|
|
* @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], true);
|
|
this._runs.splice(i, 1);
|
|
}
|
|
}
|
|
this._tickFx(dt);
|
|
}
|
|
|
|
/** 结束全部挑飞 */
|
|
abortAll(): void {
|
|
for (let i = this._runs.length - 1; i >= 0; i--) {
|
|
this._finish(this._runs[i], false);
|
|
}
|
|
this._runs.length = 0;
|
|
this._dropFx();
|
|
}
|
|
|
|
/**
|
|
* 结束该施法者未完成的挑飞。
|
|
* @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], false);
|
|
this._runs.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/** 武器动画结束占位(由 Host 收招) */
|
|
private readonly _noopAnim = (): void => {};
|
|
|
|
/**
|
|
* 单帧。返回 false 表示结束。
|
|
* @param run 运行态
|
|
* @param dt 逻辑秒
|
|
*/
|
|
private _step(run: SpearTossRun, dt: number): boolean {
|
|
if (!run.ctx.isAlive || !run.host.isPresentationCast(run.seq)) return false;
|
|
const node = SkillPresentationEnv.getMonsterNode(run.victim);
|
|
if (!node?.isValid) return false;
|
|
run.elapsed += this._animDt(run, dt);
|
|
const track = this._trackTime(run);
|
|
if (run.elapsed > TOSS_MAX && run.phase < 2) {
|
|
console.log('[SpearToss] 动画未到砸地关键帧,结束挑飞', track, run.elapsed);
|
|
return false;
|
|
}
|
|
if (run.phase === 0) {
|
|
this._applySuck(run, node, track);
|
|
if (track >= SUCK_UNTIL) run.phase = 1;
|
|
return true;
|
|
}
|
|
if (run.phase === 1) {
|
|
if (!this._stickTip(run, node)) return false;
|
|
if (track >= SMASH_AT) {
|
|
if (!this._beginSlam(run, node)) return false;
|
|
run.phase = 2;
|
|
}
|
|
return true;
|
|
}
|
|
if (run.phase === 2) {
|
|
run.slamT += this._animDt(run, dt);
|
|
this._applySlam(run, node);
|
|
if (run.slamT >= SLAM_DUR) {
|
|
this._onLand(run, node);
|
|
run.phase = 3;
|
|
}
|
|
return true;
|
|
}
|
|
run.fxWait -= dt;
|
|
this._applySettle(run, node);
|
|
return run.fxWait > 0;
|
|
}
|
|
|
|
/**
|
|
* 吸入枪尖。
|
|
* @param run 运行态
|
|
* @param node 怪节点
|
|
* @param track 动画时间
|
|
*/
|
|
private _applySuck(run: SpearTossRun, node: Node, track: number): void {
|
|
if (!this._readTip(run, this._tip)) {
|
|
this._tip.set(run.fromX, run.fromY, node.worldPosition.z);
|
|
}
|
|
const u = this._easeOut(Math.min(1, Math.max(0, track / SUCK_UNTIL)), SUCK_EASE);
|
|
this._tmp.set(
|
|
run.fromX + (this._tip.x - run.fromX) * u,
|
|
run.fromY + (this._tip.y - run.fromY) * u,
|
|
node.worldPosition.z,
|
|
);
|
|
setNodeWorldPos(node, this._tmp);
|
|
const lift = 1 + 0.08 * u;
|
|
node.setScale(run.scaleX / lift, run.scaleY * lift, 1);
|
|
}
|
|
|
|
/**
|
|
* 贴枪尖。
|
|
* @param run 运行态
|
|
* @param node 怪节点
|
|
*/
|
|
private _stickTip(run: SpearTossRun, node: Node): boolean {
|
|
if (!this._readTip(run, this._tip)) return false;
|
|
this._tmp.set(this._tip.x, this._tip.y, node.worldPosition.z);
|
|
setNodeWorldPos(node, this._tmp);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 锁定砸地落点。
|
|
* @param run 运行态
|
|
* @param node 怪节点
|
|
*/
|
|
private _beginSlam(run: SpearTossRun, node: Node): boolean {
|
|
run.slamFromX = node.worldPosition.x;
|
|
run.slamFromY = node.worldPosition.y;
|
|
run.slamT = 0;
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
const map = SkillPresentationEnv.getMap();
|
|
if (!session || !map) {
|
|
console.log('[SpearToss] 砸地时无 session/map');
|
|
return false;
|
|
}
|
|
if (!this._readTip(run, this._tip)) {
|
|
this._tip.set(run.slamFromX, run.slamFromY, 0);
|
|
}
|
|
const grid = map.getMapGridByWorldPos(this._tip);
|
|
if (!grid) {
|
|
console.log('[SpearToss] 枪尖不在地图格上', this._tip.x, this._tip.y);
|
|
return false;
|
|
}
|
|
const snap = findNearestCurrentlyWalkableCell(map, session, { row: grid.Row, col: grid.Col });
|
|
if (!snap) {
|
|
console.log('[SpearToss] 枪尖附近无可行走格', grid.Row, grid.Col);
|
|
return false;
|
|
}
|
|
const center = SkillPresentationEnv.getGridWorldCenterByRowCol(snap.Row, snap.Col);
|
|
if (!center) {
|
|
console.log('[SpearToss] 落点格无世界坐标', snap.Row, snap.Col);
|
|
return false;
|
|
}
|
|
applyMonsterFootOffset(center, this._land);
|
|
run.landX = this._land.x;
|
|
run.landY = this._land.y;
|
|
run.landRow = snap.Row;
|
|
run.landCol = snap.Col;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 砸向落点(加速下落)。
|
|
* @param run 运行态
|
|
* @param node 怪节点
|
|
*/
|
|
private _applySlam(run: SpearTossRun, node: Node): void {
|
|
const u = this._easeIn(Math.min(1, run.slamT / SLAM_DUR), SLAM_EASE);
|
|
this._tmp.set(
|
|
run.slamFromX + (run.landX - run.slamFromX) * u,
|
|
run.slamFromY + (run.landY - run.slamFromY) * u,
|
|
node.worldPosition.z,
|
|
);
|
|
setNodeWorldPos(node, this._tmp);
|
|
const squash = u * u;
|
|
node.setScale(run.scaleX * (1 + 0.22 * squash), run.scaleY * (1 - 0.28 * squash), 1);
|
|
}
|
|
|
|
/**
|
|
* 落地:特效、范围伤、眩晕、同步格子。
|
|
* @param run 运行态
|
|
* @param node 怪节点
|
|
*/
|
|
private _onLand(run: SpearTossRun, node: Node): void {
|
|
this._tmp.set(run.landX, run.landY, node.worldPosition.z);
|
|
setNodeWorldPos(node, this._tmp);
|
|
node.setScale(run.scaleX * 1.18, run.scaleY * 0.72, 1);
|
|
this._playSmashFx(run, node.worldPosition.z);
|
|
this._syncLogic(run);
|
|
this._hitArea(run);
|
|
SkillPresentationEnv.getMonsterPresenter()?.sortMonstersByY();
|
|
}
|
|
|
|
/**
|
|
* 落地压扁回正。
|
|
* @param run 运行态
|
|
* @param node 怪节点
|
|
*/
|
|
private _applySettle(run: SpearTossRun, node: Node): void {
|
|
const passed = run.fxMax - run.fxWait;
|
|
const u = Math.min(1, passed / SETTLE_SEC);
|
|
node.setScale(run.scaleX * (1.18 - 0.18 * u), run.scaleY * (0.72 + 0.28 * u), 1);
|
|
if (u >= 1) node.setScale(run.scaleX, run.scaleY, 1);
|
|
}
|
|
|
|
/**
|
|
* 写入逻辑格并重寻路(仍存活时)。
|
|
* @param run 运行态
|
|
*/
|
|
private _syncLogic(run: SpearTossRun): void {
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
if (!session?.world.isAlive(run.victim)) return;
|
|
const tf = session.world.getComponent(run.victim, TransformComp);
|
|
const move = session.world.getComponent(run.victim, MovementComp);
|
|
if (!tf || !move) return;
|
|
tf.row = run.landRow;
|
|
tf.col = run.landCol;
|
|
session.spatial.updateEntity(run.victim, { row: run.landRow, col: run.landCol });
|
|
const logical = session.grid.cellToWorld({ row: run.landRow, col: run.landCol });
|
|
move.worldX = logical.x;
|
|
move.worldY = logical.y;
|
|
move.reachedGoal = false;
|
|
move.path = [];
|
|
move.pathIndex = 0;
|
|
move.hasAttackGoal = false;
|
|
move.forcePresentSync = true;
|
|
session.movement.assignPath(run.victim);
|
|
}
|
|
|
|
/**
|
|
* 落点圆形范围伤 + 眩晕。
|
|
* @param run 运行态
|
|
*/
|
|
private _hitArea(run: SpearTossRun): void {
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
if (!session || !session.world.isAlive(run.caster)) return;
|
|
this._hitRun = run;
|
|
this._hitX = run.landX;
|
|
this._hitY = run.landY;
|
|
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 (!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;
|
|
if (!this._inRadius(id, run.radius)) return;
|
|
if (run.stunSec > 0) this._stun(session, run.caster, id, run.stunSec);
|
|
run.ctx.dealDamage(id, { scale: run.scale, damageType: DMG_TYPE });
|
|
};
|
|
|
|
/**
|
|
* 是否在落点半径内(挑飞目标用节点坐标)。
|
|
* @param id 实体
|
|
* @param radius 半径
|
|
*/
|
|
private _inRadius(id: EntityId, radius: number): boolean {
|
|
const node = SkillPresentationEnv.getMonsterNode(id);
|
|
const wp = node?.isValid ? node.worldPosition : SkillPresentationEnv.getTargetWorldPos(id);
|
|
if (!wp) return false;
|
|
const dx = wp.x - this._hitX;
|
|
const dy = wp.y - this._hitY;
|
|
return dx * dx + dy * dy <= radius * radius;
|
|
}
|
|
|
|
/**
|
|
* 上眩晕。
|
|
* @param session 会话
|
|
* @param caster 施法者
|
|
* @param target 目标
|
|
* @param dur 秒
|
|
*/
|
|
private _stun(session: BattleSession, caster: EntityId, target: EntityId, dur: number): void {
|
|
if (!session.config.getBuff(STUN_BUFF)) {
|
|
console.log('[SpearToss] 缺 buff', STUN_BUFF);
|
|
return;
|
|
}
|
|
session.buffs.add(target, STUN_BUFF, caster, 1, { duration: dur });
|
|
}
|
|
|
|
/**
|
|
* 播砸地预制体。
|
|
* @param run 运行态
|
|
* @param z 世界 Z
|
|
*/
|
|
private _playSmashFx(run: SpearTossRun, z: number): void {
|
|
const parent = SkillPresentationEnv.flyLayer ?? SkillPresentationEnv.getMap()?.ensureHeroParent();
|
|
if (!parent?.isValid) {
|
|
console.log('[SpearToss] 砸地特效无父节点');
|
|
this._setFxWait(run, SETTLE_SEC);
|
|
return;
|
|
}
|
|
let node: Node | null = null;
|
|
try {
|
|
node = gg.res.getNode(GPath.HeroSkillPrefab(), SMASH_PREFAB, GBundle.BattleHero);
|
|
} catch (e) {
|
|
console.log('[SpearToss] 砸地特效加载异常', String(e));
|
|
this._setFxWait(run, SETTLE_SEC);
|
|
return;
|
|
}
|
|
if (!node?.isValid) {
|
|
console.log('[SpearToss] 砸地特效预制体缺失', SMASH_PREFAB);
|
|
this._setFxWait(run, SETTLE_SEC);
|
|
return;
|
|
}
|
|
if (!parent.active) parent.active = true;
|
|
parent.addChild(node);
|
|
node.active = true;
|
|
node.layer = parent.layer;
|
|
node.setWorldPosition(run.landX, run.landY, z);
|
|
const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton);
|
|
if (!sk?.isValid) {
|
|
console.log('[SpearToss] 砸地特效缺少 Skeleton');
|
|
gg.res.putNode(node);
|
|
this._setFxWait(run, SETTLE_SEC);
|
|
return;
|
|
}
|
|
const wait = BattleSpinePlayback.resolveLogicalCompleteDelay(sk, SMASH_ANIM);
|
|
if (wait == null) {
|
|
console.log('[SpearToss] 砸地动画不存在', SMASH_ANIM);
|
|
gg.res.putNode(node);
|
|
this._setFxWait(run, SETTLE_SEC);
|
|
return;
|
|
}
|
|
if (!BattleSpinePlayback.play(sk, SMASH_ANIM, { loop: false })) {
|
|
gg.res.putNode(node);
|
|
this._setFxWait(run, SETTLE_SEC);
|
|
return;
|
|
}
|
|
this._fx.push({ node, wait });
|
|
this._setFxWait(run, Math.max(SETTLE_SEC, wait));
|
|
}
|
|
|
|
/**
|
|
* 记录落地等待。
|
|
* @param run 运行态
|
|
* @param wait 秒
|
|
*/
|
|
private _setFxWait(run: SpearTossRun, wait: number): void {
|
|
run.fxWait = wait;
|
|
run.fxMax = wait;
|
|
}
|
|
|
|
/**
|
|
* 枪尖世界坐标。
|
|
* @param run 运行态
|
|
* @param out 输出
|
|
*/
|
|
private _readTip(run: SpearTossRun, out: Vec3): boolean {
|
|
const sk = run.host.visualChannel.weapon.skinAnim;
|
|
const aim = run.host.visualChannel.weapon.aim;
|
|
if (sk?.isValid) {
|
|
const bone = typeof sk.findBone === 'function' ? sk.findBone(TIP_BONE) : null;
|
|
const ui = sk.node?.getComponent(UITransform);
|
|
if (bone && ui) {
|
|
this._boneLocal.set(bone.worldX, bone.worldY, 0);
|
|
ui.convertToWorldSpaceAR(this._boneLocal, out);
|
|
return true;
|
|
}
|
|
}
|
|
if (aim?.isValid) {
|
|
if (!this._tipWarned) {
|
|
console.log('[SpearToss] 枪尖骨骼缺失,改用朝向节点', TIP_BONE);
|
|
this._tipWarned = true;
|
|
}
|
|
out.set(aim.worldPosition);
|
|
return true;
|
|
}
|
|
console.log('[SpearToss] 无法取得枪尖坐标');
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 当前武器动画时间。
|
|
* @param run 运行态
|
|
*/
|
|
private _trackTime(run: SpearTossRun): number {
|
|
const sk = run.host.visualChannel.weapon.skinAnim;
|
|
const entry = sk?.isValid && typeof sk.getCurrent === 'function' ? sk.getCurrent(0) : null;
|
|
const t = entry != null ? Number(entry.trackTime) : NaN;
|
|
const spine = Number.isFinite(t) && t >= 0 ? t : 0;
|
|
return spine > run.elapsed ? spine : run.elapsed;
|
|
}
|
|
|
|
/**
|
|
* 动画时间增量(跟枪尖关键帧)。
|
|
* @param run 运行态
|
|
* @param dt 逻辑秒
|
|
*/
|
|
private _animDt(run: SpearTossRun, dt: number): number {
|
|
const sk = run.host.visualChannel.weapon.skinAnim;
|
|
const scale = sk?.isValid ? Math.max(0.01, sk.timeScale) : 1;
|
|
const battle = Math.max(0.01, getBattleTimeScale());
|
|
return dt * (scale / battle);
|
|
}
|
|
|
|
/**
|
|
* 缓出。
|
|
* @param t 0~1
|
|
* @param p 次数
|
|
*/
|
|
private _easeOut(t: number, p: number): number {
|
|
const r = 1 - t;
|
|
let v = r;
|
|
for (let i = 1; i < p; i++) v *= r;
|
|
return 1 - v;
|
|
}
|
|
|
|
/**
|
|
* 缓入。
|
|
* @param t 0~1
|
|
* @param p 次数
|
|
*/
|
|
private _easeIn(t: number, p: number): number {
|
|
let v = t;
|
|
for (let i = 1; i < p; i++) v *= t;
|
|
return v;
|
|
}
|
|
|
|
/**
|
|
* 推进砸地特效。
|
|
* @param dt 逻辑秒
|
|
*/
|
|
private _tickFx(dt: number): void {
|
|
for (let i = this._fx.length - 1; i >= 0; i--) {
|
|
const fx = this._fx[i];
|
|
fx.wait -= dt;
|
|
if (fx.wait > 0) continue;
|
|
this._recycleFx(fx.node);
|
|
this._fx.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/** 回收全部砸地特效 */
|
|
private _dropFx(): void {
|
|
for (let i = this._fx.length - 1; i >= 0; i--) {
|
|
this._recycleFx(this._fx[i].node);
|
|
}
|
|
this._fx.length = 0;
|
|
}
|
|
|
|
/**
|
|
* 砸地特效回池。
|
|
* @param node 特效
|
|
*/
|
|
private _recycleFx(node: Node): void {
|
|
if (!node?.isValid) return;
|
|
const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton);
|
|
BattleSpinePlayback.clearPendingComplete(sk);
|
|
gg.res.putNode(node);
|
|
}
|
|
|
|
/**
|
|
* 收招:解锁寻路或回收已死目标。
|
|
* @param run 运行态
|
|
* @param releaseLock 是否 finish
|
|
*/
|
|
private _finish(run: SpearTossRun, releaseLock: boolean): void {
|
|
const node = SkillPresentationEnv.getMonsterNode(run.victim);
|
|
if (node?.isValid) node.setScale(run.scaleX, run.scaleY, 1);
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
if (session?.world.isAlive(run.victim)) {
|
|
const move = session.world.getComponent(run.victim, MovementComp);
|
|
if (move) {
|
|
move.presentLocked = false;
|
|
move.forcePresentSync = true;
|
|
}
|
|
if (run.landRow >= 0) this._syncLogic(run);
|
|
else session.movement.assignPath(run.victim);
|
|
const view = node?.getComponent(BATTLE_MONSTER_CCCLASS) as IBattleMonsterView | null;
|
|
view?.playWalk?.();
|
|
}
|
|
SkillPresentationEnv.releaseMonsterVisual(run.victim);
|
|
const host = run.host;
|
|
if (!host?.isValid || !host.visualChannel) {
|
|
console.log('[SpearToss] 收招时宿主已销毁', run.seq);
|
|
if (!releaseLock && run.ctx.isAlive) run.ctx.cancel('aborted');
|
|
return;
|
|
}
|
|
host.visualChannel.endHoldCast();
|
|
if (releaseLock && host.isPresentationCast(run.seq)) {
|
|
host.finishPresentationCast(run.seq, false);
|
|
} else if (!releaseLock && run.ctx.isAlive) {
|
|
run.ctx.cancel('aborted');
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Skill.skill type=7 表现入口 */
|
|
export class SpearTossPresenter implements ISkillPresenter {
|
|
/**
|
|
* 播放挑飞砸地。
|
|
* @param ctx 表现上下文
|
|
* @param host Skill 宿主
|
|
*/
|
|
present(ctx: SkillPresentationContext, host: Skill): void {
|
|
if (!ctx.isAlive) {
|
|
ctx.finish();
|
|
return;
|
|
}
|
|
getSpearTossHost().start(ctx, host);
|
|
}
|
|
}
|
|
|