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.
656 lines
23 KiB
656 lines
23 KiB
import { isValid, Node, UITransform, Vec3, sp } from 'cc';
|
|
import type { EntityId } from 'db://assets/mx/module/ecs';
|
|
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 { logicalWorldToCocos } from '../bridge/CocosBattleMovementBridge';
|
|
import type { Skill } from '../skill/Skill';
|
|
import { SkillPresentationEnv } from '../skill/SkillPresentationEnv';
|
|
import type { SkillPresentationContext } from '../skill/SkillPresentationTypes';
|
|
import { SkillTransformDriver } from '../skill/SkillTransformDriver';
|
|
import { BattleSpinePlayback } from './BattleSpinePlayback';
|
|
import { buildShadowDealContext } from './SkillShadowContext';
|
|
|
|
const DMG_TYPE = 'thrown_circle';
|
|
const FLY_SPEED = 576;
|
|
/** battleHero/prefab/skill/飞棍 */
|
|
const FLY_STICK_PREFAB = '飞棍';
|
|
/** 飞棍 Spine 循环动画 */
|
|
const FLY_STICK_ANIM = '原地旋转大风车';
|
|
/**
|
|
* 飞棍预制体 UITransform 边长 350:scale=1 时视觉半径 175。
|
|
* 落地缩放 = atk_para 半径 / 该值,使圈伤范围与表现对齐。
|
|
*/
|
|
const FLY_STICK_DESIGN_RADIUS = 175;
|
|
|
|
/**
|
|
* 以落点为圆心扫描当前敌对单位并回调(每段重新索敌,不锁施法瞬间名单)。
|
|
* @param session 战斗会话
|
|
* @param caster 施法者
|
|
* @param x 圆心逻辑 X
|
|
* @param y 圆心逻辑 Y
|
|
* @param radius 半径像素
|
|
* @param onHit 命中回调
|
|
*/
|
|
export function pulseHostileCircle(
|
|
session: NonNullable<ReturnType<typeof SkillPresentationEnv.getBattleSession>>,
|
|
caster: EntityId,
|
|
x: number,
|
|
y: number,
|
|
radius: number,
|
|
onHit: (id: EntityId) => void,
|
|
): void {
|
|
if (!(radius > 0)) return;
|
|
const casterTeam = session.world.getComponent(caster, TeamComp);
|
|
const cell = session.grid.worldToCell(x, y);
|
|
const radiusCells = radius / Math.max(1, session.grid.cellSize);
|
|
const ids = session.spatial.queryRadius(cell, radiusCells + 1);
|
|
const r2 = radius * radius;
|
|
for (let i = 0; i < ids.length; i++) {
|
|
const id = ids[i];
|
|
if (id === caster) continue;
|
|
if (!session.world.isAlive(id)) continue;
|
|
const hp = session.world.getComponent(id, HealthComp);
|
|
if (!hp || !(hp.hp > 0)) continue;
|
|
const move = session.world.getComponent(id, MovementComp);
|
|
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) continue;
|
|
if (!BattleTeamId.isHostile(casterTeam, session.world.getComponent(id, TeamComp))) continue;
|
|
const pos = logicPosOf(session, id);
|
|
if (!pos) continue;
|
|
const dx = pos.x - x;
|
|
const dy = pos.y - y;
|
|
if (dx * dx + dy * dy > r2) continue;
|
|
onHit(id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 实体逻辑坐标。
|
|
* @param session 战斗会话
|
|
* @param entity 实体
|
|
*/
|
|
export function logicPosOf(
|
|
session: NonNullable<ReturnType<typeof SkillPresentationEnv.getBattleSession>>,
|
|
entity: EntityId,
|
|
): { x: number; y: number } | null {
|
|
const move = session.world.getComponent(entity, MovementComp);
|
|
if (move && (move.worldX !== 0 || move.worldY !== 0)) {
|
|
return { x: move.worldX, y: move.worldY };
|
|
}
|
|
const tf = session.world.getComponent(entity, TransformComp);
|
|
if (!tf) return null;
|
|
return session.grid.cellToWorld({ row: tf.row, col: tf.col });
|
|
}
|
|
|
|
/** atk_type=18 独立圈伤任务 */
|
|
interface ThrownCircleRun {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** 影子伤害上下文 */
|
|
ctx: SkillPresentationContext;
|
|
/** 圆心逻辑 X */
|
|
x: number;
|
|
/** 圆心逻辑 Y */
|
|
y: number;
|
|
/** 半径像素 */
|
|
radius: number;
|
|
/** 伤害倍率 */
|
|
scale: number;
|
|
/** 总段数 */
|
|
hitCount: number;
|
|
/** 段间隔秒 */
|
|
intervalSec: number;
|
|
/** 已结算段数 */
|
|
done: number;
|
|
/** 距下一段剩余秒 */
|
|
wait: number;
|
|
/** 飞出的飞棍节点 */
|
|
flyNode: Node | null;
|
|
}
|
|
|
|
/** 飞出结果 */
|
|
interface ThrowFlyResult {
|
|
/** 飞棍节点 */
|
|
node: Node;
|
|
/** 飞行秒数(落地后才开始段伤) */
|
|
duration: number;
|
|
}
|
|
|
|
let _host: ThrownCircleHost | null = null;
|
|
|
|
/** atk_type=18 丢出独立圈伤宿主 */
|
|
export function getThrownCircleHost(): ThrownCircleHost {
|
|
if (!_host) _host = new ThrownCircleHost();
|
|
return _host;
|
|
}
|
|
|
|
/**
|
|
* atk_type=18:瞬间释放施法锁后,丢出飞棍到主目标落点,按段数/间隔独立圈伤。
|
|
*/
|
|
export class ThrownCircleHost {
|
|
private readonly _runs: ThrownCircleRun[] = [];
|
|
|
|
/**
|
|
* 开战:锁定圆心并启动独立段伤。
|
|
* @param ctx 表现上下文
|
|
* @param host Skill 宿主
|
|
*/
|
|
start(ctx: SkillPresentationContext, host: Skill): void {
|
|
const hitCount = Math.max(0, Math.floor(Number(ctx.params.hitCount) || 0));
|
|
const intervalSec = Math.max(0, Number(ctx.params.hitIntervalSec) || 0);
|
|
const radius = Math.max(0, Number(ctx.params.aroundPrimaryRadius) || 0);
|
|
const wan = Number(ctx.params.damageWan);
|
|
if (!(hitCount > 0) || !(radius > 0) || !Number.isFinite(wan)) {
|
|
console.log('[ThrownCircle] atk_para 无效', ctx.params.skillId, ctx.params.atkPara);
|
|
return;
|
|
}
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
if (!session) {
|
|
console.log('[ThrownCircle] 无战斗会话', ctx.params.skillId);
|
|
return;
|
|
}
|
|
const primary = ctx.targets[0] ?? 0;
|
|
const lockId = primary > 0 ? primary : ctx.caster;
|
|
const center = this._logicPos(session, lockId) ?? this._logicPos(session, ctx.caster);
|
|
if (!center) {
|
|
console.log('[ThrownCircle] 无落点', ctx.params.skillId);
|
|
return;
|
|
}
|
|
const scale = wan / 10000;
|
|
const shadow = buildShadowDealContext(
|
|
ctx,
|
|
String(ctx.params.buff1 ?? ''),
|
|
String(ctx.params.buff2 ?? ''),
|
|
);
|
|
const fly = this._throwFlyStick(host, ctx.caster, lockId, center, radius);
|
|
this._runs.push({
|
|
caster: ctx.caster,
|
|
ctx: shadow,
|
|
x: center.x,
|
|
y: center.y,
|
|
radius,
|
|
scale,
|
|
hitCount,
|
|
intervalSec,
|
|
done: 0,
|
|
wait: fly?.duration ?? 0,
|
|
flyNode: fly?.node ?? null,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 推进独立段伤。
|
|
* @param dt 逻辑秒
|
|
*/
|
|
tick(dt: number): void {
|
|
if (!(dt > 0) || this._runs.length <= 0) return;
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
for (let i = this._runs.length - 1; i >= 0; i--) {
|
|
const run = this._runs[i];
|
|
if (!session || !session.world.isAlive(run.caster) || !run.ctx.isAlive) {
|
|
this._drop(run);
|
|
this._runs.splice(i, 1);
|
|
continue;
|
|
}
|
|
run.wait -= dt;
|
|
while (run.done < run.hitCount && run.wait <= 0) {
|
|
this._pulse(session, run);
|
|
run.done += 1;
|
|
run.wait += run.intervalSec;
|
|
}
|
|
if (run.done >= run.hitCount) {
|
|
this._drop(run);
|
|
this._runs.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 战斗结束清空 */
|
|
abortAll(): void {
|
|
for (let i = 0; i < this._runs.length; i++) this._drop(this._runs[i]);
|
|
this._runs.length = 0;
|
|
}
|
|
|
|
private _pulse(session: NonNullable<ReturnType<typeof SkillPresentationEnv.getBattleSession>>, run: ThrownCircleRun): void {
|
|
this._activePulse = run;
|
|
pulseHostileCircle(session, run.caster, run.x, run.y, run.radius, this._onPulseHit);
|
|
this._activePulse = null;
|
|
}
|
|
|
|
private _activePulse: ThrownCircleRun | null = null;
|
|
|
|
private readonly _onPulseHit = (id: EntityId): void => {
|
|
const run = this._activePulse;
|
|
if (!run) return;
|
|
run.ctx.dealDamage(id, { scale: run.scale, damageType: DMG_TYPE });
|
|
};
|
|
|
|
/**
|
|
* 实例化飞棍飞向锁定怪落点,缩放对齐圈伤半径。
|
|
* @param host Skill 宿主
|
|
* @param caster 施法者
|
|
* @param lockId 锁定目标实体(主目标优先)
|
|
* @param center 圈伤逻辑坐标
|
|
* @param radius 圈伤半径像素
|
|
*/
|
|
private _throwFlyStick(host: Skill, caster: EntityId, lockId: EntityId, center: { x: number; y: number }, radius: number): ThrowFlyResult | null {
|
|
const flyLayer = SkillPresentationEnv.flyLayer;
|
|
if (!flyLayer?.isValid) {
|
|
console.log('[ThrownCircle] 无飞行层');
|
|
return null;
|
|
}
|
|
let node: Node | null = null;
|
|
try {
|
|
node = gg.res.getNode(GPath.HeroSkillPrefab(), FLY_STICK_PREFAB, GBundle.BattleHero);
|
|
} catch (e) {
|
|
console.log('[ThrownCircle] 飞棍加载异常', String(e));
|
|
return null;
|
|
}
|
|
if (!node?.isValid) {
|
|
console.log('[ThrownCircle] 飞棍预制体未加载', FLY_STICK_PREFAB);
|
|
return null;
|
|
}
|
|
const ut = flyLayer.getComponent(UITransform);
|
|
if (!ut) {
|
|
gg.res.putNode(node);
|
|
return null;
|
|
}
|
|
const startWorld = this._resolveThrowStartWorld(host, caster);
|
|
if (!startWorld) {
|
|
console.log('[ThrownCircle] 无飞出起点', caster);
|
|
gg.res.putNode(node);
|
|
return null;
|
|
}
|
|
const targetWorld = this._resolveThrowTargetWorld(lockId, center, startWorld.z);
|
|
if (!targetWorld) {
|
|
console.log('[ThrownCircle] 无飞向落点', lockId);
|
|
gg.res.putNode(node);
|
|
return null;
|
|
}
|
|
const visualScale = Math.max(0.05, radius / FLY_STICK_DESIGN_RADIUS);
|
|
flyLayer.active = true;
|
|
node.parent = flyLayer;
|
|
node.active = true;
|
|
node.layer = flyLayer.layer;
|
|
node.setScale(visualScale, visualScale, 1);
|
|
const startLocal = ut.convertToNodeSpaceAR(startWorld);
|
|
const targetLocal = ut.convertToNodeSpaceAR(targetWorld);
|
|
node.setPosition(startLocal);
|
|
node.setSiblingIndex(flyLayer.children.length - 1);
|
|
this._playFlyStickAnim(node);
|
|
const dist = Vec3.distance(startWorld, targetWorld);
|
|
const duration = Math.max(0.06, dist / FLY_SPEED);
|
|
SkillTransformDriver.animate({
|
|
node,
|
|
duration,
|
|
fromPos: startLocal,
|
|
toPos: targetLocal,
|
|
easing: SkillTransformDriver.easeSineInOut,
|
|
});
|
|
return { node, duration };
|
|
}
|
|
|
|
/**
|
|
* 飞出起点:优先武器挂点,否则施法者格心。
|
|
* @param host Skill 宿主
|
|
* @param caster 施法者
|
|
*/
|
|
private _resolveThrowStartWorld(host: Skill, caster: EntityId): Vec3 | null {
|
|
host.visualChannel.ensureWeapon();
|
|
const weaponRoot = host.visualChannel.weapon?.root;
|
|
if (weaponRoot?.isValid) return weaponRoot.worldPosition.clone();
|
|
const grid = SkillPresentationEnv.getCasterGridCenter(caster);
|
|
return grid ? new Vec3(grid.x, grid.y, grid.z) : null;
|
|
}
|
|
|
|
/**
|
|
* 飞向落点:锁定怪 Cocos 世界坐标(禁止 logicX/-logicY 绝对取反)。
|
|
* @param lockId 锁定目标
|
|
* @param center 圈伤逻辑坐标(无怪节点时回退)
|
|
* @param z 与起点同 Z
|
|
*/
|
|
private _resolveThrowTargetWorld(lockId: EntityId, center: { x: number; y: number }, z: number): Vec3 | null {
|
|
const fromMonster = SkillPresentationEnv.getTargetWorldPos(lockId);
|
|
if (fromMonster) {
|
|
fromMonster.z = z;
|
|
return fromMonster;
|
|
}
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
const map = SkillPresentationEnv.getMap();
|
|
if (!session || !map) return null;
|
|
const tf = session.world.getComponent(lockId, TransformComp);
|
|
if (!tf) return null;
|
|
const cocos = logicalWorldToCocos(session, map, tf.row, tf.col, center.x, center.y);
|
|
cocos.z = z;
|
|
return cocos;
|
|
}
|
|
|
|
/**
|
|
* 播飞棍旋转动画。
|
|
* @param node 飞棍根
|
|
*/
|
|
private _playFlyStickAnim(node: Node): void {
|
|
const sk = node.getChildByName('anim')?.getComponent(sp.Skeleton)
|
|
?? node.getComponentInChildren(sp.Skeleton);
|
|
if (!sk?.isValid) {
|
|
console.log('[ThrownCircle] 飞棍缺 Spine', FLY_STICK_PREFAB);
|
|
return;
|
|
}
|
|
sk.enabled = true;
|
|
sk.paused = false;
|
|
if (!BattleSpinePlayback.play(sk, FLY_STICK_ANIM, { loop: true })) {
|
|
console.log('[ThrownCircle] 飞棍无动画', FLY_STICK_ANIM);
|
|
}
|
|
}
|
|
|
|
private _drop(run: ThrownCircleRun): void {
|
|
const node = run.flyNode;
|
|
run.flyNode = null;
|
|
if (!node?.isValid) return;
|
|
SkillTransformDriver.cancelNode(node);
|
|
const sk = node.getChildByName('anim')?.getComponent(sp.Skeleton)
|
|
?? node.getComponentInChildren(sp.Skeleton);
|
|
BattleSpinePlayback.clearPendingComplete(sk);
|
|
BattleSpinePlayback.resetTracks(sk);
|
|
if (node['resKey']) gg.res.putNode(node);
|
|
else if (isValid(node)) node.destroy();
|
|
const flyLayer = SkillPresentationEnv.flyLayer;
|
|
if (flyLayer?.isValid && flyLayer.children.length === 0) flyLayer.active = false;
|
|
}
|
|
|
|
private _logicPos(session: NonNullable<ReturnType<typeof SkillPresentationEnv.getBattleSession>>, entity: EntityId): { x: number; y: number } | null {
|
|
return logicPosOf(session, entity);
|
|
}
|
|
}
|
|
|
|
const GROUND_DMG_TYPE = 'ground_circle';
|
|
/** battleHero/prefab/skill/暴风雪 */
|
|
const BLIZZARD_PREFAB = '暴风雪';
|
|
/**
|
|
* 暴风雪预制体 UITransform 边长 400:scale=1 时视觉半径 200。
|
|
* 落地缩放 = atk_para 半径 / 该值。
|
|
*/
|
|
const BLIZZARD_DESIGN_RADIUS = 200;
|
|
|
|
/** atk_type=20 地面圈伤任务 */
|
|
interface GroundCircleRun {
|
|
/** 施法者 */
|
|
caster: EntityId;
|
|
/** 影子伤害上下文 */
|
|
ctx: SkillPresentationContext;
|
|
/** Skill 宿主(占锁收招) */
|
|
host: Skill | null;
|
|
/** 播控序号 */
|
|
seq: number;
|
|
/** 圆心逻辑 X */
|
|
x: number;
|
|
/** 圆心逻辑 Y */
|
|
y: number;
|
|
/** 半径像素 */
|
|
radius: number;
|
|
/** 伤害倍率 */
|
|
scale: number;
|
|
/** 总段数 */
|
|
hitCount: number;
|
|
/** 段间隔秒 */
|
|
intervalSec: number;
|
|
/** 已结算段数 */
|
|
done: number;
|
|
/** 距下一段剩余秒 */
|
|
wait: number;
|
|
/** 暴风雪节点 */
|
|
fxNode: Node | null;
|
|
}
|
|
|
|
let _groundHost: GroundCircleHost | null = null;
|
|
|
|
/** atk_type=20 目标点地面圈伤宿主 */
|
|
export function getGroundCircleHost(): GroundCircleHost {
|
|
if (!_groundHost) _groundHost = new GroundCircleHost();
|
|
return _groundHost;
|
|
}
|
|
|
|
/**
|
|
* atk_type=20:以主目标为圆心放置暴风雪(预制体默认循环),按段独立圈伤;占锁至结束。
|
|
*/
|
|
export class GroundCircleHost {
|
|
private readonly _runs: GroundCircleRun[] = [];
|
|
private _activePulse: GroundCircleRun | null = null;
|
|
|
|
/**
|
|
* 开战:锁主目标圆心并放置暴风雪。
|
|
* @param ctx 表现上下文
|
|
* @param host Skill 宿主
|
|
* @param seq 播控序号
|
|
*/
|
|
start(ctx: SkillPresentationContext, host: Skill, seq: number): void {
|
|
const hitCount = Math.max(0, Math.floor(Number(ctx.params.hitCount) || 0));
|
|
const intervalSec = Math.max(0, Number(ctx.params.hitIntervalSec) || 0);
|
|
const radius = Math.max(0, Number(ctx.params.aroundPrimaryRadius) || 0);
|
|
const wan = Number(ctx.params.damageWan);
|
|
if (!(hitCount > 0) || !(radius > 0) || !Number.isFinite(wan)) {
|
|
console.log('[GroundCircle] atk_para 无效', ctx.params.skillId, ctx.params.atkPara);
|
|
this._finishCast(host, seq);
|
|
return;
|
|
}
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
if (!session) {
|
|
console.log('[GroundCircle] 无战斗会话', ctx.params.skillId);
|
|
this._finishCast(host, seq);
|
|
return;
|
|
}
|
|
const primary = ctx.targets[0] ?? 0;
|
|
if (!(primary > 0)) {
|
|
console.log('[GroundCircle] 无索敌目标', ctx.params.skillId);
|
|
this._finishCast(host, seq);
|
|
return;
|
|
}
|
|
const center = logicPosOf(session, primary);
|
|
if (!center) {
|
|
console.log('[GroundCircle] 无落点', ctx.params.skillId, primary);
|
|
this._finishCast(host, seq);
|
|
return;
|
|
}
|
|
const fxNode = this._placeBlizzard(primary, center, radius);
|
|
this._runs.push({
|
|
caster: ctx.caster,
|
|
ctx: buildShadowDealContext(ctx, String(ctx.params.buff1 ?? ''), String(ctx.params.buff2 ?? '')),
|
|
host,
|
|
seq,
|
|
x: center.x,
|
|
y: center.y,
|
|
radius,
|
|
scale: wan / 10000,
|
|
hitCount,
|
|
intervalSec,
|
|
done: 0,
|
|
wait: 0,
|
|
fxNode,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 推进独立段伤。
|
|
* @param dt 逻辑秒
|
|
*/
|
|
tick(dt: number): void {
|
|
if (!(dt > 0) || this._runs.length <= 0) return;
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
for (let i = this._runs.length - 1; i >= 0; i--) {
|
|
const run = this._runs[i];
|
|
if (!session || !session.world.isAlive(run.caster) || !run.ctx.isAlive) {
|
|
this._endRun(run);
|
|
this._runs.splice(i, 1);
|
|
continue;
|
|
}
|
|
if (run.host && !run.host.isPresentationCast(run.seq)) {
|
|
this._drop(run);
|
|
this._runs.splice(i, 1);
|
|
continue;
|
|
}
|
|
run.wait -= dt;
|
|
while (run.done < run.hitCount && run.wait <= 0) {
|
|
this._pulse(session, run);
|
|
run.done += 1;
|
|
run.wait += run.intervalSec;
|
|
}
|
|
if (run.done >= run.hitCount) {
|
|
this._endRun(run);
|
|
this._runs.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 打断指定施法者的暴风雪。
|
|
* @param caster 施法实体
|
|
*/
|
|
abortCaster(caster: EntityId): void {
|
|
for (let i = this._runs.length - 1; i >= 0; i--) {
|
|
if (this._runs[i].caster !== caster) continue;
|
|
this._drop(this._runs[i]);
|
|
this._runs.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/** 战斗结束清空 */
|
|
abortAll(): void {
|
|
for (let i = 0; i < this._runs.length; i++) this._drop(this._runs[i]);
|
|
this._runs.length = 0;
|
|
}
|
|
|
|
/**
|
|
* 收招并结束播控。
|
|
* @param host Skill 宿主
|
|
* @param seq 播控序号
|
|
*/
|
|
private _finishCast(host: Skill | null, seq: number): void {
|
|
if (!host?.isValid) return;
|
|
host.visualChannel.endHoldCast();
|
|
if (host.isPresentationCast(seq)) host.finishPresentationCast(seq, false);
|
|
}
|
|
|
|
private _endRun(run: GroundCircleRun): void {
|
|
const host = run.host;
|
|
const seq = run.seq;
|
|
run.host = null;
|
|
this._drop(run);
|
|
this._finishCast(host, seq);
|
|
}
|
|
|
|
private _pulse(session: NonNullable<ReturnType<typeof SkillPresentationEnv.getBattleSession>>, run: GroundCircleRun): void {
|
|
this._activePulse = run;
|
|
pulseHostileCircle(session, run.caster, run.x, run.y, run.radius, this._onPulseHit);
|
|
this._activePulse = null;
|
|
}
|
|
|
|
private readonly _onPulseHit = (id: EntityId): void => {
|
|
const run = this._activePulse;
|
|
if (!run) return;
|
|
run.ctx.dealDamage(id, { scale: run.scale, damageType: GROUND_DMG_TYPE });
|
|
};
|
|
|
|
/**
|
|
* 在锁定怪位置放置暴风雪,缩放对齐圈伤半径;不强制播动画名,只恢复预制体默认循环。
|
|
* @param lockId 锁定目标
|
|
* @param center 圈伤逻辑坐标
|
|
* @param radius 圈伤半径像素
|
|
*/
|
|
private _placeBlizzard(lockId: EntityId, center: { x: number; y: number }, radius: number): Node | null {
|
|
const flyLayer = SkillPresentationEnv.flyLayer;
|
|
if (!flyLayer?.isValid) {
|
|
console.log('[GroundCircle] 无飞行层');
|
|
return null;
|
|
}
|
|
let node: Node | null = null;
|
|
try {
|
|
node = gg.res.getNode(GPath.HeroSkillPrefab(), BLIZZARD_PREFAB, GBundle.BattleHero);
|
|
} catch (e) {
|
|
console.log('[GroundCircle] 暴风雪加载异常', String(e));
|
|
return null;
|
|
}
|
|
if (!node?.isValid) {
|
|
console.log('[GroundCircle] 暴风雪预制体未加载', BLIZZARD_PREFAB);
|
|
return null;
|
|
}
|
|
const ut = flyLayer.getComponent(UITransform);
|
|
if (!ut) {
|
|
gg.res.putNode(node);
|
|
return null;
|
|
}
|
|
const world = this._resolvePlaceWorld(lockId, center);
|
|
if (!world) {
|
|
console.log('[GroundCircle] 无放置世界坐标', lockId);
|
|
gg.res.putNode(node);
|
|
return null;
|
|
}
|
|
const visualScale = Math.max(0.05, radius / BLIZZARD_DESIGN_RADIUS);
|
|
flyLayer.active = true;
|
|
node.parent = flyLayer;
|
|
node.active = true;
|
|
node.layer = flyLayer.layer;
|
|
node.setScale(visualScale, visualScale, 1);
|
|
node.setPosition(ut.convertToNodeSpaceAR(world));
|
|
node.setSiblingIndex(flyLayer.children.length - 1);
|
|
this._resumePrefabDefaultLoop(node);
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* 放置世界坐标:优先怪节点,否则逻辑坐标映射。
|
|
* @param lockId 锁定目标
|
|
* @param center 逻辑圆心
|
|
*/
|
|
private _resolvePlaceWorld(lockId: EntityId, center: { x: number; y: number }): Vec3 | null {
|
|
const fromMonster = SkillPresentationEnv.getTargetWorldPos(lockId);
|
|
if (fromMonster) return fromMonster;
|
|
const session = SkillPresentationEnv.getBattleSession();
|
|
const map = SkillPresentationEnv.getMap();
|
|
if (!session || !map) return null;
|
|
const tf = session.world.getComponent(lockId, TransformComp);
|
|
if (!tf) return null;
|
|
return logicalWorldToCocos(session, map, tf.row, tf.col, center.x, center.y);
|
|
}
|
|
|
|
/**
|
|
* 出池后恢复 Spine:只播预制体 defaultAnimation(循环),不硬编码技能动画名。
|
|
* @param root 暴风雪根
|
|
*/
|
|
private _resumePrefabDefaultLoop(root: Node): void {
|
|
const skins = root.getComponentsInChildren(sp.Skeleton);
|
|
for (let i = 0; i < skins.length; i++) {
|
|
const sk = skins[i];
|
|
if (!sk?.isValid) continue;
|
|
sk.enabled = true;
|
|
sk.paused = false;
|
|
const typed = sk as sp.Skeleton & { defaultAnimation?: string };
|
|
const name = String(typed.defaultAnimation || sk.animation || '').trim();
|
|
if (!name) continue;
|
|
if (!BattleSpinePlayback.play(sk, name, { loop: true })) {
|
|
console.log('[GroundCircle] 暴风雪默认动画失败', name);
|
|
}
|
|
}
|
|
}
|
|
|
|
private _drop(run: GroundCircleRun): void {
|
|
const node = run.fxNode;
|
|
run.fxNode = null;
|
|
if (!node?.isValid) return;
|
|
const skins = node.getComponentsInChildren(sp.Skeleton);
|
|
for (let i = 0; i < skins.length; i++) {
|
|
BattleSpinePlayback.clearPendingComplete(skins[i]);
|
|
BattleSpinePlayback.resetTracks(skins[i]);
|
|
}
|
|
if (node['resKey']) gg.res.putNode(node);
|
|
else if (isValid(node)) node.destroy();
|
|
const flyLayer = SkillPresentationEnv.flyLayer;
|
|
if (flyLayer?.isValid && flyLayer.children.length === 0) flyLayer.active = false;
|
|
}
|
|
}
|
|
|