消消方块阵换皮表情
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.
 
 
 

1885 lines
77 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 { tetriFloorNode } from './tetriFloorNode';
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();
}
}
}
/**
* 落稳后或周期重算时补标稳定支撑:contact 可能晚于 settle 判定,AABB 兜底避免白线/镜头卡住。
*/
public tryPromoteStackSupportStable(): boolean {
if (this._stackSupportStable || !this._settled) return false;
for (const [partner, cnt] of this._touchingByContactCount) {
if (cnt > 0 && partner?.hasStackSupportStable()) {
this.markStackSupportStable();
return true;
}
}
const map = gg.game?.CurentBattle?.TetraMap;
const table = map?._tetrisTable;
if (!table?.isValid) return false;
const selfCols = this._getAllColliders2D(this.node);
if (!selfCols.length) return false;
const pad = 1.5;
const minOv = 0.35;
for (const child of table.children) {
if (!child?.isValid || child === this.node) continue;
const t = child.getComponent(tetriNode) ?? child.getComponentInChildren(tetriNode);
if (t && t !== this && t.shouldCountTowardStackHeight()) {
const otherCols = t._getAllColliders2D(t.node);
if (otherCols.length && this._anyAabbPairIntersects(selfCols, otherCols, pad, minOv)) {
this.markStackSupportStable();
return true;
}
}
const f = child.getComponent(tetriFloorNode) ?? child.getComponentInChildren(tetriFloorNode);
if (f?.hasStackSupportStable()) {
const fCols = f.node.getComponentsInChildren(PolygonCollider2D) ?? [];
if (fCols.length && this._anyAabbPairIntersects(selfCols, fCols, pad, minOv)) {
this.markStackSupportStable();
return true;
}
}
}
return false;
}
private _inheritStackSupportFromContacts(): void {
this.tryPromoteStackSupportStable();
}
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;
}
}