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(); 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(); private _vineNextLinkAllowedAtMs = 0; private _vinePendingPartners = new Map(); 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(); 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; } }