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/skill/SkillCollisionHit.ts

247 lines
8.7 KiB

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<EntityId>();
private _hitCount = 0;
private _overlapElapsed = new Map<EntityId, number>();
/** 本帧仍重叠的实体 */
private _overlapping = createMembershipSet<EntityId>();
/** 技能体世界 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;
}