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