|
|
|
|
import {
|
|
|
|
|
_decorator,
|
|
|
|
|
CCFloat,
|
|
|
|
|
CircleCollider2D,
|
|
|
|
|
Collider2D,
|
|
|
|
|
Component,
|
|
|
|
|
Contact2DType,
|
|
|
|
|
Director,
|
|
|
|
|
director,
|
|
|
|
|
ERigidBody2DType,
|
|
|
|
|
IPhysics2DContact,
|
|
|
|
|
Node,
|
|
|
|
|
Rect,
|
|
|
|
|
RigidBody2D,
|
|
|
|
|
sp,
|
|
|
|
|
v2,
|
|
|
|
|
v3,
|
|
|
|
|
Vec2,
|
|
|
|
|
Vec3,
|
|
|
|
|
} from 'cc';
|
|
|
|
|
import { Monster } from './Monster';
|
|
|
|
|
import { tetriNode } from '../../tetriCard/tetriNode';
|
|
|
|
|
import { GBundle } from '../../game/ConfigRes';
|
|
|
|
|
import { tetriFloorNode } from '../../tetriCard/tetriFloorNode';
|
|
|
|
|
import { BattlePerformance } from './BattlePerformance';
|
|
|
|
|
const { ccclass, property } = _decorator;
|
|
|
|
|
|
|
|
|
|
/** 相对原拱高再抬高 30%(控制点更高) */
|
|
|
|
|
const ARC_PEAK_HEIGHT_MULT = 1.3;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 与 settings/v2/packages/project.json 中 physics.collisionGroups 的 index 10「bossSkill」一致。
|
|
|
|
|
* 预制体里 CircleCollider2D 常为 1024,但 RigidBody2D 若误为 2(monster)则与 card 无碰撞,球会穿方块下落。
|
|
|
|
|
*/
|
|
|
|
|
const PHYSICS_GROUP_BOSS_SKILL = 1 << 10;
|
|
|
|
|
|
|
|
|
|
@ccclass('BossSkill')
|
|
|
|
|
export class BossSkill extends Component {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@property(Vec3)
|
|
|
|
|
offset: Vec3 = v3(0, 0, 0);
|
|
|
|
|
|
|
|
|
|
/** 抛物线飞行总时长(秒) */
|
|
|
|
|
@property(CCFloat)
|
|
|
|
|
flyDuration: number = 0.75;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 拱高相对水平位移的比例,越大弧越高。
|
|
|
|
|
* 实际拱高 = max(arcMinHeight, 水平距离 * arcHeightRatio)
|
|
|
|
|
*/
|
|
|
|
|
@property(CCFloat)
|
|
|
|
|
arcHeightRatio: number = 0.38;
|
|
|
|
|
|
|
|
|
|
/** 拱高下限(像素),避免很近的目标弧太平 */
|
|
|
|
|
@property(CCFloat)
|
|
|
|
|
arcMinHeight: number = 80;
|
|
|
|
|
|
|
|
|
|
/** 是否根据速度方向旋转节点(2D 常用贴图朝上为默认朝向) */
|
|
|
|
|
@property
|
|
|
|
|
orientToVelocity: boolean = true;
|
|
|
|
|
|
|
|
|
|
/** 与 atan2 结果叠加的转角修正(度),贴图朝 +Y 时常用 -90 */
|
|
|
|
|
@property(CCFloat)
|
|
|
|
|
velocityAngleOffsetDeg: number = -90;
|
|
|
|
|
|
|
|
|
|
monster: Monster;
|
|
|
|
|
|
|
|
|
|
/*爆炸延迟时间 **/
|
|
|
|
|
bombDelayTime: number = 0;
|
|
|
|
|
/*冲击力 **/
|
|
|
|
|
bombForce: number = 0;
|
|
|
|
|
/*爆炸范围 **/
|
|
|
|
|
bombRange: number = 0;
|
|
|
|
|
|
|
|
|
|
targetPos: Vec3;
|
|
|
|
|
|
|
|
|
|
isFly: boolean = false;
|
|
|
|
|
|
|
|
|
|
private readonly _flyStart = new Vec3();
|
|
|
|
|
private readonly _flyEnd = new Vec3();
|
|
|
|
|
private _flyT = 0;
|
|
|
|
|
private _arcPeak = 0;
|
|
|
|
|
/** 抛物线结束后是否已切回动态刚体(避免重复调用) */
|
|
|
|
|
private _physicsRestored = false;
|
|
|
|
|
/** 是否已排队延迟恢复刚体(避免物理回调内改 type 报错) */
|
|
|
|
|
private _restorePhysicsDeferredPending = false;
|
|
|
|
|
|
|
|
|
|
collider: Collider2D;
|
|
|
|
|
private _rigidBody: RigidBody2D | null = null;
|
|
|
|
|
|
|
|
|
|
/** 上一帧世界坐标:用于 Kinematic 瞬移时的扫段检测,避免薄/悬浮块隧道效应导致 BEGIN_CONTACT 过晚 */
|
|
|
|
|
private readonly _flyPrevWorld = new Vec3();
|
|
|
|
|
|
|
|
|
|
/** 碰砖冲量:延后到本步物理解算之后执行(避免在 contact 回调里直接改冲量) */
|
|
|
|
|
private _brickImpulsePending = false;
|
|
|
|
|
private _brickImpulseCollider: Collider2D | null = null;
|
|
|
|
|
private _brickImpulseContact: IPhysics2DContact | null = null;
|
|
|
|
|
|
|
|
|
|
protected onLoad(): void {
|
|
|
|
|
this.collider = this.getComponent(Collider2D);
|
|
|
|
|
this._rigidBody = this.node.getComponent(RigidBody2D);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected onEnable(): void {
|
|
|
|
|
if (this.collider) {
|
|
|
|
|
this.collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected onDisable(): void {
|
|
|
|
|
director.off(Director.EVENT_AFTER_PHYSICS, this._deferredRestoreDynamicPhysics, this);
|
|
|
|
|
director.off(Director.EVENT_AFTER_PHYSICS, this._applyDeferredBrickImpulse, this);
|
|
|
|
|
this.unschedule(this._deferredRestoreDynamicPhysics);
|
|
|
|
|
this._restorePhysicsDeferredPending = false;
|
|
|
|
|
this._brickImpulsePending = false;
|
|
|
|
|
this._brickImpulseCollider = null;
|
|
|
|
|
this._brickImpulseContact = null;
|
|
|
|
|
if (this.collider) {
|
|
|
|
|
this.collider.off(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setData(monster: Monster, targetPos: Vec3) {
|
|
|
|
|
this.monster = monster;
|
|
|
|
|
this.bombDelayTime = Number(monster.Data.skillpar[0]);
|
|
|
|
|
this.bombForce = Number(monster.Data.skillpar[1]);
|
|
|
|
|
this.bombRange = Number(monster.Data.skillpar[2]);
|
|
|
|
|
this.targetPos = v3(targetPos.x + this.offset.x, targetPos.y + this.offset.y, targetPos.z + this.offset.z);
|
|
|
|
|
|
|
|
|
|
// 与方块、地板同属 GameRoot 子树,保证与 card/floor 的 Box2D 碰撞在同一世界里解算;
|
|
|
|
|
// 挂在 ui_skilllayer 下时常见表现为穿透堆叠与地板。
|
|
|
|
|
this._reparentToGameRootForPhysics();
|
|
|
|
|
this.startFly();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 挂到俄罗斯地图 GameRoot,并对齐渲染层,避免与 TetrisTable 物理隔离 */
|
|
|
|
|
private _reparentToGameRootForPhysics(): void {
|
|
|
|
|
const gr = gg.game.CurentBattle?.TetraMap?._gameRoot;
|
|
|
|
|
if (!gr?.isValid || this.node.parent === gr) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.node.setParent(gr, true);
|
|
|
|
|
this.node.layer = gr.layer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected update(dt: number): void {
|
|
|
|
|
if (!this.isFly) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const dur = this.flyDuration > 1e-4 ? this.flyDuration : 1e-4;
|
|
|
|
|
this._flyT += dt / dur;
|
|
|
|
|
const t = Math.min(1, this._flyT);
|
|
|
|
|
// 竖直抛物线叠加:t=0,1 时为 0,t=0.5 时最高
|
|
|
|
|
const arc = 4 * t * (1 - t) * this._arcPeak;
|
|
|
|
|
const wx = this._flyStart.x + (this._flyEnd.x - this._flyStart.x) * t;
|
|
|
|
|
const wy = this._flyStart.y + (this._flyEnd.y - this._flyStart.y) * t + arc;
|
|
|
|
|
const wz = this._flyStart.z + (this._flyEnd.z - this._flyStart.z) * t;
|
|
|
|
|
|
|
|
|
|
const px = this._flyPrevWorld.x;
|
|
|
|
|
const py = this._flyPrevWorld.y;
|
|
|
|
|
const pz = this._flyPrevWorld.z;
|
|
|
|
|
const sweptHit = this._sweptHitTetriBrickBetween(px, py, pz, wx, wy, wz);
|
|
|
|
|
if (sweptHit) {
|
|
|
|
|
this._queueBrickImpulse(sweptHit, null);
|
|
|
|
|
this._stopFlyAtCurrentWorldPos();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.node.setWorldPosition(wx, wy, wz);
|
|
|
|
|
this._flyPrevWorld.set(wx, wy, wz);
|
|
|
|
|
|
|
|
|
|
if (this.orientToVelocity && t < 1) {
|
|
|
|
|
const dArc = 4 * this._arcPeak * (1 - 2 * t);
|
|
|
|
|
const vx = this._flyEnd.x - this._flyStart.x;
|
|
|
|
|
const vy = this._flyEnd.y - this._flyStart.y + dArc;
|
|
|
|
|
this.node.angle = (Math.atan2(vy, vx) * 180) / Math.PI + this.velocityAngleOffsetDeg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (t >= 1) {
|
|
|
|
|
this.node.setWorldPosition(this._flyEnd);
|
|
|
|
|
this._flyPrevWorld.set(this._flyEnd);
|
|
|
|
|
this.isFly = false;
|
|
|
|
|
this._scheduleRestoreDynamicPhysicsForRoll();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 炸弹碰撞圆的世界半径(近似) */
|
|
|
|
|
private _getBombWorldRadius(): number {
|
|
|
|
|
const c = this.collider;
|
|
|
|
|
if (c instanceof CircleCollider2D) {
|
|
|
|
|
const sx = Math.abs(c.node.worldScale.x);
|
|
|
|
|
const sy = Math.abs(c.node.worldScale.y);
|
|
|
|
|
const s = Math.max(sx, sy, 1e-4);
|
|
|
|
|
return c.radius * s;
|
|
|
|
|
}
|
|
|
|
|
const aabb = c?.worldAABB;
|
|
|
|
|
if (aabb) {
|
|
|
|
|
return Math.max(aabb.width, aabb.height) * 0.5;
|
|
|
|
|
}
|
|
|
|
|
return 18;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _circleIntersectsRect(cx: number, cy: number, r: number, rect: Rect): boolean {
|
|
|
|
|
const closestX = Math.min(Math.max(cx, rect.xMin), rect.xMax);
|
|
|
|
|
const closestY = Math.min(Math.max(cy, rect.yMin), rect.yMax);
|
|
|
|
|
const dx = cx - closestX;
|
|
|
|
|
const dy = cy - closestY;
|
|
|
|
|
return dx * dx + dy * dy <= r * r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 本帧位移线段上是否穿过「场上俄罗斯块」的任一 2D 碰撞体 AABB(与物理 BEGIN_CONTACT 互补,避免 Kinematic 瞬移隧道)。
|
|
|
|
|
* 命中时把节点放在线段上首次命中的位置并返回该碰撞体;未命中返回 null。
|
|
|
|
|
*/
|
|
|
|
|
private _sweptHitTetriBrickBetween(
|
|
|
|
|
fx: number,
|
|
|
|
|
fy: number,
|
|
|
|
|
fz: number,
|
|
|
|
|
tx: number,
|
|
|
|
|
ty: number,
|
|
|
|
|
tz: number,
|
|
|
|
|
): Collider2D | null {
|
|
|
|
|
const table = gg.game.CurentBattle?.TetraMap?._tetrisTable;
|
|
|
|
|
if (!table?.isValid) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const r = this._getBombWorldRadius();
|
|
|
|
|
const dx = tx - fx;
|
|
|
|
|
const dy = ty - fy;
|
|
|
|
|
const len = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
|
if (len < 0.5) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const stepPx = Math.max(r * 0.4, 5);
|
|
|
|
|
const steps = Math.min(40, Math.max(4, Math.ceil(len / stepPx)));
|
|
|
|
|
const roots = table.children;
|
|
|
|
|
for (let si = 1; si <= steps; si++) {
|
|
|
|
|
const u = si / steps;
|
|
|
|
|
const cx = fx + dx * u;
|
|
|
|
|
const cy = fy + dy * u;
|
|
|
|
|
const cz = fz + (tz - fz) * u;
|
|
|
|
|
for (let i = 0; i < roots.length; i++) {
|
|
|
|
|
const root = roots[i];
|
|
|
|
|
if (!root?.isValid) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!root.getComponent(tetriNode) && !root.getComponent(tetriFloorNode)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const cols = root.getComponentsInChildren(Collider2D) ?? [];
|
|
|
|
|
for (let j = 0; j < cols.length; j++) {
|
|
|
|
|
const col = cols[j];
|
|
|
|
|
if (!col?.enabled || !col.node?.isValid) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const aabb = col.worldAABB;
|
|
|
|
|
if (!aabb) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (this._circleIntersectsRect(cx, cy, r, aabb)) {
|
|
|
|
|
this.node.setWorldPosition(cx, cy, cz);
|
|
|
|
|
return col;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startFly() {
|
|
|
|
|
director.off(Director.EVENT_AFTER_PHYSICS, this._deferredRestoreDynamicPhysics, this);
|
|
|
|
|
director.off(Director.EVENT_AFTER_PHYSICS, this._applyDeferredBrickImpulse, this);
|
|
|
|
|
this.unschedule(this._deferredRestoreDynamicPhysics);
|
|
|
|
|
this._brickImpulsePending = false;
|
|
|
|
|
this._brickImpulseCollider = null;
|
|
|
|
|
this._brickImpulseContact = null;
|
|
|
|
|
this._physicsRestored = false;
|
|
|
|
|
this._restorePhysicsDeferredPending = false;
|
|
|
|
|
this._flyStart.set(this.node.worldPosition);
|
|
|
|
|
this._flyEnd.set(this.targetPos);
|
|
|
|
|
const dx = this._flyEnd.x - this._flyStart.x;
|
|
|
|
|
const dy = this._flyEnd.y - this._flyStart.y;
|
|
|
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
|
const basePeak = Math.max(this.arcMinHeight, dist * this.arcHeightRatio);
|
|
|
|
|
this._arcPeak = basePeak * ARC_PEAK_HEIGHT_MULT;
|
|
|
|
|
this._flyT = 0;
|
|
|
|
|
this.isFly = true;
|
|
|
|
|
this._reparentToGameRootForPhysics();
|
|
|
|
|
this._flyPrevWorld.set(this.node.worldPosition);
|
|
|
|
|
this._configurePhysicsForOverlapOnly();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 飞行全程:Sensor 不产生挤压/冲量;Kinematic + 清速度,由脚本 setWorldPosition 驱动,避免“撞开”砖块。
|
|
|
|
|
*/
|
|
|
|
|
private _configurePhysicsForOverlapOnly(): void {
|
|
|
|
|
const rb = this._rigidBody ?? this.node.getComponent(RigidBody2D);
|
|
|
|
|
if (rb) {
|
|
|
|
|
rb.enabledContactListener = true;
|
|
|
|
|
rb.type = ERigidBody2DType.Kinematic;
|
|
|
|
|
rb.linearVelocity = v2(0, 0);
|
|
|
|
|
rb.angularVelocity = 0;
|
|
|
|
|
}
|
|
|
|
|
if (this.collider) {
|
|
|
|
|
this.collider.sensor = true;
|
|
|
|
|
}
|
|
|
|
|
this._syncBossSkillPhysicsGroup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 刚体与碰撞体统一为 bossSkill 分组,与 card / floor 的碰撞矩阵一致 */
|
|
|
|
|
private _syncBossSkillPhysicsGroup(): void {
|
|
|
|
|
const rb = this._rigidBody ?? this.node.getComponent(RigidBody2D);
|
|
|
|
|
const col = this.collider;
|
|
|
|
|
if (rb) {
|
|
|
|
|
rb.group = PHYSICS_GROUP_BOSS_SKILL;
|
|
|
|
|
}
|
|
|
|
|
if (col) {
|
|
|
|
|
col.group = PHYSICS_GROUP_BOSS_SKILL;
|
|
|
|
|
const apply = (col as unknown as { apply?: () => void }).apply;
|
|
|
|
|
if (typeof apply === 'function') {
|
|
|
|
|
try {
|
|
|
|
|
apply.call(col);
|
|
|
|
|
} catch {
|
|
|
|
|
/* ignore */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 抛物线结束后:恢复实体碰撞与动态刚体,受重力下落并允许滚动。
|
|
|
|
|
* BEGIN_CONTACT 内不能改 type:放到「当前物理步结束之后」再执行(避免 setType 断言)。
|
|
|
|
|
*/
|
|
|
|
|
private _scheduleRestoreDynamicPhysicsForRoll(): void {
|
|
|
|
|
if (this._physicsRestored || this._restorePhysicsDeferredPending) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._restorePhysicsDeferredPending = true;
|
|
|
|
|
director.once(Director.EVENT_AFTER_PHYSICS, this._deferredRestoreDynamicPhysics, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private readonly _deferredRestoreDynamicPhysics = (): void => {
|
|
|
|
|
this._restorePhysicsDeferredPending = false;
|
|
|
|
|
if (!this.isValid || !this.node?.isValid || this._physicsRestored) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._physicsRestored = true;
|
|
|
|
|
const rb = this._rigidBody ?? this.node.getComponent(RigidBody2D);
|
|
|
|
|
if (this.collider) {
|
|
|
|
|
this.collider.sensor = false;
|
|
|
|
|
if (this.collider instanceof CircleCollider2D) {
|
|
|
|
|
// 略提高摩擦,便于在方块顶面滚动而不是一直打滑
|
|
|
|
|
this.collider.friction = 0.55;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this._syncBossSkillPhysicsGroup();
|
|
|
|
|
if (rb) {
|
|
|
|
|
rb.enabledContactListener = true;
|
|
|
|
|
rb.bullet = true;
|
|
|
|
|
rb.type = ERigidBody2DType.Dynamic;
|
|
|
|
|
rb.gravityScale = 1;
|
|
|
|
|
rb.fixedRotation = false;
|
|
|
|
|
rb.linearVelocity = v2(0, 0);
|
|
|
|
|
rb.angularVelocity = 0;
|
|
|
|
|
const anyRb = rb as unknown as { wakeUp?: () => void };
|
|
|
|
|
anyRb.wakeUp?.();
|
|
|
|
|
}
|
|
|
|
|
const colApply = this.collider as unknown as { apply?: () => void };
|
|
|
|
|
if (typeof colApply?.apply === 'function') {
|
|
|
|
|
try {
|
|
|
|
|
colApply.apply.call(this.collider);
|
|
|
|
|
} catch {
|
|
|
|
|
/* ignore */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private _queueBrickImpulse(other: Collider2D, contact: IPhysics2DContact | null): void {
|
|
|
|
|
if (this.bombForce <= 0 || !other?.isValid) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._brickImpulseCollider = other;
|
|
|
|
|
this._brickImpulseContact = contact;
|
|
|
|
|
if (!this._brickImpulsePending) {
|
|
|
|
|
this._brickImpulsePending = true;
|
|
|
|
|
director.once(Director.EVENT_AFTER_PHYSICS, this._applyDeferredBrickImpulse, this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private readonly _applyDeferredBrickImpulse = (): void => {
|
|
|
|
|
this._brickImpulsePending = false;
|
|
|
|
|
const col = this._brickImpulseCollider;
|
|
|
|
|
const contact = this._brickImpulseContact;
|
|
|
|
|
this._brickImpulseCollider = null;
|
|
|
|
|
this._brickImpulseContact = null;
|
|
|
|
|
if (!this.isValid || this.bombForce <= 0 || !col?.isValid) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._applyBombForceImpulse(col, contact);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 从碰撞体节点向上查找砖块刚体(tetri 根上多为 RigidBody2D) */
|
|
|
|
|
private _findBrickRigidBody(otherCollider: Collider2D): RigidBody2D | null {
|
|
|
|
|
let n: Node | null = otherCollider.node;
|
|
|
|
|
while (n) {
|
|
|
|
|
const rb = n.getComponent(RigidBody2D);
|
|
|
|
|
if (rb) {
|
|
|
|
|
return rb;
|
|
|
|
|
}
|
|
|
|
|
n = n.parent;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 配表 bombForce:对动态砖块施加世界冲量(沿接触法线或炸弹→砖方向),Static/Kinematic 目标跳过。
|
|
|
|
|
*/
|
|
|
|
|
private _applyBombForceImpulse(otherCollider: Collider2D, contact: IPhysics2DContact | null): void {
|
|
|
|
|
const otherRb = this._findBrickRigidBody(otherCollider);
|
|
|
|
|
if (!otherRb?.isValid || otherRb.type !== ERigidBody2DType.Dynamic) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const bombPos = this.node.worldPosition;
|
|
|
|
|
let nx = 0;
|
|
|
|
|
let ny = 1;
|
|
|
|
|
const wm = contact?.getWorldManifold?.() as { normal?: Vec2; points?: Vec2[] } | undefined;
|
|
|
|
|
const nn = wm?.normal;
|
|
|
|
|
if (nn && nn.x * nn.x + nn.y * nn.y > 1e-8) {
|
|
|
|
|
nx = nn.x;
|
|
|
|
|
ny = nn.y;
|
|
|
|
|
} else {
|
|
|
|
|
const aabb = otherCollider.worldAABB;
|
|
|
|
|
const ox = aabb ? (aabb.xMin + aabb.xMax) * 0.5 : otherCollider.node.worldPosition.x;
|
|
|
|
|
const oy = aabb ? (aabb.yMin + aabb.yMax) * 0.5 : otherCollider.node.worldPosition.y;
|
|
|
|
|
nx = ox - bombPos.x;
|
|
|
|
|
ny = oy - bombPos.y;
|
|
|
|
|
const hl = Math.hypot(nx, ny);
|
|
|
|
|
if (hl < 1e-4) {
|
|
|
|
|
nx = 0;
|
|
|
|
|
ny = 1;
|
|
|
|
|
} else {
|
|
|
|
|
nx /= hl;
|
|
|
|
|
ny /= hl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const otherPos = otherCollider.node.worldPosition;
|
|
|
|
|
const ox = otherPos.x - bombPos.x;
|
|
|
|
|
const oy = otherPos.y - bombPos.y;
|
|
|
|
|
if (nx * ox + ny * oy < 0) {
|
|
|
|
|
nx = -nx;
|
|
|
|
|
ny = -ny;
|
|
|
|
|
}
|
|
|
|
|
const len = Math.hypot(nx, ny) || 1;
|
|
|
|
|
nx /= len;
|
|
|
|
|
ny /= len;
|
|
|
|
|
const pts = wm?.points;
|
|
|
|
|
const px = pts?.length ? pts[0].x : otherPos.x;
|
|
|
|
|
const py = pts?.length ? pts[0].y : otherPos.y;
|
|
|
|
|
otherRb.applyLinearImpulse(v2(nx * this.bombForce, ny * this.bombForce), v2(px, py), true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 碰撞体所在节点向上查找已落稳的俄罗斯方块(砖块) */
|
|
|
|
|
private _getSettledTetriBrickFromCollider(other: Collider2D) {
|
|
|
|
|
let n: Node | null = other?.node;
|
|
|
|
|
while (n) {
|
|
|
|
|
const t = n.getComponent(tetriNode) || n.getComponent(tetriFloorNode);
|
|
|
|
|
if (t ) {
|
|
|
|
|
return t;
|
|
|
|
|
}
|
|
|
|
|
n = n.parent;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _stopFlyAtCurrentWorldPos(): void {
|
|
|
|
|
if (!this.isFly) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.isFly = false;
|
|
|
|
|
this._flyT = 1;
|
|
|
|
|
const wp = this.node.worldPosition;
|
|
|
|
|
this.node.setWorldPosition(wp.x, wp.y, wp.z);
|
|
|
|
|
this._scheduleRestoreDynamicPhysicsForRoll();
|
|
|
|
|
|
|
|
|
|
this.scheduleOnce(() => {
|
|
|
|
|
this.playExplosionEffect();
|
|
|
|
|
}, this.bombDelayTime);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
|
|
|
|
|
if (!this.isFly) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!this._getSettledTetriBrickFromCollider(otherCollider)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._queueBrickImpulse(otherCollider, contact);
|
|
|
|
|
this._stopFlyAtCurrentWorldPos();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**播放爆炸效果 */
|
|
|
|
|
playExplosionEffect() {
|
|
|
|
|
const bs = this.getExplosionBlocks();
|
|
|
|
|
const destroyedStackRoots = new Set<string>();
|
|
|
|
|
for (let i = 0; i < bs.length; i++) {
|
|
|
|
|
const b = bs[i];
|
|
|
|
|
if (!b?.isValid) continue;
|
|
|
|
|
const t = b.getComponent(tetriNode);
|
|
|
|
|
if (t?.node?.isValid) {
|
|
|
|
|
const stackRoot = t.getStackRootUnderTetrisTable();
|
|
|
|
|
const key = stackRoot?.uuid ?? t.node.uuid;
|
|
|
|
|
if (destroyedStackRoots.has(key)) continue;
|
|
|
|
|
destroyedStackRoots.add(key);
|
|
|
|
|
t.destroyForRemoval('boss');
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
b.destroy();
|
|
|
|
|
}
|
|
|
|
|
if (!BattlePerformance.rollShowCombatVfx(0.55)) {
|
|
|
|
|
this.scheduleOnce(() => {
|
|
|
|
|
if (this.node?.isValid) gg.res.putNode(this.node);
|
|
|
|
|
}, 0.1);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
void this._spawnExplosionVfx();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _spawnExplosionVfx(): Promise<void> {
|
|
|
|
|
if (!this.node?.isValid) return;
|
|
|
|
|
const parent = this.node.parent?.isValid
|
|
|
|
|
? this.node.parent
|
|
|
|
|
: (gg.game?.CurentBattle?.TetraMap?._gameRoot ?? null);
|
|
|
|
|
const wp = this.node.worldPosition.clone();
|
|
|
|
|
this.scheduleOnce(() => {
|
|
|
|
|
if (this.node?.isValid) gg.res.putNode(this.node);
|
|
|
|
|
}, 0.1);
|
|
|
|
|
|
|
|
|
|
let n = await gg.res.getNodeAsync('prefab/爆炸/', '砖块爆炸', GBundle.BattleGameWeapon);
|
|
|
|
|
if (!n?.isValid) return;
|
|
|
|
|
if (parent?.isValid) {
|
|
|
|
|
n.setParent(parent, true);
|
|
|
|
|
} else {
|
|
|
|
|
gg.res.putNode(n);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
n.worldPosition = wp;
|
|
|
|
|
const vfxMul = BattlePerformance.vfxScaleMul();
|
|
|
|
|
n.setScale(vfxMul, vfxMul, vfxMul);
|
|
|
|
|
const sk = n.getComponent(sp.Skeleton);
|
|
|
|
|
if (!sk) {
|
|
|
|
|
gg.res.putNode(n);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
sk.setAnimation(0, '爆炸', false);
|
|
|
|
|
sk.setCompleteListener(() => {
|
|
|
|
|
if (n?.isValid) gg.res.putNode(n);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**获取爆炸范围内的所有堆叠的方块 */
|
|
|
|
|
getExplosionBlocks(): Node[] {
|
|
|
|
|
const map = gg.game.CurentBattle?.TetraMap;
|
|
|
|
|
const table = map?._tetrisTable;
|
|
|
|
|
if (!table?.isValid) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
const cell = Math.max(1, map.blockHeight ?? 36);
|
|
|
|
|
const minOverlapArea = cell * cell;
|
|
|
|
|
const cx = this.node.worldPosition.x;
|
|
|
|
|
const cy = this.node.worldPosition.y;
|
|
|
|
|
const r = this.bombRange;
|
|
|
|
|
if (r <= 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const blocks: Node[] = [];
|
|
|
|
|
const roots = table.children;
|
|
|
|
|
for (let i = 0; i < roots.length; i++) {
|
|
|
|
|
const root = roots[i];
|
|
|
|
|
if (!root?.isValid) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let overlapSum = 0;
|
|
|
|
|
const cols = root.getComponentsInChildren(Collider2D) ?? [];
|
|
|
|
|
for (let j = 0; j < cols.length; j++) {
|
|
|
|
|
const col = cols[j];
|
|
|
|
|
if (!col?.enabled || !col.node?.isValid) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const aabb = col.worldAABB;
|
|
|
|
|
if (!aabb) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
overlapSum += this._estimateCircleRectOverlapArea(cx, cy, r, aabb);
|
|
|
|
|
}
|
|
|
|
|
if (overlapSum >= minOverlapArea) {
|
|
|
|
|
blocks.push(root);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return blocks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 估算圆盘与轴对齐矩形相交面积(像素²),在圆与矩形的公共包围子域内均匀采样。
|
|
|
|
|
*/
|
|
|
|
|
private _estimateCircleRectOverlapArea(cx: number, cy: number, r: number, rect: Rect): number {
|
|
|
|
|
const r2 = r * r;
|
|
|
|
|
const x0 = Math.max(rect.xMin, cx - r);
|
|
|
|
|
const x1 = Math.min(rect.xMax, cx + r);
|
|
|
|
|
const y0 = Math.max(rect.yMin, cy - r);
|
|
|
|
|
const y1 = Math.min(rect.yMax, cy + r);
|
|
|
|
|
if (x1 <= x0 || y1 <= y0) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
const gw = x1 - x0;
|
|
|
|
|
const gh = y1 - y0;
|
|
|
|
|
const GRID = 14;
|
|
|
|
|
let inside = 0;
|
|
|
|
|
const inv = 1 / GRID;
|
|
|
|
|
for (let i = 0; i < GRID; i++) {
|
|
|
|
|
const px = x0 + (i + 0.5) * inv * gw;
|
|
|
|
|
const dx = px - cx;
|
|
|
|
|
const dx2 = dx * dx;
|
|
|
|
|
for (let j = 0; j < GRID; j++) {
|
|
|
|
|
const py = y0 + (j + 0.5) * inv * gh;
|
|
|
|
|
const dy = py - cy;
|
|
|
|
|
if (dx2 + dy * dy <= r2) {
|
|
|
|
|
inside++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return (inside / (GRID * GRID)) * gw * gh;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|