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.
1850 lines
75 KiB
1850 lines
75 KiB
import { _decorator, Collider2D, Component, Contact2DType, ERigidBody2DType, IPhysics2DContact, Mat4, Node, PolygonCollider2D, Rect, RigidBody2D, sp, tween, Tween, UIOpacity, Vec2, Vec3 } from 'cc';
|
|
import { GEvent } from '../../mx/module/event/GEvent';
|
|
import { TetriType } from '../game/ConfigProjectData';
|
|
import { tetriWeaponCarrier } from './tetriWeaponCarrier';
|
|
import { GBundle, GPath } from '../game/ConfigRes';
|
|
import { UILayer } from '../../mx/module/ui/UIManager';
|
|
import { TetriStackDiagnostics } from '../tetriMap/TetriStackDiagnostics';
|
|
import { BattlePerformance } from '../ui/BattleGame/BattlePerformance';
|
|
import { isTetriBlockInShop, isTetriBlockOnTetrisTable, playTetriBlockSpine, TetriBlockSpineState } from './tetriBlockSpine';
|
|
const { ccclass, property } = _decorator;
|
|
|
|
|
|
/**自由下落的方块节点
|
|
* group == card
|
|
* 如果rigidbody2d组件存在,设置group为card
|
|
* 如果polygoncollider2d组件存在,设置group为card
|
|
* 可以升级
|
|
*/
|
|
@ccclass('tetriNode')
|
|
export class tetriNode extends Component {
|
|
private static _nextVineGroupId = 1;
|
|
/**方块配置(BattleCube 表) */
|
|
public config: ITableBattleCube | null = null;
|
|
_curweaponId: number = 0; //使用当前方块的武器id 不要使用config.weaponid 注意
|
|
public setConfig(cfg: ITableBattleCube | null) {
|
|
this.config = cfg;
|
|
|
|
//
|
|
let cubeName = cfg?.cubename
|
|
//获取当前武器的id
|
|
let weaponId = gg.game.CurentBattle.TetriWeaponMap.get(cubeName)
|
|
this._curweaponId = weaponId
|
|
|
|
this.refreshTengman();
|
|
this._isVineAssimilateSource = cfg?.type === TetriType.Tetris_Basevine;
|
|
if (this._isVineMode()) {
|
|
this._enterVineGroup(this._ensureVineGroupId());
|
|
}
|
|
}
|
|
|
|
//@property({ displayName: '落稳静止帧数' })
|
|
settleStillFrames: number = 8;
|
|
/** 已通过静止帧计数后,再连续若干帧(每帧一次 update≈上一帧物理结束后的速度)仍低于阈值才 onSettled,防止堆叠后下一两帧又被冲开 */
|
|
///@property({ displayName: '落稳后补确认帧数' })
|
|
settlePostConfirmFrames: number = 3;
|
|
//@property({ displayName: '落稳速度阈值' })
|
|
settleVelThreshold: number = 20;
|
|
//@property({ displayName: '落稳竖直速度阈值' })
|
|
settleVyThreshold: number = 25;
|
|
//@property({ displayName: '落稳角速度阈值' })
|
|
settleAngThreshold: number = 10;
|
|
// @property({ displayName: '落稳后衰减系数' })
|
|
settledDamp: number = 0.2;
|
|
|
|
//@property({ displayName: '接触后横向衰减系数' })
|
|
contactXDamp: number = 3;
|
|
|
|
/** 藤蔓关节:最晚在多少秒内等到「基本完全静止」后再打关节;超时则放弃本次打关节(打日志) */
|
|
//@property({ displayName: '藤蔓关节最大等待(秒)' })
|
|
vineJointMaxWaitSec = 1.5;
|
|
/** 判定「可打关节」前需连续满足静止的帧数(update 帧) */
|
|
//@property({ displayName: '藤蔓关节前连续静止帧数' })
|
|
vineJointStillConsecutiveFrames = 4;
|
|
//@property({ displayName: '藤蔓关节:静止速度阈值' })
|
|
vineJointStillSpeedThreshold = 5;
|
|
//@property({ displayName: '藤蔓关节:静止竖直速度阈值' })
|
|
vineJointStillVyThreshold = 6;
|
|
//@property({ displayName: '藤蔓关节:静止角速度阈值' })
|
|
vineJointStillAngThreshold = 3;
|
|
/** 接近静止即可感染/绑定:整体速度阈值(不要求完全睡眠) */
|
|
//@property({ displayName: '藤蔓绑定近静止速度阈值' })
|
|
vineJointNearStillSpeedThreshold = 14;
|
|
/** 接近静止即可感染/绑定:竖直速度阈值 */
|
|
//@property({ displayName: '藤蔓绑定近静止竖直速度阈值' })
|
|
vineJointNearStillVyThreshold = 12;
|
|
/** 接近静止即可感染/绑定:角速度阈值 */
|
|
//@property({ displayName: '藤蔓绑定近静止角速度阈值' })
|
|
vineJointNearStillAngThreshold = 6;
|
|
/** 接近静止即可感染/绑定:双方相对速度阈值 */
|
|
//@property({ displayName: '藤蔓绑定相对速度阈值' })
|
|
vineJointNearStillRelativeSpeedThreshold = 8;
|
|
/** 同一对方块需连续满足近静止若干次才允许绑定,避免“擦到就绑” */
|
|
//@property({ displayName: '藤蔓绑定连续近静止次数' })
|
|
vineJointNearStillNeedConsecutiveTicks = 1;
|
|
/** 绑定成功后短冷却,避免同一时刻多约束注入冲量 */
|
|
//@property({ displayName: '藤蔓绑定冷却(秒)' })
|
|
vineJointLinkCooldownSec = 0.12;
|
|
/** 关节启用时要求更严格的相对速度比例(相对 vineJointNearStillRelativeSpeedThreshold) */
|
|
//@property({ displayName: '藤蔓关节启用相对速度系数' })
|
|
vineJointEnableRelativeSpeedScale = 0.8;
|
|
/** 藤蔓锚点贴边间隙兜底(0=仅真实重叠接触点) */
|
|
//@property({ displayName: '藤蔓锚点最大贴边间隙(px)' })
|
|
vineJointAnchorMaxGapPx = 0;
|
|
/** 固定关节:弹性系数,越大越硬(见官方 2D 关节文档 FixedJoint2D) */
|
|
//@property({ displayName: '藤蔓 FixedJoint2D 频率(过高易与堆叠解算打架抖)' })
|
|
vineJointFrequency = 4;
|
|
//@property({ displayName: '藤蔓 FixedJoint2D 阻尼比(0~1,略高更吃振)' })
|
|
vineJointDampingRatio = 0.95;
|
|
/** 同一藤蔓上最多打几条关节;多邻块全连易过约束弹飞,2 通常够用 */
|
|
//@property({ displayName: '藤蔓最多连接邻块数' })
|
|
vineMaxPartnerJoints = 2;
|
|
/** 藤蔓接触扩张检测间隔 */
|
|
//@property({ displayName: '藤蔓扩张检测间隔(秒)' })
|
|
vineAssimilateIntervalSec = 0.5;
|
|
/** 首次发生接触后,延迟多久再开始藤蔓检测 */
|
|
//@property({ displayName: '藤蔓接触后检测延迟(秒)' })
|
|
vineAssimilateStartDelaySec = 0.35;
|
|
/** 藤蔓感染允许跨越的最大缝隙(像素),0=必须真实接触 */
|
|
//@property({ displayName: '藤蔓感染允许间隙(px)' })
|
|
vineAssimilateMaxGapPx = 8;
|
|
/** 合并时 AABB 最小轴重叠上限(像素);两轴都超过视为深嵌/挤压,拒绝合并防弹飞 */
|
|
vineMergeMaxOverlapPx = 18;
|
|
|
|
/** AABB 接触检测膨胀(像素):解决“刚好贴边但 AABB 不相交”的漏检;调大更容易命中,调小更严格 */
|
|
private _touchAabbInflatePx = 0.2;
|
|
/** AABB 兜底判定需要的最小重叠量(像素),避免“擦边/隔空”误判 */
|
|
private _touchAabbMinOverlapPx = 0.5;
|
|
|
|
/** 由物理 contact 维护的“真实接触中”节点(比 AABB 更准,避免右侧隔空误判) */
|
|
private _touchingByContactCount = new Map<tetriNode, number>();
|
|
private _boundContactColliders: Collider2D[] = [];
|
|
private _settled = false;
|
|
/** 怪物区吞噬/合并销毁/锤子等:已从「武器方块叠层 & DPS」统计中剔除,避免销毁动画期间仍算 cubeCount */
|
|
private _excludedFromWeaponStackMetrics = false;
|
|
/** 已进入杀区销毁流程,避免多碰撞体/重复 contact 多次 skill → 双 tween、双扣雷电爱心 */
|
|
private _killSkillDispatched = false;
|
|
private _pendingSettle = false;
|
|
private _pendingSettleLeft = 0;
|
|
private _prevVy: number = 0;
|
|
private _contactNotified = false;
|
|
private _stillFrames = 0;
|
|
|
|
/** 落稳后:在 vineJointMaxWaitSec 内轮询直到基本静止再打藤蔓关节 */
|
|
private _vineJointWaitActive = false;
|
|
private _vineJointDeadlineMs = 0;
|
|
private _vineJointStillConsec = 0;
|
|
private _vineAssimilateActive = false;
|
|
private _vineGroupId = 0;
|
|
private _isVineAssimilateSource = false;
|
|
private _vinePairNearStillStreak = new Map<string, number>();
|
|
private _vineNextLinkAllowedAtMs = 0;
|
|
private _vinePendingPartners = new Map<string, tetriNode>();
|
|
private _vineAssimilateStartPending = false;
|
|
/** 藤蔓合并扫描次数(防止无限轮询) */
|
|
private _vineAssimilateAttemptCount = 0;
|
|
private static readonly VINE_ASSIMILATE_MAX_ATTEMPTS = 12;
|
|
|
|
private _tetriLevelMax = 5
|
|
private _tetriLevel = 1; //12345 对应白绿蓝紫橙色
|
|
/**落稳时回调(由外部注入,例如 tetriMap) */
|
|
public onSettled: (() => void) | null = null;
|
|
|
|
/**
|
|
* 与悬浮地板接触后:在若干次物理步结束时由 tetriMap 调用 RigidBody2D 的 clearForces/clearVelocity(及可选 clearState)消除寄生回弹。
|
|
*/
|
|
public floorRbHardClearPhyStepsLeft = 0;
|
|
/**
|
|
* floor「窄接缝桥接」物理步剩余次数:与 floorRbHardClearPhyStepsLeft 一起在 tetriMap 的 AFTER_PHYSICS 里做更强角速度/微弹清理。
|
|
* 仅在与 floor 弱支撑且同时叠在已落稳块上时由 PRE_SOLVE 写入。
|
|
*/
|
|
public floorNarrowBridgePhyStepsLeft = 0;
|
|
|
|
/** 已与藤蔓通过 FixedJoint2D 绑成一组 */
|
|
public vineGroupLinked = false;
|
|
|
|
/** 进入 _settled 的时间戳(ms),用于做“真正稳定后再处理” */
|
|
private _settledAtMs: number = 0;
|
|
/** 落稳后连续低速帧数,达标后冻结刚体(退出物理解算) */
|
|
private _settledSleepStreak = 0;
|
|
/** 安静窗口起始角度,用于检出「角速度≈0 但仍在慢倾」 */
|
|
private _freezeQuietStartAngle = 0;
|
|
/** 已落稳且切为 Static,不再参与每帧物理/update */
|
|
private _physicsFrozen = false;
|
|
private _freezeScheduled = false;
|
|
/** 锤子/撞击解冻后禁止再冻 Static 直到该时刻,避免水平悬臂尚未倾覆又被冻住 */
|
|
private _freezeSuppressUntilMs = 0;
|
|
|
|
get isPhysicsFrozen(): boolean { return this._physicsFrozen; }
|
|
|
|
get prevVy(): number { return this._prevVy; }
|
|
/**已落稳*/
|
|
get isSettled(): boolean { return this._settled; }
|
|
|
|
/** 场景预置在 TetrisTable 上的块(不经下落流程),参与堆高/白线 */
|
|
public markScenePlacedSettled(): void {
|
|
this._settled = true;
|
|
this._settledAtMs = Date.now();
|
|
this._stackSupportStable = true;
|
|
this.addWeapon();
|
|
}
|
|
|
|
/** 是否计入堆叠高度 / 白线(落稳且支撑链稳定) */
|
|
public shouldCountTowardStackHeight(): boolean {
|
|
return this._settled && this._stackSupportStable;
|
|
}
|
|
|
|
/**
|
|
* 是否正与至少一块「已落稳」的普通方块有物理接触(BEGIN/END 维护的计数 > 0)。
|
|
* 供 tetriMap PRE_SOLVE 识别「同时压在 floor 与堆叠块上」:与 floor 接触面很小时约束易打架,需更强衰减。
|
|
*/
|
|
public hasPhysContactWithSettledTetri(): boolean {
|
|
for (const [partner, cnt] of this._touchingByContactCount) {
|
|
if (cnt > 0 && partner?.node?.isValid && partner !== this && partner.isSettled) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 是否可与「已落稳」普通块形成桥接支撑:优先物理 contact;无计数时用 AABB 兜底(PRE_SOLVE 常早于 BEGIN_CONTACT)。
|
|
*/
|
|
public hasSettledTetriBridgeSupport(): boolean {
|
|
if (this.hasPhysContactWithSettledTetri()) return true;
|
|
const selfCols = this._getAllColliders2D(this.node);
|
|
if (!selfCols.length) return false;
|
|
const scene = this.node.scene;
|
|
if (!scene) return false;
|
|
const pad = 1.5;
|
|
const minOv = 0.35;
|
|
const all = scene.getComponentsInChildren(tetriNode) ?? [];
|
|
for (const t of all) {
|
|
if (!t?.node?.isValid || t === this || !t.isSettled) continue;
|
|
const otherCols = t._getAllColliders2D(t.node);
|
|
if (!otherCols.length) continue;
|
|
if (this._anyAabbPairIntersects(selfCols, otherCols, pad, minOv)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** 参与场上武器方块数、getTetrStackLevelSum、DPS 等统计 */
|
|
public shouldCountTowardWeaponStack(): boolean {
|
|
return this._settled && !this._excludedFromWeaponStackMetrics;
|
|
}
|
|
|
|
/**
|
|
* 本节点子树内所有 tetriNode 不再参与叠层/DPS。
|
|
* 藤蔓合并后多块挂在同一根下,仅排除点击的那一块会在 destroy 前的一帧内仍把子块算进 cubeCount。
|
|
*/
|
|
public excludeWholeStackMetricsSubtree(): void {
|
|
const root = this._getStackRootUnderTetrisTable();
|
|
if (!root?.isValid) return;
|
|
const list = root.getComponentsInChildren(tetriNode) ?? [];
|
|
for (let i = 0; i < list.length; i++) {
|
|
const t = list[i];
|
|
if (t?.node?.isValid) t._excludedFromWeaponStackMetrics = true;
|
|
}
|
|
}
|
|
|
|
private _getTetrisTable(): Node | null {
|
|
return gg.game?.CurentBattle?.TetraMap?._tetrisTable ?? null;
|
|
}
|
|
|
|
/** 藤蔓并组后取堆叠根;下落中在 GameRoot 下时止步,避免爬到 Scene */
|
|
private _getStackRootUnderTetrisTable(): Node {
|
|
const map = gg.game?.CurentBattle?.TetraMap;
|
|
const table = this._getTetrisTable();
|
|
const gameRoot = map?._gameRoot;
|
|
let n: Node = this.node;
|
|
if (!this.node?.isValid) return n;
|
|
if (!table?.isValid) return n;
|
|
while (n.parent?.isValid && n.parent !== table) {
|
|
if (gameRoot?.isValid && n.parent === gameRoot) break;
|
|
n = n.parent;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
private _isSafeDestroyTarget(n: Node | null): boolean {
|
|
return !!(n?.isValid && n.parent);
|
|
}
|
|
|
|
private _getStackPrimaryTetriNode(): tetriNode | null {
|
|
const root = this._getStackRootUnderTetrisTable();
|
|
if (!root?.isValid) return null;
|
|
return root.getComponent(tetriNode) ?? root.getComponentInChildren(tetriNode);
|
|
}
|
|
|
|
private _isVineMergedStack(): boolean {
|
|
return this.vineGroupLinked || this._vineGroupId > 0;
|
|
}
|
|
|
|
/** 已 setParent 并入 stackRoot 的子块(共享 vineGroupId 但仍是独立根节点时不算) */
|
|
private _isPhysicallyMergedIntoStackRoot(): boolean {
|
|
const root = this._getStackRootUnderTetrisTable();
|
|
if (!root?.isValid || this.node === root) return false;
|
|
let p: Node | null = this.node.parent;
|
|
while (p?.isValid) {
|
|
if (p === root) return true;
|
|
p = p.parent;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private _getRemovalTargetNode(): Node {
|
|
return this._isPhysicallyMergedIntoStackRoot()
|
|
? this._getStackRootUnderTetrisTable()
|
|
: this.node;
|
|
}
|
|
|
|
private _markKillSkillDispatchedOnStack(): void {
|
|
const root = this._getRemovalTargetNode();
|
|
if (!root?.isValid) {
|
|
this._killSkillDispatched = true;
|
|
return;
|
|
}
|
|
const list = root.getComponentsInChildren(tetriNode) ?? [];
|
|
for (let i = 0; i < list.length; i++) {
|
|
const t = list[i];
|
|
if (t) t._killSkillDispatched = true;
|
|
}
|
|
}
|
|
|
|
/** 销毁并组挂入的其它方块节点(带 tetriNode 的子节点);橙/紫/绿/蓝/藤曼 无需单独 destroy */
|
|
private _destroyVineMergedPartnerTetriNodes(): void {
|
|
const root = this._getStackRootUnderTetrisTable();
|
|
if (!root?.isValid) return;
|
|
const children = root.children;
|
|
for (let i = children.length - 1; i >= 0; i--) {
|
|
const ch = children[i];
|
|
if (ch?.isValid && ch.getComponent(tetriNode)) ch.destroy();
|
|
}
|
|
}
|
|
|
|
private _destroyForRemoval(): void {
|
|
if (!this.node?.isValid) return;
|
|
gg.game?.CurentBattle?.TetraMap?.invalidateStackTopCache?.();
|
|
gg.game?.CurentBattle?.pruneDetachedBattleLayers?.(this.node);
|
|
const root = this._getStackRootUnderTetrisTable();
|
|
if (this._isPhysicallyMergedIntoStackRoot()) {
|
|
if (this._isSafeDestroyTarget(root)) {
|
|
gg.game?.CurentBattle?.pruneDetachedBattleLayers?.(root);
|
|
root.destroy();
|
|
}
|
|
return;
|
|
}
|
|
if (this.node === root) {
|
|
this._destroyVineMergedPartnerTetriNodes();
|
|
}
|
|
if (this.node?.isValid) this.node.destroy();
|
|
}
|
|
|
|
/** 渐隐后销毁;目标非法时直接销毁,避免对 Scene tween opacity */
|
|
private _fadeOutAndDestroy(removalNode: Node, diagSource?: string): void {
|
|
if (!this._isSafeDestroyTarget(removalNode)) {
|
|
this._destroyForRemovalWithDiag(diagSource ?? 'skill');
|
|
return;
|
|
}
|
|
Tween.stopAllByTarget(removalNode);
|
|
let uiOpacity = removalNode.getComponent(UIOpacity);
|
|
if (!uiOpacity) {
|
|
try {
|
|
uiOpacity = removalNode.addComponent(UIOpacity);
|
|
} catch {
|
|
this._destroyForRemovalWithDiag(diagSource ?? 'skill');
|
|
return;
|
|
}
|
|
}
|
|
Tween.stopAllByTarget(uiOpacity);
|
|
tween(uiOpacity)
|
|
.to(0.5, { opacity: 50 })
|
|
.call(() => {
|
|
if (!removalNode?.isValid) return;
|
|
gg.game?.CurentBattle?.tryApplyLightningLoveOnRemovalGround();
|
|
GEvent.Ins.emit(GEvent.UpdateDps, 0);
|
|
const primary = removalNode.getComponent(tetriNode);
|
|
const source = diagSource ?? 'skill';
|
|
if (primary) primary._destroyForRemovalWithDiag(source);
|
|
else {
|
|
const weaponBefore = TetriStackDiagnostics.countBlocks().weapon;
|
|
const nodeName = removalNode.name ?? '?';
|
|
removalNode.destroy();
|
|
TetriStackDiagnostics.afterWeaponRemoved(source, weaponBefore, nodeName);
|
|
}
|
|
})
|
|
.start();
|
|
}
|
|
|
|
private _destroyForRemovalWithDiag(source: string): void {
|
|
const weaponBefore = TetriStackDiagnostics.countBlocks().weapon;
|
|
const nodeName = this.node?.name ?? '?';
|
|
this._destroyForRemoval();
|
|
TetriStackDiagnostics.afterWeaponRemoved(source, weaponBefore, nodeName);
|
|
}
|
|
|
|
/** 藤蔓并组时 _tetrisTable 下的整组根节点 */
|
|
public getStackRootUnderTetrisTable(): Node {
|
|
return this._getStackRootUnderTetrisTable();
|
|
}
|
|
|
|
/** 掉落/杀区/锤子/Boss 炸弹等统一移除(含藤蔓并组子方块) */
|
|
public destroyForRemoval(source: string = 'other'): void {
|
|
const cutY = this.node?.isValid ? this.node.worldPosition.y : Number.NEGATIVE_INFINITY;
|
|
const map = gg.game?.CurentBattle?.TetraMap;
|
|
// 只解冻到上方最近无重力板下沿;板上普通块保持支撑,不可穿板下落
|
|
map?.wakeFrozenStackPhysics(cutY - 40, this.node);
|
|
this.excludeWholeStackMetricsSubtree();
|
|
GEvent.Ins.emit(GEvent.UpdateDps, 0);
|
|
this._destroyForRemovalWithDiag(source);
|
|
}
|
|
|
|
private _stackSupportStable = false;
|
|
|
|
/** 是否已接到地图的「着地/碰支撑」通知,尚在落稳计时中(供 tetriMap 等做叠塔角度收敛) */
|
|
get hasSettleContact(): boolean { return this._contactNotified; }
|
|
|
|
/** 已落稳且支撑链可追溯到地面/台子/已归档稳定塔(悬空叠在另一块下落体上不计入白线) */
|
|
public hasStackSupportStable(): boolean { return this._stackSupportStable; }
|
|
|
|
/** 碰到地面/台子,或落在已有稳定支撑的块上时标记(并向上传递) */
|
|
public markStackSupportStable(): void {
|
|
if (this._stackSupportStable) return;
|
|
this._stackSupportStable = true;
|
|
const map = gg.game?.CurentBattle?.TetraMap;
|
|
map?.invalidateStackTopCache?.();
|
|
map?._updateHighLine?.();
|
|
for (const [partner, cnt] of this._touchingByContactCount) {
|
|
if (cnt > 0 && partner?.isSettled) {
|
|
partner.markStackSupportStable();
|
|
}
|
|
}
|
|
}
|
|
|
|
private _inheritStackSupportFromContacts(): void {
|
|
if (this._stackSupportStable) return;
|
|
for (const [partner, cnt] of this._touchingByContactCount) {
|
|
if (cnt > 0 && partner?.hasStackSupportStable()) {
|
|
this.markStackSupportStable();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
protected onLoad(): void {
|
|
this.settledDamp = 0
|
|
this._forceNoBounceOnOwnColliders();
|
|
const rb = this.node.getComponent(RigidBody2D);
|
|
if (rb) {
|
|
rb.enabledContactListener = true;
|
|
rb.allowSleep = false;
|
|
// 默认作为动态体(放置后会由外部确保开启重力)
|
|
rb.type = ERigidBody2DType.Dynamic;
|
|
rb.angularDamping = 0;
|
|
}
|
|
}
|
|
|
|
/** 播放方块表情 Spine(商店 / 下落 / 落地) */
|
|
public playBlockSpine(state: TetriBlockSpineState): void {
|
|
playTetriBlockSpine(this.node, state);
|
|
}
|
|
|
|
protected start(): void {
|
|
// 避免某些 prefab 第一次启用时碰撞不生效
|
|
//橙,紫,绿,蓝
|
|
this.node.getChildByName('橙').active = false;
|
|
this.node.getChildByName('紫').active = false;
|
|
this.node.getChildByName('绿').active = false;
|
|
this.node.getChildByName('蓝').active = false;
|
|
this.node.getChildByName('藤曼').active = false;
|
|
this.refreshTengman()
|
|
|
|
this.scheduleOnce(() => {
|
|
const poly = this.node.getComponent(PolygonCollider2D);
|
|
if (!poly) return;
|
|
poly.enabled = false;
|
|
poly.enabled = true;
|
|
}, 0);
|
|
|
|
this.scheduleOnce(() => {
|
|
if (!this.node?.isValid) return;
|
|
if (isTetriBlockInShop(this.node)) {
|
|
this.playBlockSpine(TetriBlockSpineState.Shop);
|
|
} else if (this._settled || isTetriBlockOnTetrisTable(this.node)) {
|
|
this.playBlockSpine(TetriBlockSpineState.Settled);
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
protected onEnable(): void {
|
|
// 锤子点选改由 UItetriGame 遮罩 + GameCamera 世界坐标拾取;不再挂节点 TOUCH(缩放后 UI 命中会点偏)
|
|
GEvent.Ins.on(GEvent.ChangeCubeWeaponConfig, this.ChangeCubeWeapon, this)
|
|
|
|
// 真实接触集合:绑定物理碰撞回调(比 AABB 更严格)
|
|
this._bindContactEvents();
|
|
// 某些 prefab 首帧会重新 enable collider,这里再延迟绑定一次兜底
|
|
this.scheduleOnce(this._bindContactEvents, 0);
|
|
this.scheduleOnce(this._forceNoBounceOnOwnColliders, 0);
|
|
}
|
|
|
|
protected onDisable(): void {
|
|
GEvent.Ins.off(GEvent.ChangeCubeWeaponConfig, this.ChangeCubeWeapon, this)
|
|
this.unregisterTouchEvent();
|
|
this._unbindContactEvents();
|
|
this._stopVineAssimilateLoop();
|
|
}
|
|
|
|
registerTouchEvent() {
|
|
// 保留空实现,避免旧事件残留;真正点选走遮罩
|
|
this.unregisterTouchEvent();
|
|
}
|
|
unregisterTouchEvent() {
|
|
this.node.off(Node.EventType.TOUCH_END, this.click_btnEnd, this);
|
|
}
|
|
|
|
/** 供锤子遮罩命中后调用(不要绑在节点 TOUCH 上) */
|
|
click_btnEnd() {
|
|
console.log('click_btnEnd')
|
|
if (!gg.game.CurentBattle.IsUseShovel) {
|
|
return
|
|
}
|
|
|
|
gg.game.CurentBattle.gameShovelNum--
|
|
gg.game.CurentBattle.IsUseShovel = false
|
|
gg.game.CurentBattle.subCoinNum(gg.game.CurentBattle.GameShovelCoin)
|
|
gg.sdk.reportDY("inLevel", `章节${gg.game.CurentBattle.getReportDYChapterId()}_${gg.game.CurentBattle.CurentWaveNum}-使用锤子`)
|
|
|
|
GEvent.Ins.emit(GEvent.TetriGameCancelUseShovel)
|
|
|
|
this.destroyForRemoval('hammer');
|
|
}
|
|
|
|
/** 落出玩法区销毁:须对 isSettled 块也生效(已落定块被挤落/滑落时原先会提前 return 而漏检) */
|
|
private _tryDestroyIfOutOfPlayArea(): boolean {
|
|
if (!this.node?.isValid) return true;
|
|
const wp = this.node.worldPosition;
|
|
const out =
|
|
wp.y < -1000
|
|
|| Math.abs(wp.x) > 2500
|
|
|| Math.abs(wp.y) > 50000
|
|
|| !Number.isFinite(wp.x)
|
|
|| !Number.isFinite(wp.y);
|
|
if (!out) return false;
|
|
const map = gg.game?.CurentBattle?.TetraMap;
|
|
if (wp.y < -1000) {
|
|
try { map?.showFloorRedFlash?.(map?._groundTable ?? null); } catch (e) { console.error(e); }
|
|
}
|
|
this.destroyForRemoval('fall');
|
|
console.log('[tetriNode] 方块落出玩法区销毁', this.node.name, wp);
|
|
return true;
|
|
}
|
|
|
|
update(dt: number): void {
|
|
if (this._tryDestroyIfOutOfPlayArea()) return;
|
|
// 已冻 Static 但明显倾斜:先解冻,否则 update 直接 return,重力无法继续倾覆贴合
|
|
if (this._physicsFrozen) {
|
|
if (this._settled && !this.vineGroupLinked && !this._isNearAxisAlignedForFreeze()) {
|
|
this.unfreezePhysicsForImpact();
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
if (this._pendingSettle) {
|
|
const rb0 = this._getMainRigidBody();
|
|
if (rb0) {
|
|
const vx = rb0.linearVelocity.x;
|
|
const vy = rb0.linearVelocity.y;
|
|
const speed = Math.sqrt(vx * vx + vy * vy);
|
|
const ang = Math.abs(rb0.angularVelocity);
|
|
if (speed >= this.settleVelThreshold || ang >= this.settleAngThreshold || Math.abs(vy) >= this.settleVyThreshold) {
|
|
// 上一帧物理结束后仍明显在动/竖直弹跳:退回接触态重新累积静止帧
|
|
this._pendingSettle = false;
|
|
this._pendingSettleLeft = 0;
|
|
this._stillFrames = 0;
|
|
return;
|
|
}
|
|
}
|
|
this._pendingSettleLeft--;
|
|
if (this._pendingSettleLeft > 0) return;
|
|
this._pendingSettle = false;
|
|
this._settled = true;
|
|
this._settledAtMs = Date.now();
|
|
this.playBlockSpine(TetriBlockSpineState.Settled);
|
|
this._inheritStackSupportFromContacts();
|
|
this.onSettled?.();
|
|
if (this._isVineMode()) {
|
|
this._enterVineGroup(this._ensureVineGroupId());
|
|
}
|
|
return;
|
|
}
|
|
const rb = this._getMainRigidBody();
|
|
if (!rb) return;
|
|
|
|
if (this._settled) {
|
|
// 已落定:快速衰减横向移动和旋转,纵向不干预
|
|
// 藤蔓合并成组后不要再每帧改速度,否则会与物理约束「对打」导致一直微抖
|
|
if (!this.vineGroupLinked) {
|
|
const vel = rb.linearVelocity;
|
|
if (Math.abs(vel.x) > 0.5) {
|
|
rb.linearVelocity = new Vec2(vel.x * Math.exp(-this.settledDamp * dt), vel.y);
|
|
}
|
|
if (Math.abs(rb.angularVelocity) > 0.5) {
|
|
rb.angularVelocity *= Math.exp(-this.settledDamp * dt);
|
|
}
|
|
const speed = Math.sqrt(vel.x * vel.x + vel.y * vel.y);
|
|
if (BattlePerformance.shouldFreezeTetriPhysics()) {
|
|
// 锤子解冻后的抑制窗内禁止再冻,否则水平悬臂还没倾就冻回 Static
|
|
if (Date.now() < this._freezeSuppressUntilMs) {
|
|
this._settledSleepStreak = 0;
|
|
} else {
|
|
const settledMs = Date.now() - this._settledAtMs;
|
|
const buried = this._isBuriedBelowActiveBand();
|
|
const canFreezeSpeed =
|
|
speed < BattlePerformance.tetriFreezeMaxSpeed()
|
|
&& Math.abs(rb.angularVelocity) < BattlePerformance.tetriFreezeMaxAng()
|
|
&& Math.abs(vel.y) < BattlePerformance.tetriFreezeMaxVy()
|
|
&& this._isNearAxisAlignedForFreeze()
|
|
&& settledMs >= BattlePerformance.tetriFreezeMinSettledMs(buried);
|
|
if (canFreezeSpeed) {
|
|
if (this._settledSleepStreak === 0) {
|
|
this._freezeQuietStartAngle = this.node.angle;
|
|
} else if (this._quietAngleDriftExceeded()) {
|
|
this._settledSleepStreak = 0;
|
|
this._freezeQuietStartAngle = this.node.angle;
|
|
return;
|
|
}
|
|
this._settledSleepStreak++;
|
|
if (this._settledSleepStreak >= BattlePerformance.tetriFreezeSleepStreak(buried) && !this._freezeScheduled) {
|
|
this._freezeScheduled = true;
|
|
this.scheduleOnce(() => {
|
|
this._freezeScheduled = false;
|
|
this._freezeSettledPhysics();
|
|
}, 0);
|
|
}
|
|
} else {
|
|
this._settledSleepStreak = 0;
|
|
}
|
|
}
|
|
} else if (speed < 3 && Math.abs(rb.angularVelocity) < 2 && Math.abs(vel.y) < 4) {
|
|
// 不走 Static 冻结时也不允许 Box2D sleep,否则濒倾块会「睡着站着」
|
|
rb.allowSleep = false;
|
|
} else {
|
|
this._settledSleepStreak = 0;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
this._prevVy = rb.linearVelocity.y;
|
|
|
|
if (this._contactNotified) {
|
|
// 接触后衰减横向速度,竖向不受影响
|
|
const vel = rb.linearVelocity;
|
|
if (Math.abs(vel.x) > 0.5) {
|
|
rb.linearVelocity = new Vec2(vel.x * Math.exp(-this.contactXDamp * dt), vel.y);
|
|
}
|
|
|
|
const speed = Math.sqrt(vel.x * vel.x + vel.y * vel.y);
|
|
if (speed < this.settleVelThreshold && Math.abs(rb.angularVelocity) < this.settleAngThreshold && Math.abs(vel.y) < this.settleVyThreshold) {
|
|
this._stillFrames++;
|
|
if (this._stillFrames >= this.settleStillFrames) {
|
|
this.settle();
|
|
}
|
|
} else {
|
|
this._stillFrames = 0;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
/**碰到地面/已落定方块时调用,让物理继续运行直到真正静止 */
|
|
public notifyContact(): void {
|
|
if (this._settled || this._contactNotified) return;
|
|
this._contactNotified = true;
|
|
this._stillFrames = 0;
|
|
// 接触后仍禁止 sleep:Box2D 睡眠会把「角速度≈0 但力矩未平衡」的块钉死,表现为该滚不滚
|
|
const rb = this.node.getComponent(RigidBody2D);
|
|
if (rb) {
|
|
rb.allowSleep = false;
|
|
// 略提阻尼便于堆叠收敛;过高会让下落/滑移发黏(重力阶段主要仍由 tetriMap 重力控制)
|
|
if (rb.linearDamping < 1) rb.linearDamping = 1;
|
|
if (rb.angularDamping < 1) rb.angularDamping = 0;
|
|
}
|
|
if (this._isVineMode()) {
|
|
this._enterVineGroup(this._ensureVineGroupId());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 连续若干帧低于阈值后进入「待确认落稳」;再连续 settlePostConfirmFrames 帧(每帧读的是上一帧物理后的速度)
|
|
* 仍满足才真正 _settled 并触发 onSettled,减轻堆叠后短时冲量造成的误判。
|
|
*/
|
|
public settle(): void {
|
|
if (this._settled || this._pendingSettle) return;
|
|
this._pendingSettle = true;
|
|
this._pendingSettleLeft = Math.max(1, Math.floor(this.settlePostConfirmFrames));
|
|
const rb = this._getMainRigidBody();
|
|
if (rb) rb.allowSleep = false;
|
|
|
|
//添加方块的武器
|
|
this.addWeapon();
|
|
}
|
|
|
|
addWeapon() {
|
|
//获取方块的武器
|
|
const weapons = this.node.getComponentsInChildren(tetriWeaponCarrier);
|
|
if (weapons.length == 1) {
|
|
let weapon = weapons[0]
|
|
let weaponId = weapon.getCurWeaponId()
|
|
if (weaponId) {
|
|
gg.game.CurentBattle.addCurUseWeaponArr([weaponId]);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/** 相对 90° 网格的倾斜角(0~45) */
|
|
private _tiltFromAxisAlignedDeg(): number {
|
|
const a = ((this.node.angle % 90) + 90) % 90;
|
|
return Math.min(a, 90 - a);
|
|
}
|
|
|
|
private _isNearAxisAlignedForFreeze(): boolean {
|
|
return this._tiltFromAxisAlignedDeg() <= BattlePerformance.tetriFreezeMaxTiltFromGridDeg();
|
|
}
|
|
|
|
private _quietAngleDriftExceeded(): boolean {
|
|
const drift = Math.abs(this.node.angle - this._freezeQuietStartAngle);
|
|
const wrapped = Math.min(drift, 360 - drift);
|
|
return wrapped > BattlePerformance.tetriFreezeMaxQuietAngleDriftDeg();
|
|
}
|
|
|
|
/** 是否在堆顶活跃带以下(可更快冻 Static 省算力) */
|
|
private _isBuriedBelowActiveBand(): boolean {
|
|
const map = gg.game?.CurentBattle?.TetraMap;
|
|
if (!map?.getStackTopWorldYMax || !this.node?.isValid) return false;
|
|
const top = map.getStackTopWorldYMax();
|
|
const band = BattlePerformance.tetriFreezeActiveBandPx();
|
|
return this.node.worldPosition.y < top - band;
|
|
}
|
|
|
|
/** 落稳且低速一段时间后冻结为 Static,减轻叠塔后 Box2D 持续解算 */
|
|
private _freezeSettledPhysics(): void {
|
|
if (!BattlePerformance.shouldFreezeTetriPhysics()) return;
|
|
if (this._physicsFrozen || !this._settled || this.vineGroupLinked || !this.node?.isValid) return;
|
|
const rb = this._getMainRigidBody();
|
|
if (!rb?.isValid || rb.type !== ERigidBody2DType.Dynamic) return;
|
|
// 调度到下一帧时再验一次:倾斜/慢倾/仍在动则放弃,避免「看着该滚却冻住」
|
|
const vel = rb.linearVelocity;
|
|
const speed = Math.sqrt(vel.x * vel.x + vel.y * vel.y);
|
|
const motionOk =
|
|
speed < BattlePerformance.tetriFreezeMaxSpeed()
|
|
&& Math.abs(rb.angularVelocity) < BattlePerformance.tetriFreezeMaxAng()
|
|
&& Math.abs(vel.y) < BattlePerformance.tetriFreezeMaxVy();
|
|
if (!motionOk || !this._isNearAxisAlignedForFreeze() || this._quietAngleDriftExceeded()) {
|
|
this._settledSleepStreak = 0;
|
|
return;
|
|
}
|
|
this._physicsFrozen = true;
|
|
rb.linearVelocity = new Vec2(0, 0);
|
|
rb.angularVelocity = 0;
|
|
rb.gravityScale = 0;
|
|
rb.fixedRotation = true;
|
|
rb.allowSleep = true;
|
|
rb.type = ERigidBody2DType.Static;
|
|
}
|
|
|
|
/**
|
|
* 恢复 Dynamic。锤子解冻须给倾覆留时间:重置落稳计时并短时禁止再冻 Static。
|
|
* @param suppressFreezeMs 禁止再冻时长;锤子唤醒默认 2s,普通撞击可传更短或不传(默认 1.2s)
|
|
*/
|
|
public unfreezePhysicsForImpact(suppressFreezeMs: number = 1200): void {
|
|
if (!this._physicsFrozen || !this._settled || !this.node?.isValid) return;
|
|
this._physicsFrozen = false;
|
|
this._settledSleepStreak = 0;
|
|
// 原先 _settledAtMs 很旧时,解冻后会立刻满足「落稳已久」又在几十帧内冻回 Static,悬臂来不及倾
|
|
this._settledAtMs = Date.now();
|
|
this._freezeSuppressUntilMs = Date.now() + Math.max(0, suppressFreezeMs);
|
|
const rb = this._getMainRigidBody();
|
|
if (!rb?.isValid) return;
|
|
const map = gg.game?.CurentBattle?.TetraMap;
|
|
rb.type = ERigidBody2DType.Dynamic;
|
|
rb.gravityScale = map?.fallGravityScale ?? 1.15;
|
|
rb.linearDamping = map?.settledLinearDamping ?? 1.2;
|
|
rb.angularDamping = map?.settledAngularDamping ?? 0.45;
|
|
rb.fixedRotation = false;
|
|
rb.allowSleep = false;
|
|
rb.wakeUp();
|
|
}
|
|
|
|
/** 已是 Dynamic 的落稳块:锤子后同样禁止立刻再冻,并清零冻结计数 */
|
|
public suppressSettledFreezeForMs(ms: number): void {
|
|
if (!this._settled || !this.node?.isValid) return;
|
|
this._settledSleepStreak = 0;
|
|
this._settledAtMs = Date.now();
|
|
this._freezeSuppressUntilMs = Date.now() + Math.max(0, ms);
|
|
const rb = this._getMainRigidBody();
|
|
if (rb?.isValid && rb.type === ERigidBody2DType.Dynamic) {
|
|
rb.fixedRotation = false;
|
|
rb.allowSleep = false;
|
|
rb.wakeUp();
|
|
}
|
|
}
|
|
|
|
private _unfreezeQueued = false;
|
|
private readonly _deferredUnfreezeCb = (): void => {
|
|
this._unfreezeQueued = false;
|
|
this.unfreezePhysicsForImpact();
|
|
};
|
|
|
|
/** Box2D 禁止在 BeginContact 回调里 setType,延后到下一帧解冻 */
|
|
public deferUnfreezePhysicsForImpact(): void {
|
|
if (!this._physicsFrozen || !this.node?.isValid || this._unfreezeQueued) return;
|
|
this._unfreezeQueued = true;
|
|
this.scheduleOnce(this._deferredUnfreezeCb, 0);
|
|
}
|
|
|
|
protected onDestroy(): void {
|
|
this.unschedule(this._deferredUnfreezeCb);
|
|
this._stopVineAssimilateLoop();
|
|
this._stopVineJointStillWait();
|
|
this.unschedule(this._vineMergeAfterSettleCb);
|
|
this._unbindContactEvents();
|
|
}
|
|
|
|
private _isVineMode(): boolean {
|
|
return this.config?.type === TetriType.Tetris_Basevine || this._vineGroupId > 0;
|
|
}
|
|
|
|
/** 藤蔓源块或已被藤蔓并组的方块 */
|
|
public isVineMode(): boolean {
|
|
return this._isVineMode();
|
|
}
|
|
|
|
private _ensureVineGroupId(): number {
|
|
if (this._vineGroupId <= 0) {
|
|
this._vineGroupId = tetriNode._nextVineGroupId++;
|
|
}
|
|
return this._vineGroupId;
|
|
}
|
|
|
|
private _enterVineGroup(groupId: number): void {
|
|
if (groupId <= 0) return;
|
|
this._vineGroupId = groupId;
|
|
// vineGroupLinked 仅在实际并组成单刚体后置 true;此处只展示藤蔓外观
|
|
this.showTengman();
|
|
if (this._isVineAssimilateSource && this._settled) {
|
|
this._vineAssimilateAttemptCount = 0;
|
|
const delay = Math.max(0.1, this.vineAssimilateStartDelaySec);
|
|
this.unschedule(this._vineMergeAfterSettleCb);
|
|
this.scheduleOnce(this._vineMergeAfterSettleCb, delay);
|
|
this._scheduleVineAssimilateStart();
|
|
} else if (!this._isVineAssimilateSource) {
|
|
this._stopVineAssimilateLoop();
|
|
this._stopVineJointStillWait();
|
|
}
|
|
}
|
|
|
|
private _vineMergeAfterSettleCb = (): void => {
|
|
if (!this.node?.isValid || !this._isVineAssimilateSource || this.vineGroupLinked) return;
|
|
this._tryMergeVineNeighbors();
|
|
};
|
|
|
|
private _scheduleVineAssimilateStart(): void {
|
|
if (this._vineAssimilateActive || this._vineAssimilateStartPending) return;
|
|
const delay = Math.max(0, this.vineAssimilateStartDelaySec);
|
|
if (delay <= 0) {
|
|
this._startVineAssimilateLoop();
|
|
return;
|
|
}
|
|
this._vineAssimilateStartPending = true;
|
|
this.unschedule(this._startVineAssimilateAfterDelay);
|
|
this.scheduleOnce(this._startVineAssimilateAfterDelay, delay);
|
|
}
|
|
|
|
private _startVineAssimilateAfterDelay = (): void => {
|
|
this._vineAssimilateStartPending = false;
|
|
if (!this.node?.isValid) return;
|
|
if (!this._isVineMode() || !this._isVineAssimilateSource) return;
|
|
if (!this._settled) return;
|
|
if (!this._getTouchingTetriNodesNow().length) return;
|
|
this._startVineAssimilateLoop();
|
|
};
|
|
|
|
private _startVineAssimilateLoop(): void {
|
|
if (this._vineAssimilateActive) return;
|
|
this._vineAssimilateActive = true;
|
|
this._vineAssimilateStartPending = false;
|
|
this.unschedule(this._startVineAssimilateAfterDelay);
|
|
this.unschedule(this._vineAssimilateTick);
|
|
this.schedule(this._vineAssimilateTick, Math.max(0.02, this.vineAssimilateIntervalSec));
|
|
this._vineAssimilateTick();
|
|
}
|
|
|
|
private _stopVineAssimilateLoop(): void {
|
|
this.unschedule(this._startVineAssimilateAfterDelay);
|
|
this._vineAssimilateStartPending = false;
|
|
if (!this._vineAssimilateActive) return;
|
|
this.unschedule(this._vineAssimilateTick);
|
|
this._vineAssimilateActive = false;
|
|
this._vinePairNearStillStreak.clear();
|
|
this._vinePendingPartners.clear();
|
|
this._vineAssimilateAttemptCount = 0;
|
|
}
|
|
|
|
private _vineAssimilateTick = (): void => {
|
|
if (!this.node?.isValid) {
|
|
this._stopVineAssimilateLoop();
|
|
return;
|
|
}
|
|
if (!this._isVineMode() || !this._isVineAssimilateSource) {
|
|
this._stopVineAssimilateLoop();
|
|
return;
|
|
}
|
|
if (!this._settled) return;
|
|
if (this.vineGroupLinked) {
|
|
this._stopVineAssimilateLoop();
|
|
return;
|
|
}
|
|
this._vineAssimilateAttemptCount++;
|
|
if (this._vineAssimilateAttemptCount > tetriNode.VINE_ASSIMILATE_MAX_ATTEMPTS) {
|
|
this._stopVineAssimilateLoop();
|
|
return;
|
|
}
|
|
this._tryMergeVineNeighbors();
|
|
};
|
|
|
|
private _startVineJointStillWait(): void {
|
|
if (this._vineJointWaitActive) return;
|
|
this._vineJointWaitActive = true;
|
|
this._vineJointStillConsec = 0;
|
|
this._vineJointDeadlineMs = Date.now() + Math.max(0, this.vineJointMaxWaitSec) * 1000;
|
|
// 落稳后立即开始每帧轮询,直到“连续静止达标”或超时
|
|
this.unschedule(this._vineJointStillPollingTick);
|
|
this.schedule(this._vineJointStillPollingTick, 0);
|
|
}
|
|
|
|
private _stopVineJointStillWait(): void {
|
|
if (!this._vineJointWaitActive) return;
|
|
this.unschedule(this._vineJointStillPollingTick);
|
|
this._vineJointWaitActive = false;
|
|
this._vineJointDeadlineMs = 0;
|
|
this._vineJointStillConsec = 0;
|
|
}
|
|
|
|
private _vineJointStillPollingTick = (): void => {
|
|
if (!this.node?.isValid) {
|
|
this._stopVineJointStillWait();
|
|
return;
|
|
}
|
|
if (!this._settled) {
|
|
this._stopVineJointStillWait();
|
|
return;
|
|
}
|
|
if (this.vineGroupLinked) {
|
|
this._stopVineJointStillWait();
|
|
return;
|
|
}
|
|
|
|
const now = Date.now();
|
|
if (this._vineJointDeadlineMs > 0 && now >= this._vineJointDeadlineMs) {
|
|
this._stopVineJointStillWait();
|
|
this._scheduleVineAssimilateStart();
|
|
return;
|
|
}
|
|
|
|
const rb = this._getMainRigidBody();
|
|
if (!rb?.isValid) {
|
|
this._stopVineJointStillWait();
|
|
return;
|
|
}
|
|
|
|
// 没有接触对象时不累计静止帧,避免“空中静止”也触发关节逻辑
|
|
const touching = this._getTouchingTetriNodesNow();
|
|
if (!touching.length) {
|
|
this._vineJointStillConsec = 0;
|
|
return;
|
|
}
|
|
|
|
if (this._isRigidbodyMostlyStillForJoint(rb)) {
|
|
this._vineJointStillConsec++;
|
|
} else {
|
|
this._vineJointStillConsec = 0;
|
|
return;
|
|
}
|
|
|
|
const needConsec = Math.max(1, Math.floor(this.vineJointStillConsecutiveFrames));
|
|
if (this._vineJointStillConsec < needConsec) return;
|
|
|
|
this._runVineJointTouchAndConnect();
|
|
if (this.vineGroupLinked) {
|
|
this._stopVineJointStillWait();
|
|
return;
|
|
}
|
|
|
|
// 达到静止帧但当前仍未连上(例如接触对象均不满足连接规则),继续等待到超时
|
|
this._vineJointStillConsec = 0;
|
|
};
|
|
|
|
private _isRigidbodyMostlyStillForJoint(rb: RigidBody2D | null): boolean {
|
|
if (!rb?.isValid) return true;
|
|
const vx = rb.linearVelocity.x;
|
|
const vy = rb.linearVelocity.y;
|
|
const speed = Math.sqrt(vx * vx + vy * vy);
|
|
return (
|
|
speed < this.vineJointStillSpeedThreshold &&
|
|
Math.abs(vy) < this.vineJointStillVyThreshold &&
|
|
Math.abs(rb.angularVelocity) < this.vineJointStillAngThreshold
|
|
);
|
|
}
|
|
|
|
private _isRigidbodyNearStillForVine(rb: RigidBody2D | null): boolean {
|
|
if (!rb?.isValid) return false;
|
|
const vx = rb.linearVelocity.x;
|
|
const vy = rb.linearVelocity.y;
|
|
const speed = Math.sqrt(vx * vx + vy * vy);
|
|
return (
|
|
speed < this.vineJointNearStillSpeedThreshold &&
|
|
Math.abs(vy) < this.vineJointNearStillVyThreshold &&
|
|
Math.abs(rb.angularVelocity) < this.vineJointNearStillAngThreshold
|
|
);
|
|
}
|
|
|
|
private _isPairNearStillForVine(a: RigidBody2D | null, b: RigidBody2D | null): boolean {
|
|
if (!a?.isValid || !b?.isValid) return false;
|
|
if (!this._isRigidbodyNearStillForVine(a) || !this._isRigidbodyNearStillForVine(b)) return false;
|
|
const rvx = a.linearVelocity.x - b.linearVelocity.x;
|
|
const rvy = a.linearVelocity.y - b.linearVelocity.y;
|
|
const relativeSpeed = Math.sqrt(rvx * rvx + rvy * rvy);
|
|
return relativeSpeed < this.vineJointNearStillRelativeSpeedThreshold;
|
|
}
|
|
|
|
private _isPairSafeToEnableJoint(a: RigidBody2D | null, b: RigidBody2D | null): boolean {
|
|
if (!a?.isValid || !b?.isValid) return false;
|
|
return this._isPairNearStillForVine(a, b);
|
|
}
|
|
|
|
/** 合并前:贴边接触允许;有重叠时仅拒绝两轴都深嵌的情况 */
|
|
private _isMergeOverlapAcceptable(partner: tetriNode): boolean {
|
|
const aCols = this._getAllColliders2D(this.node);
|
|
const bCols = this._getAllColliders2D(partner.node);
|
|
const a = this._unionWorldAabb(aCols);
|
|
const b = this._unionWorldAabb(bCols);
|
|
if (!a || !b) return false;
|
|
|
|
const ox = Math.min(a.xMax, b.xMax) - Math.max(a.xMin, b.xMin);
|
|
const oy = Math.min(a.yMax, b.yMax) - Math.max(a.yMin, b.yMin);
|
|
|
|
if (ox <= 0 || oy <= 0) {
|
|
const gap = this._calcColliderEdgeGapDistancePx(aCols, bCols);
|
|
const maxGap = Math.max(0, this.vineAssimilateMaxGapPx);
|
|
return Number.isFinite(gap) && gap <= maxGap;
|
|
}
|
|
|
|
const maxPx = Math.max(1, this.vineMergeMaxOverlapPx);
|
|
const thin = Math.min(ox, oy);
|
|
const thick = Math.max(ox, oy);
|
|
// 贴面接触:一轴大(共享面)、一轴薄 → 允许;两轴都厚 → 深嵌拒绝
|
|
if (thin <= maxPx) return true;
|
|
return thick <= maxPx * 1.5;
|
|
}
|
|
|
|
private _isPartnerReadyForVineMerge(partner: tetriNode): boolean {
|
|
if (partner._settled) return true;
|
|
if (!partner._contactNotified) return false;
|
|
return this._isRigidbodyNearStillForVine(partner._getMainRigidBody());
|
|
}
|
|
|
|
/** 落稳后扫描邻块并尝试合并(单刚体并组) */
|
|
private _tryMergeVineNeighbors(): boolean {
|
|
if (!this._settled || !this._isVineAssimilateSource || this.vineGroupLinked) return false;
|
|
|
|
const now = Date.now();
|
|
if (now < this._vineNextLinkAllowedAtMs) return false;
|
|
|
|
const touching = this._getTouchingTetriNodesNow();
|
|
if (!touching.length) return false;
|
|
|
|
const candidates = this._collectVineCandidates(touching);
|
|
const groupId = this._ensureVineGroupId();
|
|
const myRb = this._getMainRigidBody();
|
|
|
|
candidates.sort((a, b) => {
|
|
const sx = this.node.worldPosition.x;
|
|
const sy = this.node.worldPosition.y;
|
|
const dax = sx - a.node.worldPosition.x;
|
|
const day = sy - a.node.worldPosition.y;
|
|
const dbx = sx - b.node.worldPosition.x;
|
|
const dby = sy - b.node.worldPosition.y;
|
|
return (dax * dax + day * day) - (dbx * dbx + dby * dby);
|
|
});
|
|
|
|
for (const p of candidates) {
|
|
if (!this._canVineConnectToPartner(p) || p._vineGroupId === groupId) continue;
|
|
if (!this._isTouchingOrWithinGapNow(p)) continue;
|
|
if (!this._isPartnerReadyForVineMerge(p)) continue;
|
|
|
|
const bothSettled = this._settled && p._settled;
|
|
if (!bothSettled) {
|
|
const pRb = p._getMainRigidBody();
|
|
if (!myRb?.isValid || !pRb?.isValid || !this._isPairNearStillForVine(myRb, pRb)) continue;
|
|
}
|
|
if (!bothSettled && !this._isMergeOverlapAcceptable(p)) continue;
|
|
|
|
if (this._tryMergeIntoSingleBody(p, groupId)) {
|
|
p._enterVineGroup(groupId);
|
|
this._vineNextLinkAllowedAtMs = now + Math.max(0, this.vineJointLinkCooldownSec) * 1000;
|
|
this._isVineAssimilateSource = false;
|
|
this._stopVineAssimilateLoop();
|
|
this._stopVineJointStillWait();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private _markPairNearStillAndReady(partner: tetriNode, nearStill: boolean): boolean {
|
|
const key = partner.node?.uuid ?? '';
|
|
if (!key) return false;
|
|
if (!nearStill) {
|
|
this._vinePairNearStillStreak.delete(key);
|
|
return false;
|
|
}
|
|
const next = (this._vinePairNearStillStreak.get(key) ?? 0) + 1;
|
|
this._vinePairNearStillStreak.set(key, next);
|
|
const need = Math.max(1, Math.floor(this.vineJointNearStillNeedConsecutiveTicks));
|
|
return next >= need;
|
|
}
|
|
|
|
private _queuePendingPartner(partner: tetriNode): void {
|
|
const key = partner.node?.uuid ?? '';
|
|
if (!key) return;
|
|
this._vinePendingPartners.set(key, partner);
|
|
}
|
|
|
|
private _isTouchingPartnerNow(partner: tetriNode): boolean {
|
|
const c0 = this._touchingByContactCount.get(partner) ?? 0;
|
|
if (c0 > 0) return true;
|
|
const c1 = partner._touchingByContactCount.get(this) ?? 0;
|
|
if (c1 > 0) return true;
|
|
return this._hasPhysicalOverlapWith(partner.node);
|
|
}
|
|
|
|
private _cross2(ax: number, ay: number, bx: number, by: number): number {
|
|
return ax * by - ay * bx;
|
|
}
|
|
|
|
private _pointInPolygonWorld(pt: Vec2, poly: Vec2[]): boolean {
|
|
let inside = false;
|
|
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
const xi = poly[i].x;
|
|
const yi = poly[i].y;
|
|
const xj = poly[j].x;
|
|
const yj = poly[j].y;
|
|
const intersect = ((yi > pt.y) !== (yj > pt.y))
|
|
&& (pt.x < (xj - xi) * (pt.y - yi) / ((yj - yi) || 1e-6) + xi);
|
|
if (intersect) inside = !inside;
|
|
}
|
|
return inside;
|
|
}
|
|
|
|
private _segmentsIntersect(a1: Vec2, a2: Vec2, b1: Vec2, b2: Vec2): boolean {
|
|
const abx = a2.x - a1.x;
|
|
const aby = a2.y - a1.y;
|
|
const acx = b1.x - a1.x;
|
|
const acy = b1.y - a1.y;
|
|
const adx = b2.x - a1.x;
|
|
const ady = b2.y - a1.y;
|
|
const cdx = b2.x - b1.x;
|
|
const cdy = b2.y - b1.y;
|
|
const cax = a1.x - b1.x;
|
|
const cay = a1.y - b1.y;
|
|
const cbx = a2.x - b1.x;
|
|
const cby = a2.y - b1.y;
|
|
const d1 = this._cross2(abx, aby, acx, acy);
|
|
const d2 = this._cross2(abx, aby, adx, ady);
|
|
const d3 = this._cross2(cdx, cdy, cax, cay);
|
|
const d4 = this._cross2(cdx, cdy, cbx, cby);
|
|
return d1 * d2 <= 0 && d3 * d4 <= 0;
|
|
}
|
|
|
|
private _pointSegmentDistSq(p: Vec2, a: Vec2, b: Vec2): number {
|
|
const abx = b.x - a.x;
|
|
const aby = b.y - a.y;
|
|
const apx = p.x - a.x;
|
|
const apy = p.y - a.y;
|
|
const abLenSq = abx * abx + aby * aby;
|
|
if (abLenSq <= 1e-8) {
|
|
const dx = p.x - a.x;
|
|
const dy = p.y - a.y;
|
|
return dx * dx + dy * dy;
|
|
}
|
|
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / abLenSq));
|
|
const qx = a.x + abx * t;
|
|
const qy = a.y + aby * t;
|
|
const dx = p.x - qx;
|
|
const dy = p.y - qy;
|
|
return dx * dx + dy * dy;
|
|
}
|
|
|
|
private _segmentSegmentDistSq(a1: Vec2, a2: Vec2, b1: Vec2, b2: Vec2): number {
|
|
if (this._segmentsIntersect(a1, a2, b1, b2)) return 0;
|
|
return Math.min(
|
|
this._pointSegmentDistSq(a1, b1, b2),
|
|
this._pointSegmentDistSq(a2, b1, b2),
|
|
this._pointSegmentDistSq(b1, a1, a2),
|
|
this._pointSegmentDistSq(b2, a1, a2),
|
|
);
|
|
}
|
|
|
|
private _getPolygonWorldPoints(col: Collider2D): Vec2[] | null {
|
|
if (!(col instanceof PolygonCollider2D) || !col?.node?.isValid || !col.enabled) return null;
|
|
const pts = col.points ?? [];
|
|
if (pts.length < 3) return null;
|
|
const wm = new Mat4();
|
|
col.node.getWorldMatrix(wm);
|
|
const wp = new Vec3();
|
|
const out: Vec2[] = [];
|
|
for (const p of pts) {
|
|
Vec3.transformMat4(wp, new Vec3(p.x, p.y, 0), wm);
|
|
out.push(new Vec2(wp.x, wp.y));
|
|
}
|
|
return out.length >= 3 ? out : null;
|
|
}
|
|
|
|
/** 真实碰撞器(多边形)边沿最短距离;重叠/穿插返回 0 */
|
|
private _calcColliderEdgeGapDistancePx(aCols: Collider2D[], bCols: Collider2D[]): number {
|
|
const aPolys: Vec2[][] = [];
|
|
const bPolys: Vec2[][] = [];
|
|
for (const c of aCols) {
|
|
const p = this._getPolygonWorldPoints(c);
|
|
if (p) aPolys.push(p);
|
|
}
|
|
for (const c of bCols) {
|
|
const p = this._getPolygonWorldPoints(c);
|
|
if (p) bPolys.push(p);
|
|
}
|
|
if (!aPolys.length || !bPolys.length) return Number.POSITIVE_INFINITY;
|
|
|
|
let best = Number.POSITIVE_INFINITY;
|
|
for (const pa of aPolys) {
|
|
for (const pb of bPolys) {
|
|
// 多边形包含时边沿距离为 0
|
|
if (this._pointInPolygonWorld(pa[0], pb) || this._pointInPolygonWorld(pb[0], pa)) {
|
|
return 0;
|
|
}
|
|
for (let i = 0; i < pa.length; i++) {
|
|
const a1 = pa[i];
|
|
const a2 = pa[(i + 1) % pa.length];
|
|
for (let j = 0; j < pb.length; j++) {
|
|
const b1 = pb[j];
|
|
const b2 = pb[(j + 1) % pb.length];
|
|
const d2 = this._segmentSegmentDistSq(a1, a2, b1, b2);
|
|
if (d2 < best) {
|
|
best = d2;
|
|
if (best <= 1e-8) return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return Math.sqrt(best);
|
|
}
|
|
|
|
private _isWithinAssimilateGapNow(partner: tetriNode, maxGapPx: number): boolean {
|
|
if (!partner?.node?.isValid || maxGapPx <= 0) return false;
|
|
const aCols = this._getAllColliders2D(this.node);
|
|
const bCols = this._getAllColliders2D(partner.node);
|
|
if (!aCols.length || !bCols.length) return false;
|
|
const gap = this._calcColliderEdgeGapDistancePx(aCols, bCols);
|
|
return Number.isFinite(gap) && gap <= maxGapPx;
|
|
}
|
|
|
|
private _isTouchingOrWithinGapNow(partner: tetriNode): boolean {
|
|
if (this._isTouchingPartnerNow(partner)) return true;
|
|
return this._isWithinAssimilateGapNow(partner, Math.max(0, this.vineAssimilateMaxGapPx));
|
|
}
|
|
|
|
/** 感染候选:真实接触 + 允许间隙内的邻近方块 */
|
|
private _collectVineCandidates(baseTouching: tetriNode[]): tetriNode[] {
|
|
const out = new Map<string, tetriNode>();
|
|
for (const t of baseTouching) {
|
|
const key = t?.node?.uuid ?? '';
|
|
if (key) out.set(key, t);
|
|
}
|
|
const maxGap = Math.max(0, this.vineAssimilateMaxGapPx);
|
|
if (maxGap <= 0) return Array.from(out.values());
|
|
const scene = this.node.scene;
|
|
if (!scene) return Array.from(out.values());
|
|
const all = scene.getComponentsInChildren(tetriNode) ?? [];
|
|
for (const t of all) {
|
|
if (!t?.node?.isValid || t === this) continue;
|
|
const key = t.node.uuid;
|
|
if (!key || out.has(key)) continue;
|
|
if (this._isWithinAssimilateGapNow(t, maxGap)) {
|
|
out.set(key, t);
|
|
}
|
|
}
|
|
return Array.from(out.values());
|
|
}
|
|
|
|
/** 只要有配置都允许被藤蔓感染并绑定 */
|
|
private _canVineConnectToPartner(t: tetriNode): boolean {
|
|
return !!t?.config;
|
|
}
|
|
|
|
/** @deprecated 由 _tryMergeVineNeighbors 替代,保留供静止轮询兜底调用 */
|
|
private _runVineJointTouchAndConnect(): void {
|
|
this._tryMergeVineNeighbors();
|
|
}
|
|
|
|
private _getMainRigidBody(): RigidBody2D | null {
|
|
return this.node.getComponent(RigidBody2D) ?? this.node.getComponentInChildren(RigidBody2D);
|
|
}
|
|
|
|
/**
|
|
* 视口/分辨率变化后:节点→刚体(Canvas 父级缩放不会自动同步到 Box2D)。
|
|
* 禁止 rb.enabled 切换(会把刚体旧坐标写回节点导致塌塔/错位)。
|
|
*/
|
|
public syncRigidBodyPoseFromNode(zeroLinearVel = true): void {
|
|
const rb = this._getMainRigidBody();
|
|
if (!rb?.isValid || !this.node?.isValid) return;
|
|
const savedVy = rb.linearVelocity.y;
|
|
const savedVx = rb.linearVelocity.x;
|
|
|
|
this._pushNodeTransformToPhysics(rb);
|
|
|
|
if (this._physicsFrozen) {
|
|
rb.type = ERigidBody2DType.Static;
|
|
rb.gravityScale = 0;
|
|
rb.fixedRotation = true;
|
|
rb.allowSleep = true;
|
|
}
|
|
|
|
rb.angularVelocity = 0;
|
|
if (zeroLinearVel) {
|
|
rb.linearVelocity = new Vec2(0, 0);
|
|
} else {
|
|
rb.linearVelocity = new Vec2(savedVx, savedVy);
|
|
}
|
|
this._coldRefreshCollidersOnNode(this.node);
|
|
}
|
|
|
|
private _coldRefreshCollidersOnNode(root: Node): void {
|
|
const cols = root.getComponentsInChildren(PolygonCollider2D) ?? [];
|
|
for (let i = 0; i < cols.length; i++) {
|
|
const col = cols[i];
|
|
if (!col?.isValid) continue;
|
|
const was = col.enabled;
|
|
col.enabled = false;
|
|
col.enabled = was;
|
|
if (typeof col.apply === 'function') col.apply();
|
|
}
|
|
}
|
|
|
|
/** 把当前节点世界姿态写入 Box2D(不移动节点) */
|
|
private _pushNodeTransformToPhysics(rb: RigidBody2D): void {
|
|
const body = (rb as { _body?: { syncPositionToPhysics?: (resetVel?: boolean) => void } })._body;
|
|
if (body?.syncPositionToPhysics) {
|
|
body.syncPositionToPhysics(false);
|
|
return;
|
|
}
|
|
const p = this.node.position;
|
|
this.node.setPosition(p.x, p.y, p.z);
|
|
}
|
|
|
|
/** 两方块碰撞体是否有真实重叠(可选贴边小间隙兜底) */
|
|
private _hasPhysicalOverlapWith(otherRoot: Node): boolean {
|
|
const a = this._unionWorldAabb(this._getAllColliders2D(this.node));
|
|
const b = this._unionWorldAabb(this._getAllColliders2D(otherRoot));
|
|
if (a && b) {
|
|
const ix1 = Math.max(a.xMin, b.xMin);
|
|
const iy1 = Math.max(a.yMin, b.yMin);
|
|
const ix2 = Math.min(a.xMax, b.xMax);
|
|
const iy2 = Math.min(a.yMax, b.yMax);
|
|
if (ix2 >= ix1 && iy2 >= iy1) {
|
|
return true;
|
|
}
|
|
if (this.vineJointAnchorMaxGapPx > 0) {
|
|
const acx = (a.xMin + a.xMax) * 0.5;
|
|
const acy = (a.yMin + a.yMax) * 0.5;
|
|
const bcx = (b.xMin + b.xMax) * 0.5;
|
|
const bcy = (b.yMin + b.yMax) * 0.5;
|
|
const pax = Math.max(a.xMin, Math.min(a.xMax, bcx));
|
|
const pay = Math.max(a.yMin, Math.min(a.yMax, bcy));
|
|
const pbx = Math.max(b.xMin, Math.min(b.xMax, acx));
|
|
const pby = Math.max(b.yMin, Math.min(b.yMax, acy));
|
|
const dx = pax - pbx;
|
|
const dy = pay - pby;
|
|
const maxGapPx = Math.max(0, this.vineJointAnchorMaxGapPx);
|
|
if (dx * dx + dy * dy <= maxGapPx * maxGapPx) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 单刚体合并:把 partner 整块并入当前根节点,移除 partner 子树内刚体,
|
|
* 让所有 collider 统一挂到当前根刚体上。
|
|
*/
|
|
private _tryMergeIntoSingleBody(partner: tetriNode | null, groupId: number): boolean {
|
|
if (!partner?.node?.isValid || partner === this) return false;
|
|
if (!this.node?.isValid) return false;
|
|
if (!this._settled) return false;
|
|
if (!this._isPartnerReadyForVineMerge(partner)) return false;
|
|
const myRb = this._getMainRigidBody();
|
|
const partnerRb = partner._getMainRigidBody();
|
|
if (!myRb?.isValid || !partnerRb?.isValid || myRb === partnerRb) return false;
|
|
|
|
const bothSettled = this._settled && partner._settled;
|
|
if (!bothSettled && !this._isPairSafeToEnableJoint(myRb, partnerRb)) return false;
|
|
if (!this._isTouchingOrWithinGapNow(partner)) return false;
|
|
if (!bothSettled && !this._isMergeOverlapAcceptable(partner)) return false;
|
|
if (partner.node.parent === this.node) return true;
|
|
|
|
partner._stopVineAssimilateLoop();
|
|
partner._stopVineJointStillWait();
|
|
partner._unbindContactEvents();
|
|
partner._isVineAssimilateSource = false;
|
|
|
|
// 合并瞬间切 Kinematic 并清零速度,避免烘焙碰撞体时 overlap 解算把整塔弹飞
|
|
myRb.linearVelocity = new Vec2(0, 0);
|
|
myRb.angularVelocity = 0;
|
|
partnerRb.linearVelocity = new Vec2(0, 0);
|
|
partnerRb.angularVelocity = 0;
|
|
myRb.type = ERigidBody2DType.Kinematic;
|
|
partnerRb.type = ERigidBody2DType.Kinematic;
|
|
|
|
// 先并到当前根,再把 partner 子树碰撞形状烘焙到当前根刚体
|
|
partner.node.setParent(this.node, true);
|
|
this._bakePartnerCollidersToRoot(partner.node);
|
|
|
|
const partnerRbs = partner.node.getComponentsInChildren(RigidBody2D) ?? [];
|
|
for (const rb of partnerRbs) {
|
|
if (!rb?.isValid) continue;
|
|
rb.enabledContactListener = false;
|
|
rb.linearVelocity = new Vec2(0, 0);
|
|
rb.angularVelocity = 0;
|
|
rb.gravityScale = 0;
|
|
rb.allowSleep = true;
|
|
rb.enabled = false;
|
|
rb.destroy();
|
|
}
|
|
|
|
// 原子树碰撞体改为禁用,避免“无刚体碰撞体”继续参与世界静态碰撞导致穿透/错判
|
|
const oldCols = partner.node.getComponentsInChildren(PolygonCollider2D) ?? [];
|
|
for (const col of oldCols) {
|
|
if (!col?.isValid) continue;
|
|
col.enabled = false;
|
|
}
|
|
|
|
const allMerged = partner.node.getComponentsInChildren(tetriNode) ?? [];
|
|
for (const merged of allMerged) {
|
|
merged._vineGroupId = groupId;
|
|
merged.vineGroupLinked = true;
|
|
merged.showTengman();
|
|
merged._isVineAssimilateSource = false;
|
|
merged._stopVineAssimilateLoop();
|
|
merged._stopVineJointStillWait();
|
|
}
|
|
this._bindContactEvents();
|
|
this._forceNoBounceOnOwnColliders();
|
|
|
|
const map = gg.game?.CurentBattle?.TetraMap;
|
|
const fallG = map?.fallGravityScale ?? 1;
|
|
const settledLinD = map?.settledLinearDamping ?? 1.5;
|
|
const settledAngD = map?.settledAngularDamping ?? 1;
|
|
this.scheduleOnce(() => {
|
|
if (!myRb?.isValid) return;
|
|
myRb.type = ERigidBody2DType.Dynamic;
|
|
myRb.gravityScale = fallG;
|
|
myRb.linearDamping = settledLinD;
|
|
myRb.angularDamping = settledAngD;
|
|
myRb.allowSleep = true;
|
|
myRb.wakeUp();
|
|
}, 0);
|
|
this.vineGroupLinked = true;
|
|
return true;
|
|
}
|
|
|
|
private _bakePartnerCollidersToRoot(partnerRoot: Node): void {
|
|
if (!this.node?.isValid || !partnerRoot?.isValid) return;
|
|
const srcCols = partnerRoot.getComponentsInChildren(PolygonCollider2D) ?? [];
|
|
if (!srcCols.length) return;
|
|
|
|
const wm = new Mat4();
|
|
const worldP = new Vec3();
|
|
const rootLocal = new Vec3();
|
|
|
|
for (const src of srcCols) {
|
|
if (!src?.isValid || !src.enabled || !src.node?.isValid) continue;
|
|
const srcPts = src.points ?? [];
|
|
if (srcPts.length < 3) continue;
|
|
|
|
src.node.getWorldMatrix(wm);
|
|
const bakedPts: Vec2[] = [];
|
|
for (const p of srcPts) {
|
|
Vec3.transformMat4(worldP, new Vec3(p.x, p.y, 0), wm);
|
|
this.node.inverseTransformPoint(rootLocal, worldP);
|
|
bakedPts.push(new Vec2(rootLocal.x, rootLocal.y));
|
|
}
|
|
if (bakedPts.length < 3) continue;
|
|
|
|
const dst = this.node.addComponent(PolygonCollider2D);
|
|
dst.points = bakedPts;
|
|
const srcAny = src as any;
|
|
const dstAny = dst as any;
|
|
if (typeof srcAny.tag === 'number') dstAny.tag = srcAny.tag;
|
|
if (typeof srcAny.group === 'number') dstAny.group = srcAny.group;
|
|
if (typeof srcAny.sensor === 'boolean') dstAny.sensor = srcAny.sensor;
|
|
if (typeof srcAny.density === 'number') dstAny.density = srcAny.density;
|
|
if (typeof srcAny.friction === 'number') dstAny.friction = srcAny.friction;
|
|
if (typeof srcAny.restitution === 'number') dstAny.restitution = srcAny.restitution;
|
|
dst.enabled = false;
|
|
dst.enabled = true;
|
|
}
|
|
}
|
|
|
|
private _getAllColliders2D(root: Node | null): Collider2D[] {
|
|
if (!root?.isValid) return [];
|
|
const cols = root.getComponentsInChildren(Collider2D) ?? [];
|
|
return cols.filter(c => c && c.enabled && c.node?.isValid);
|
|
}
|
|
|
|
private _tryGetWorldAabb(col: Collider2D | null): Rect | null {
|
|
if (!col?.isValid) return null;
|
|
try {
|
|
const a = col.worldAABB as Rect | null;
|
|
if (!a) return null;
|
|
if (!Number.isFinite(a.xMin) || !Number.isFinite(a.xMax) || !Number.isFinite(a.yMin) || !Number.isFinite(a.yMax)) {
|
|
return null;
|
|
}
|
|
return a;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private _forceNoBounceOnOwnColliders = (): void => {
|
|
const cols = this._getAllColliders2D(this.node);
|
|
for (const c of cols) {
|
|
c.restitution = 0;
|
|
c.friction = 1;
|
|
// const anyCol = c as any;
|
|
// if (typeof anyCol.restitution === 'number') {
|
|
// anyCol.restitution = 0;
|
|
// }
|
|
// const mat = anyCol.sharedMaterial ?? anyCol.material;
|
|
// if (mat && typeof mat.restitution === 'number') {
|
|
// mat.restitution = 0;
|
|
// }
|
|
}
|
|
};
|
|
|
|
private _inflateRect(r: Rect, pad: number): Rect {
|
|
return new Rect(r.x - pad, r.y - pad, r.width + pad * 2, r.height + pad * 2);
|
|
}
|
|
|
|
private _unionWorldAabb(cols: Collider2D[]): Rect | null {
|
|
if (!cols.length) return null;
|
|
let minX = Number.POSITIVE_INFINITY;
|
|
let minY = Number.POSITIVE_INFINITY;
|
|
let maxX = Number.NEGATIVE_INFINITY;
|
|
let maxY = Number.NEGATIVE_INFINITY;
|
|
for (const c of cols) {
|
|
const a = this._tryGetWorldAabb(c);
|
|
if (!a) continue;
|
|
minX = Math.min(minX, a.xMin);
|
|
minY = Math.min(minY, a.yMin);
|
|
maxX = Math.max(maxX, a.xMax);
|
|
maxY = Math.max(maxY, a.yMax);
|
|
}
|
|
if (!Number.isFinite(minX)) return null;
|
|
return new Rect(minX, minY, maxX - minX, maxY - minY);
|
|
}
|
|
|
|
private _rectIntersectsWithMinOverlap(a: Rect, b: Rect, minOverlapPx: number): boolean {
|
|
const ox = Math.min(a.xMax, b.xMax) - Math.max(a.xMin, b.xMin);
|
|
const oy = Math.min(a.yMax, b.yMax) - Math.max(a.yMin, b.yMin);
|
|
return ox > minOverlapPx && oy > minOverlapPx;
|
|
}
|
|
|
|
private _anyAabbPairIntersects(aCols: Collider2D[], bCols: Collider2D[], pad: number, minOverlapPx: number): boolean {
|
|
for (const ca of aCols) {
|
|
const aa = this._tryGetWorldAabb(ca);
|
|
if (!aa) continue;
|
|
const aInfl = this._inflateRect(aa, pad);
|
|
for (const cb of bCols) {
|
|
const bb = this._tryGetWorldAabb(cb);
|
|
if (!bb) continue;
|
|
// 这里不再对双方都 inflate(会放大误判);只 inflate 自身,再要求有最小重叠面积
|
|
if (this._rectIntersectsWithMinOverlap(aInfl, bb as unknown as Rect, minOverlapPx)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* “当前这一刻”仍与我重叠(AABB overlap)的 tetriNode 列表。
|
|
* 说明:用所有子节点 Collider2D 做两两 AABB overlap(并带极小膨胀),比只取“第一个 collider”更接近真实接触;
|
|
* 若仍偏差,再把命中候选做多边形二次判定即可。
|
|
*/
|
|
private _getTouchingTetriNodesNow(): tetriNode[] {
|
|
// 优先用物理 contact(最严格、不会“右侧没接触也算”)
|
|
if (this._touchingByContactCount.size) {
|
|
const out: tetriNode[] = [];
|
|
for (const [t, cnt] of this._touchingByContactCount) {
|
|
if (cnt > 0 && t?.node?.isValid) out.push(t);
|
|
}
|
|
if (out.length) return out;
|
|
}
|
|
|
|
const selfCols = this._getAllColliders2D(this.node);
|
|
if (!selfCols.length) return [];
|
|
|
|
const scene = this.node.scene;
|
|
if (!scene) return [];
|
|
|
|
const all = scene.getComponentsInChildren(tetriNode) ?? [];
|
|
const out: tetriNode[] = [];
|
|
|
|
for (const t of all) {
|
|
if (!t?.node?.isValid) continue;
|
|
if (t === this) continue;
|
|
|
|
const otherCols = t._getAllColliders2D(t.node);
|
|
if (!otherCols.length) continue;
|
|
|
|
if (this._anyAabbPairIntersects(selfCols, otherCols, this._touchAabbInflatePx, this._touchAabbMinOverlapPx)) {
|
|
out.push(t);
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
private _unbindContactEvents(): void {
|
|
for (const c of this._boundContactColliders) {
|
|
if (!c?.isValid) continue;
|
|
c.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
|
|
c.off(Contact2DType.END_CONTACT, this._onEndContact, this);
|
|
}
|
|
this._boundContactColliders.length = 0;
|
|
this._touchingByContactCount.clear();
|
|
}
|
|
|
|
private _bindContactEvents = (): void => {
|
|
if (!this.node?.isValid) return;
|
|
// 重新绑定,避免 collider enable/disable 后旧监听失效或重复
|
|
this._unbindContactEvents();
|
|
const cols = this._getAllColliders2D(this.node);
|
|
for (const c of cols) {
|
|
c.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
|
|
c.on(Contact2DType.END_CONTACT, this._onEndContact, this);
|
|
this._boundContactColliders.push(c);
|
|
}
|
|
};
|
|
|
|
private _getTetriFromCollider(col: Collider2D | null): tetriNode | null {
|
|
if (!col?.node?.isValid) return null;
|
|
// collider 可能在子节点上,向上找最近的 tetriNode
|
|
let n: Node | null = col.node;
|
|
while (n) {
|
|
const t = n.getComponent(tetriNode);
|
|
if (t) return t;
|
|
n = n.parent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private _onBeginContact(selfCol: Collider2D, otherCol: Collider2D, contact: IPhysics2DContact): void {
|
|
if (!this.node?.isValid) return;
|
|
if (!contact?.isTouching?.() && contact) return;
|
|
const otherT = this._getTetriFromCollider(otherCol);
|
|
if (!otherT || otherT === this) return;
|
|
const prev = this._touchingByContactCount.get(otherT) ?? 0;
|
|
this._touchingByContactCount.set(otherT, prev + 1);
|
|
if (this._isVineMode() && this._isVineAssimilateSource) {
|
|
this._enterVineGroup(this._ensureVineGroupId());
|
|
}
|
|
}
|
|
|
|
private _onEndContact(selfCol: Collider2D, otherCol: Collider2D, contact: IPhysics2DContact): void {
|
|
if (!this.node?.isValid) return;
|
|
const otherT = this._getTetriFromCollider(otherCol);
|
|
if (!otherT || otherT === this) return;
|
|
const prev = this._touchingByContactCount.get(otherT) ?? 0;
|
|
const next = prev - 1;
|
|
if (next <= 0) this._touchingByContactCount.delete(otherT);
|
|
else this._touchingByContactCount.set(otherT, next);
|
|
}
|
|
|
|
/**触碰怪物区等:直接销毁 */
|
|
public skill(): void {
|
|
if (this._isPhysicallyMergedIntoStackRoot()) {
|
|
const primary = this._getStackPrimaryTetriNode();
|
|
if (primary && primary !== this) {
|
|
primary.skill();
|
|
return;
|
|
}
|
|
}
|
|
if (this._killSkillDispatched) return;
|
|
this._markKillSkillDispatchedOnStack();
|
|
|
|
this.excludeWholeStackMetricsSubtree();
|
|
GEvent.Ins.emit(GEvent.UpdateDps, 0);
|
|
const removalNode = this._getRemovalTargetNode();
|
|
//往下掉落0.5秒种渐隐消失移除 fix me
|
|
|
|
this.scheduleOnce(() => {
|
|
if (!removalNode?.isValid) return;
|
|
let box = removalNode.getComponent(PolygonCollider2D) ?? removalNode.getComponentInChildren(PolygonCollider2D);
|
|
if (box) {
|
|
//取消碰撞
|
|
box.enabled = false;
|
|
|
|
}
|
|
|
|
const rb = removalNode.getComponent(RigidBody2D) ?? removalNode.getComponentInChildren(RigidBody2D);
|
|
if (rb) {
|
|
// rb.linearVelocity = new Vec2(0, 0);
|
|
|
|
rb.fixedRotation = true;
|
|
rb.gravityScale = 3;
|
|
//rb.type = ERigidBody2DType.Static;
|
|
}
|
|
if (removalNode.isValid) {
|
|
this._fadeOutAndDestroy(removalNode, 'skill');
|
|
}
|
|
}, 0)
|
|
|
|
}
|
|
|
|
addLevel(delay: number = 0) {
|
|
this._tetriLevel++;
|
|
if (this._tetriLevel >= this._tetriLevelMax) {
|
|
this._tetriLevel = this._tetriLevelMax;
|
|
}
|
|
this.node.getChildByName('橙').active = false;
|
|
this.node.getChildByName('紫').active = false;
|
|
this.node.getChildByName('绿').active = false;
|
|
this.node.getChildByName('蓝').active = false;
|
|
|
|
if (this._tetriLevel == 2) {
|
|
this.node.getChildByName('绿').active = true;
|
|
} else if (this._tetriLevel == 3) {
|
|
this.node.getChildByName('蓝').active = true;
|
|
} else if (this._tetriLevel == 4) {
|
|
this.node.getChildByName('紫').active = true;
|
|
} else if (this._tetriLevel == 5) {
|
|
this.node.getChildByName('橙').active = true;
|
|
}
|
|
console.log('addLevel 我升级拉, 现在是', this._tetriLevel);
|
|
this.showUpLevelEffect(true,delay);
|
|
|
|
GEvent.Ins.emit(GEvent.UpdateDps, 1);
|
|
}
|
|
|
|
/**显示升级特效
|
|
*@param isUpLevel: true 表示升级,false 表示替换武器
|
|
*/
|
|
showUpLevelEffect(isUpLevel: boolean = true,delay: number = 0) {
|
|
if(!this.node || !this.node.isValid){
|
|
return
|
|
}
|
|
|
|
this.scheduleOnce(()=>{
|
|
let eff = gg.res.getNode(GPath.Prefab, "三选一完成格子效果", GBundle.tetriGame);
|
|
if (eff) {
|
|
gg.ui.showNode(eff, UILayer.Anim)
|
|
eff.parent = this.node;
|
|
eff.position = Vec3.ZERO;
|
|
let n1 = eff.find("武器替换");
|
|
let n2 = eff.find("升级");
|
|
n1.active = !isUpLevel;
|
|
n2.active = isUpLevel;
|
|
if (n1.active) {
|
|
n1.getComponent(sp.Skeleton).setAnimation(0, 'animation', false)
|
|
}
|
|
if (n2.active) {
|
|
n2.getComponent(sp.Skeleton).setAnimation(0, '升级', false)
|
|
}
|
|
this.scheduleOnce(() => {
|
|
gg.res.putNode(eff);
|
|
}, 1.5)
|
|
}
|
|
},delay)
|
|
|
|
}
|
|
|
|
mergeDestory() {
|
|
this.excludeWholeStackMetricsSubtree();
|
|
this.scheduleOnce(() => {
|
|
this.node.destroy();
|
|
GEvent.Ins.emit(GEvent.UpdateDps, 1);
|
|
//gg.res.putNode(this.node);
|
|
console.log('合并升级, 销毁方块普通');
|
|
}, 0)
|
|
}
|
|
|
|
canMerge(): boolean {
|
|
if (this._tetriLevel >= this._tetriLevelMax) {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
getTetriType() {
|
|
return this.config?.type;
|
|
}
|
|
/** 同形状合并用;无配置时返回 undefined,避免物理回调里读 null 报错 */
|
|
getTetriCubeType(): number | undefined {
|
|
return this.config?.cubetype;
|
|
}
|
|
|
|
|
|
refreshTengman() {
|
|
if (this.node.getChildByName('藤曼') && this.config) {
|
|
this.node.getChildByName('藤曼').active = this.config.type == TetriType.Tetris_Basevine;
|
|
}
|
|
}
|
|
|
|
|
|
showTengman() {
|
|
if (this.node.getChildByName('藤曼')) {
|
|
this.node.getChildByName('藤曼').active = true;
|
|
}
|
|
}
|
|
hideTengman() {
|
|
if (this.node.getChildByName('藤曼')) {
|
|
this.node.getChildByName('藤曼').active = false;
|
|
}
|
|
}
|
|
|
|
|
|
ChangeCubeWeapon(cubename: string, weaponid: number) {
|
|
if (cubename == this.config?.cubename) {
|
|
this._curweaponId = weaponid
|
|
// 仅对已落稳(已参与堆叠)的对应形状方块播放“武器替换”特效
|
|
if (this.isSettled && this.node?.isValid && this.node.activeInHierarchy) {
|
|
this.showUpLevelEffect(false,0.5);
|
|
}
|
|
console.log('ChangeCubeWeapon ++++ tetriNode++++++++++===', cubename, weaponid)
|
|
}
|
|
}
|
|
|
|
getNodeLevel() {
|
|
return this._tetriLevel;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|