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.
217 lines
7.9 KiB
217 lines
7.9 KiB
import { instantiate, isValid, Node, UITransform, Vec3 } 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 type { Skill } from '../skill/Skill';
|
|
import { SkillPresentationEnv } from '../skill/SkillPresentationEnv';
|
|
import type { SkillPresentationContext } from '../skill/SkillPresentationTypes';
|
|
import { SkillTransformDriver } from '../skill/SkillTransformDriver';
|
|
import { buildShadowDealContext } from './SkillShadowContext';
|
|
|
|
const DMG_TYPE = 'thrown_circle';
|
|
const FLY_SPEED = 576;
|
|
|
|
/** 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;
|
|
}
|
|
|
|
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 center = this._logicPos(session, primary > 0 ? primary : ctx.caster)
|
|
?? 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 flyNode = this._tryThrowWeapon(host, center);
|
|
this._runs.push({
|
|
caster: ctx.caster,
|
|
ctx: shadow,
|
|
x: center.x,
|
|
y: center.y,
|
|
radius,
|
|
scale,
|
|
hitCount,
|
|
intervalSec,
|
|
done: 0,
|
|
wait: 0,
|
|
flyNode,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 推进独立段伤。
|
|
* @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 {
|
|
const casterTeam = session.world.getComponent(run.caster, TeamComp);
|
|
const cell = session.grid.worldToCell(run.x, run.y);
|
|
const radiusCells = run.radius / Math.max(1, session.grid.cellSize);
|
|
const ids = session.spatial.queryRadius(cell, radiusCells + 1);
|
|
const r2 = run.radius * run.radius;
|
|
for (let i = 0; i < ids.length; i++) {
|
|
const id = ids[i];
|
|
if (id === run.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 = this._logicPos(session, id);
|
|
if (!pos) continue;
|
|
const dx = pos.x - run.x;
|
|
const dy = pos.y - run.y;
|
|
if (dx * dx + dy * dy > r2) continue;
|
|
run.ctx.dealDamage(id, { scale: run.scale, damageType: DMG_TYPE });
|
|
}
|
|
}
|
|
|
|
private _tryThrowWeapon(host: Skill, center: { x: number; y: number }): Node | null {
|
|
const flyLayer = SkillPresentationEnv.flyLayer;
|
|
host.visualChannel.ensureWeapon();
|
|
const fromNode = host.visualChannel.weapon?.root;
|
|
if (!flyLayer?.isValid || !fromNode?.isValid) return null;
|
|
const skin = fromNode.getChildByName('skin') ?? fromNode;
|
|
if (!skin?.isValid) return null;
|
|
const clone = instantiate(skin);
|
|
if (!clone?.isValid) return null;
|
|
const ut = flyLayer.getComponent(UITransform);
|
|
if (!ut) {
|
|
clone.destroy();
|
|
return null;
|
|
}
|
|
flyLayer.active = true;
|
|
clone.parent = flyLayer;
|
|
const startWorld = skin.worldPosition.clone();
|
|
const targetWorld = new Vec3(center.x, -center.y, startWorld.z);
|
|
const startLocal = ut.convertToNodeSpaceAR(startWorld);
|
|
const targetLocal = ut.convertToNodeSpaceAR(targetWorld);
|
|
clone.setPosition(startLocal);
|
|
clone.setWorldScale(skin.worldScale);
|
|
const dist = Vec3.distance(startWorld, targetWorld);
|
|
const dur = Math.max(0.06, dist / FLY_SPEED);
|
|
SkillTransformDriver.animate({
|
|
node: clone,
|
|
duration: dur,
|
|
fromPos: startLocal,
|
|
toPos: targetLocal,
|
|
easing: SkillTransformDriver.easeSineInOut,
|
|
});
|
|
return clone;
|
|
}
|
|
|
|
private _drop(run: ThrownCircleRun): void {
|
|
const node = run.flyNode;
|
|
run.flyNode = null;
|
|
if (!node?.isValid) return;
|
|
SkillTransformDriver.cancelNode(node);
|
|
if (isValid(node)) node.destroy();
|
|
}
|
|
|
|
private _logicPos(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 });
|
|
}
|
|
}
|
|
|