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/ThrownCircleAtkPresenter.ts

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 });
}
}