import { director, Director, game, Node, UITransform } from 'cc'; import { createMembershipSet, membershipAdd } from '../../../mx/tools/MembershipSet'; import type { EntityId } from 'db://assets/mx/module/battle'; import { Monster } from '../../ui/BattleGame/Monster'; import { SkillPresentationEnv } from './SkillPresentationEnv'; import type { SkillPresentationContext } from './SkillPresentationTypes'; /** * @deprecated 新技能管线伤害归逻辑层,勿再以本类作为唯一命中源。 * 遗留穿透/转刀表现可暂时调用;新 SkillAbilityBuilder 技能应 ctx.hit()/finish()。 * * 历史说明:表现体 AABB ∩ 怪盒 → dealDamage。 */ /** 同 cast 对同怪只伤一次 */ export interface SkillCollisionOncePolicy { mode: 'once'; onDealDamage?: (entityId: EntityId) => void; scale?: number; damageType?: string; } /** 可穿透:总命中次数上限;同一 bind 周期内每怪只伤一次 */ export interface SkillCollisionPiercePolicy { mode: 'pierce'; maxHits?: number; /** @deprecated 用 dispose 后重新 bind 实现「去程/回程各伤一次」,勿设为 true(会取消去重) */ hitSameTarget?: boolean; alreadyHit?: EntityId[]; onDealDamage?: (entityId: EntityId) => void; scale?: number; damageType?: string; } /** 持续重叠按间隔重复伤害 */ export interface SkillCollisionIntervalPolicy { mode: 'interval'; intervalSec: number; onDealDamage?: (entityId: EntityId) => void; scale?: number; damageType?: string; } export type SkillCollisionPolicy = | SkillCollisionOncePolicy | SkillCollisionPiercePolicy | SkillCollisionIntervalPolicy; interface Aabb { minX: number; minY: number; maxX: number; maxY: number; } export class SkillCollisionHit { private static readonly _active: SkillCollisionHit[] = []; private static _hooked = false; private static _ensureHook(): void { if (SkillCollisionHit._hooked) return; SkillCollisionHit._hooked = true; director.on(Director.EVENT_AFTER_UPDATE, SkillCollisionHit._afterUpdate); } private static _afterUpdate(): void { if (SkillCollisionHit._active.length === 0) return; const battle = gg.game?.CurentBattle as { TimeScale?: number } | null | undefined; if ((battle?.TimeScale ?? 1) <= 0) return; for (let i = SkillCollisionHit._active.length - 1; i >= 0; i--) { SkillCollisionHit._active[i]._onAfterUpdate(); } } private static _register(hit: SkillCollisionHit): void { SkillCollisionHit._ensureHook(); if (SkillCollisionHit._active.indexOf(hit) < 0) { SkillCollisionHit._active.push(hit); } } private static _unregister(hit: SkillCollisionHit): void { const idx = SkillCollisionHit._active.indexOf(hit); if (idx >= 0) SkillCollisionHit._active.splice(idx, 1); } private _node: Node | null = null; private _ctx: SkillPresentationContext | null = null; private _policy: SkillCollisionPolicy | null = null; private _hitTargets = createMembershipSet(); private _hitCount = 0; private _overlapElapsed = new Map(); /** 本帧仍重叠的实体 */ private _overlapping = createMembershipSet(); /** 技能体世界 AABB */ private _selfAabb: Aabb = { minX: 0, minY: 0, maxX: 0, maxY: 0 }; /** 怪体世界 AABB */ private _otherAabb: Aabb = { minX: 0, minY: 0, maxX: 0, maxY: 0 }; private _disposed = false; bind(node: Node, ctx: SkillPresentationContext, policy: SkillCollisionPolicy): this { this.dispose(); this._disposed = false; this._node = node; this._ctx = ctx; this._policy = policy; this._hitTargets.clear(); this._hitCount = 0; this._overlapElapsed.clear(); if (policy.mode === 'pierce') { for (const id of policy.alreadyHit ?? []) { membershipAdd(this._hitTargets, id); } } SkillCollisionHit._register(this); this._onAfterUpdate(); return this; } dispose(): void { if (this._disposed && !this._node) return; this._disposed = true; SkillCollisionHit._unregister(this); this._node = null; this._ctx = null; this._policy = null; this._hitTargets.clear(); this._overlapElapsed.clear(); this._overlapping.clear(); this._hitCount = 0; } private _onAfterUpdate(): void { if (this._disposed) { SkillCollisionHit._unregister(this); return; } const node = this._node; const ctx = this._ctx; const policy = this._policy; if (!node?.isValid || !ctx?.isAlive || !policy) return; const selfBox = this._selfAabb; if (!fillWorldAabb(node, selfBox)) return; const overlapping = this._overlapping; overlapping.clear(); const otherBox = this._otherAabb; SkillPresentationEnv.forEachMonsterNode((entityId, monsterNode) => { if (!SkillPresentationEnv.isEntityAlive(entityId)) return; if (SkillPresentationEnv.isYanCaught(entityId)) return; if (SkillPresentationEnv.isBajiaoBlowRetreating(entityId)) return; const monster = monsterNode.getComponent(Monster); if (!monster || monster.IsDead) return; if (!fillWorldAabb(monsterNode, otherBox) || !aabbOverlap(selfBox, otherBox)) return; membershipAdd(overlapping, entityId); this._onOverlap(entityId, policy); }); if (policy.mode === 'interval') { this._overlapElapsed.forEach((_elapsed, id) => { if (!overlapping.has(id)) this._overlapElapsed.delete(id); }); const battle = gg.game?.CurentBattle as { TimeScale?: number } | null | undefined; const dt = (game.deltaTime || 1 / 60) * Math.max(0, battle?.TimeScale ?? 1); const interval = Math.max(0.01, policy.intervalSec); this._overlapElapsed.forEach((elapsed, entityId) => { if (!overlapping.has(entityId)) return; const next = elapsed + dt; if (next >= interval) { this._tryDeal(entityId, policy.scale, policy.damageType); this._overlapElapsed.set(entityId, next - interval); } else { this._overlapElapsed.set(entityId, next); } }); } } private _onOverlap(entityId: EntityId, policy: SkillCollisionPolicy): void { if (policy.mode === 'interval') { if (!this._overlapElapsed.has(entityId)) { this._overlapElapsed.set(entityId, 0); this._tryDeal(entityId, policy.scale, policy.damageType); } return; } if (policy.mode === 'once') { if (this._hitTargets.has(entityId)) return; if (this._tryDeal(entityId, policy.scale, policy.damageType)) { membershipAdd(this._hitTargets, entityId); } return; } // pierce:同一 bind 周期内每怪只伤一次(回旋回程须 dispose 后重新 bind) if (policy.maxHits != null && this._hitCount >= policy.maxHits) return; if (this._hitTargets.has(entityId)) return; if (this._tryDeal(entityId, policy.scale, policy.damageType)) { membershipAdd(this._hitTargets, entityId); this._hitCount++; } } private _tryDeal( entityId: EntityId, scale?: number, damageType?: string, ): boolean { const ctx = this._ctx; const policy = this._policy; if (!ctx?.isAlive) return false; const ok = ctx.dealDamage(entityId, { scale, damageType }); if (ok) policy?.onDealDamage?.(entityId); return ok; } } function aabbOverlap(a: Aabb, b: Aabb): boolean { return a.minX <= b.maxX && a.maxX >= b.minX && a.minY <= b.maxY && a.maxY >= b.minY; } /** * 写入节点 UITransform 世界 AABB;无变换则用点扩 40。 * @param node 节点 * @param out 输出 * @returns 节点无效时 false */ function fillWorldAabb(node: Node, out: Aabb): boolean { if (!node?.isValid) return false; const ut = node.getComponent(UITransform); if (!ut) { const p = node.worldPosition; out.minX = p.x - 40; out.minY = p.y - 40; out.maxX = p.x + 40; out.maxY = p.y + 40; return true; } const rect = ut.getBoundingBoxToWorld(); out.minX = rect.x; out.minY = rect.y; out.maxX = rect.x + rect.width; out.maxY = rect.y + rect.height; return true; }