import { _decorator, BoxCollider2D, Collider2D, Component, Mat4, PolygonCollider2D, tween, Tween, UITransform, Vec2, Vec3, Node } from 'cc'; import { GEvent } from '../../mx/module/event/GEvent'; import { playTetriBlockSpine, TetriBlockSpineState } from './tetriBlockSpine'; import { tetriNode } from './tetriNode'; const { ccclass } = _decorator; /**无重力方块,墙体。接触到其他方块会立马停住做为底板使用 * * grouo == floor * 如果rigidbody2d组件存在,设置group为floor * 如果polygoncollider2d组件存在,设置group为floor * 不可升级 * * 注意:地板 Collider2D 不要勾 Sensor。Sensor 只发事件不产生接触约束,动态刚体会穿透。 * 去回弹请用 restitution=0,并由 tetriMap 里 PRE_SOLVE / 物理解算后收尾处理。 */ @ccclass('tetriFloorNode') export class tetriFloorNode extends Component { private _settled = false; /** 杀区销毁已进入流程,避免重复 skill 叠 tween / 双扣雷电爱心 */ private _killSkillDispatched = false; _curentWeaponId: number = 0; /**方块配置(BattleCube 表) */ public config: ITableBattleCube | null = null; /**落稳/钉住时回调(由外部注入,例如 UItetriGame 监听第一块) */ public onSettled: (() => void) | null = null; /**脚本模拟下落中(floor 使用 Static,不走 Dynamic 物理) */ private _simFallActive = false; private _simVy = 0; private _simVx = 0; // 掉落速度:按需求整体放慢 15 倍 private _simGravity = -3200 / 15; // px/s^2(手感参数) private _simMaxFallSpeed = 1600 / 15; // px/s /** 钉住前相对支撑体的竖直安全间隙(px):略抬高以减少与 Box2D 分离冲量不一致导致的“弹一下” */ private _simSkin = 0.5; /**下落期:自身 PolygonCollider2D(可 disabled)用于几何检测 */ private _selfPolyCols: PolygonCollider2D[] = []; /** * 脚本下落「阻挡」容差:纯多边形在共边/近邻时 ox/oy=0 常判成不重叠,导致贴不上边且竖直方向仍当空闲继续掉。 * 用 AABB 在 X/Y 方向允许微小负重叠(缝隙)仍视为接触,便于侧贴与落底。 */ private static readonly _SIM_CONTACT_SKIN_X = 1.35; private static readonly _SIM_CONTACT_SKIN_Y = 0.5; /** 脚本下落「接触」判定时:向上少扩,减轻上方倾斜块真实多边形与膨胀盒误交(侧向/落底仍用 _SIM_CONTACT_SKIN_*) */ private static readonly _FALL_CONTACT_UP_PAD = 0.12; /** 钉住前为消除穿插而微抬:上限过小会叠模,过大易与「地面」子树装饰体严格重叠后一路抬成悬空 */ private static readonly _PIN_SEPARATE_MAX_RISE_PX = 10; /** onLoad 前已请求脚本下落(inactive 入树时 onLoad 晚于 beginPlacementFall) */ private _placementFallPending = false; private _stackSupportStable = false; public setConfig(cfg: ITableBattleCube | null) { this.config = cfg; this._curentWeaponId = cfg?.weaponid ?? 0; } protected onLoad(): void { if (this._placementFallPending) { this._settled = false; this._simFallActive = true; this._simVy = 0; this._simVx = 0; } else { // 默认视为“已稳定”(例如卡牌展示态);放置到地图时由 beginPlacementFall() 解锁并开始下落 this._settled = true; this._simFallActive = false; this._simVy = 0; this._simVx = 0; } this._ensureFloorCollidersSolidNoBounce(); this.scheduleOnce(() => { if (!this.node?.isValid) return; if (!this._settled || this._simFallActive) return; playTetriBlockSpine(this.node, TetriBlockSpineState.Shop); }, 0); } /** 实体地板:必须可阻挡 Dynamic 刚体;回弹由 restitution + 地图侧逻辑处理,不能用 Sensor 代替 */ private _ensureFloorCollidersSolidNoBounce(): void { if (!this.node?.isValid) return; const cols = this.node.getComponentsInChildren(Collider2D) ?? []; for (const c of cols) { if (!c?.isValid) continue; c.sensor = false; c.restitution = 0; } } setView(){ //let conf = gg.data.project.getWeaponData(this._curentWeaponId) // console.log('tetriFloorNode conf',conf) } get isSettled(): boolean { return this._settled; } public hasStackSupportStable(): boolean { return this._stackSupportStable; } /**是否正在脚本下落(用于 tetriMap 在 contact 时判定立即钉住) */ public get isSimFalling(): boolean { return this._simFallActive && !this._settled; } /**开始脚本模拟下落:下落期间禁用 collider,命中支撑后启用并钉住 */ public startSimulatedFall(): void { if (!this.node?.isValid) return; if (this._settled) return; this._simFallActive = true; this._simVy = 0; this._simVx = 0; // 下落期:floor 不进入物理世界(避免移动静态 collider 造成其他动态体“提前 contact”) // 但我们仍用 polygon points 做几何重叠检测,因此缓存 collider 并禁用 enabled this._selfPolyCols = this.node.getComponentsInChildren(PolygonCollider2D) ?? []; for (const c of this._selfPolyCols) { if (c?.isValid) c.enabled = false; } } /**放置到地图:解锁“未企稳”并开始脚本下落 */ public beginPlacementFall(): void { if (!this.node?.isValid) return; this._placementFallPending = true; this._settled = false; playTetriBlockSpine(this.node, TetriBlockSpineState.Falling); this.startSimulatedFall(); } /** * 已在 TetrisTable 上摆好、但尚未 beginPlacementFall(如章1引导:镜头动画后再下落)。 * onLoad 默认 _settled=true 会给“卡牌展示”用,摆在桌上时若不调用本方法,堆高/白线会把悬空地块算进塔高。 */ public markAwaitingScriptFallStart(): void { this._settled = false; this._simFallActive = false; this._simVy = 0; this._simVx = 0; } private _isSelfColliderNode(n: Node | null): boolean { let cur: Node | null = n; while (cur) { if (cur === this.node) return true; cur = cur.parent; } return false; } /** * 在已几何分离的位置基础上,尽量向上取一点余量(不超过 _simSkin), * 使启用物理碰撞体时不易与下方动态块产生“穿透→分离”的上顶冲量。 */ private _applyVerticalSlackAboveSupport(baseWorldY: number, anchorWorldX: number): void { const maxExtra = Math.max(0, this._simSkin); let chosen = 0; for (let extra = maxExtra; extra >= 0; extra -= 0.5) { this.node.setWorldPosition(new Vec3(anchorWorldX, baseWorldY + extra, this.node.worldPosition.z)); if (!this._isOverlappingAnyOtherCollider()) { chosen = extra; break; } } this.node.setWorldPosition(new Vec3(anchorWorldX, baseWorldY + chosen, this.node.worldPosition.z)); } private _pinNow(): void { if (this._settled) return; this._settled = true; this._placementFallPending = false; this._simFallActive = false; this._simVy = 0; this._simVx = 0; playTetriBlockSpine(this.node, TetriBlockSpineState.Settled); if (this._isOverlappingSupportOnTaizi()) { this._stackSupportStable = true; } else { this._inheritStackSupportFromTableOverlap(); } if (this._stackSupportStable) { const map = (gg as any)?.game?.CurentBattle?.TetraMap; map?.invalidateStackTopCache?.(); map?._updateHighLine?.(); } this.scheduleOnce(() => { if (!this.node?.isValid) return; const map = (gg as any)?.game?.CurentBattle?.TetraMap; if (map?.pinFloor) { map.pinFloor(this.node); } else { this.enablePhysicsColliders(); try { this.onSettled?.(); } catch (e) { console.error(e); } } }, 0); } /** 由 tetriMap.pinFloor 在邻域消冲完成后再调用,避免碰撞体 cold-enable 嵌入 Dynamic 塔 */ public enablePhysicsColliders(): void { if (!this.node?.isValid) return; this._ensureFloorCollidersSolidNoBounce(); const cols = this.node.getComponentsInChildren(PolygonCollider2D) ?? []; for (const c of cols) { if (c?.isValid) c.enabled = true; } } /**触摸推动:仅在脚本下落期生效(与 tetriMap 的触摸控制配合) */ public applySimTouchPush(dir: number, deltaVelX: number, maxVelX: number): void { if (!this.isSimFalling) return; const d = dir < 0 ? -1 : 1; const next = this._simVx + d * deltaVelX; const hardMax = Math.abs(maxVelX); this._simVx = Math.max(-hardMax, Math.min(hardMax, next)); } /**点击式横移:每次触发立刻横向挪动一段距离(更符合“点一下就动一下”的手感) */ public nudgeSimX(dir: number, stepPx: number): void { if (!this.isSimFalling) return; const d = dir < 0 ? -1 : 1; const step = Math.abs(stepPx); if (!(step > 0)) return; this.applySimDeltaXWithCollision(d * step); } /** * 横向位移:按小步推进,碰到其它已启用碰撞体则停在贴边位置(脚本下落期自身 collider 禁用,用多边形几何判断)。 * 供 tetriMap 做「与放置相同的网格横移」等自定义位移。 */ public applySimDeltaXWithCollision(deltaX: number): void { if (!this.node?.isValid || !(Math.abs(deltaX) > 1e-6)) return; const sign = deltaX > 0 ? 1 : -1; let remaining = Math.abs(deltaX); const maxStep = 0.5; while (remaining > 1e-6) { const step = Math.min(maxStep, remaining); const wp = this.node.worldPosition; const tryX = wp.x + sign * step; this.node.setWorldPosition(new Vec3(tryX, wp.y, wp.z)); if (this._isSimFallGeomBlocked()) { this.node.setWorldPosition(wp); break; } remaining -= step; } } /** * 仅与台子/堆叠子树做严格重叠(钉住前微抬用)。 * 若并入 _groundTable,易与「地面」装饰多边形严格相交,为分离会大幅上抬 → floorNode 悬空。 */ private _overlapStrictSupportStackOnly(): boolean { const map = (gg as any)?.game?.CurentBattle?.TetraMap; const roots: Node[] = []; if (map?._tetrisTable?.isValid) roots.push(map._tetrisTable); if (map?._floorTaizi?.isValid) roots.push(map._floorTaizi); return roots.length ? this._overlapCollidersUnderRoots(roots, 0, 0) : false; } /** * 落稳前:若与台子/堆叠仍严格重叠,沿竖直微抬直到分离(上限很小,避免为躲地面子树碰撞体被抬飞)。 */ private _separateVerticallyIfOverlapping(maxRisePx: number = tetriFloorNode._PIN_SEPARATE_MAX_RISE_PX): void { if (!this.node?.isValid) return; const step = 0.25; let risen = 0; while (risen < maxRisePx && this._overlapStrictSupportStackOnly()) { const w = this.node.worldPosition; this.node.setWorldPosition(new Vec3(w.x, w.y + step, w.z)); risen += step; } } /**松手:清空水平速度,避免持续横移 */ public endSimTouchPush(): void { if (!this.isSimFalling) return; this._simVx = 0; } private _isSelfNodeOrChild(n: Node | null): boolean { let cur: Node | null = n; while (cur) { if (cur === this.node) return true; cur = cur.parent; } return false; } private _cross(ax: number, ay: number, bx: number, by: number): number { return ax * by - ay * bx; } 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._cross(abx, aby, acx, acy); const d2 = this._cross(abx, aby, adx, ady); const d3 = this._cross(cdx, cdy, cax, cay); const d4 = this._cross(cdx, cdy, cbx, cby); return d1 * d2 <= 0 && d3 * d4 <= 0; } private _pointInPolygon(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 _getPolyWorldPoints(col: PolygonCollider2D): Vec2[] | null { if (!col?.node?.isValid) return null; const pts = col.points ?? []; if (pts.length < 3) return null; const wm = new Mat4(); col.node.getWorldMatrix(wm); const out: Vec2[] = []; const wp = new Vec3(); 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; } private _getBoxWorldPoints(col: BoxCollider2D): Vec2[] | null { if (!col?.node?.isValid) return null; // Cocos Creator BoxCollider2D:offset + size(本地坐标),再用节点 worldMatrix 转到世界坐标 const size = (col as any).size as { x: number; y: number } | null; if (!size) return null; const hx = (size.x ?? 0) / 2; const hy = (size.y ?? 0) / 2; if (!(hx > 0) || !(hy > 0)) return null; const off = (col as any).offset as { x: number; y: number } | null; const ox = off?.x ?? 0; const oy = off?.y ?? 0; const wm = new Mat4(); col.node.getWorldMatrix(wm); const wp = new Vec3(); const out: Vec2[] = []; const corners = [ new Vec3(ox - hx, oy - hy, 0), new Vec3(ox + hx, oy - hy, 0), new Vec3(ox + hx, oy + hy, 0), new Vec3(ox - hx, oy + hy, 0), ]; for (const c of corners) { Vec3.transformMat4(wp, c, wm); out.push(new Vec2(wp.x, wp.y)); } return out; } private _polygonsOverlap(a: Vec2[], b: Vec2[]): boolean { if (a.length < 3 || b.length < 3) return false; // 轴对齐粗测:分离则不可能相交,避免边测试在共线/浮点下的误判牵连远处形状 const aa = this._aabbFromPoly(a); const ab = this._aabbFromPoly(b); if (aa.maxX < ab.minX || ab.maxX < aa.minX || aa.maxY < ab.minY || ab.maxY < aa.minY) return false; // 边相交 for (let i = 0; i < a.length; i++) { const a1 = a[i]; const a2 = a[(i + 1) % a.length]; for (let j = 0; j < b.length; j++) { const b1 = b[j]; const b2 = b[(j + 1) % b.length]; if (this._segmentsIntersect(a1, a2, b1, b2)) return true; } } // 包含 if (this._pointInPolygon(a[0], b)) return true; if (this._pointInPolygon(b[0], a)) return true; return false; } private _aabbFromPoly(poly: Vec2[]): { minX: number; minY: number; maxX: number; maxY: number } { let minX = poly[0].x; let maxX = poly[0].x; let minY = poly[0].y; let maxY = poly[0].y; for (let i = 1; i < poly.length; i++) { const p = poly[i]; if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x; if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y; } return { minX, minY, maxX, maxY }; } /** 多边形在「y <= yMax」半平面内的部分(Sutherland–Hodgman 单边裁剪),用于取下沿真实占地,避免凹形整体 AABB 角点误触地 */ private _clipPolygonHalfPlaneYMax(poly: Vec2[], yMax: number): Vec2[] { if (poly.length < 2) return []; const inside = (p: Vec2) => p.y <= yMax; const intersectY = (a: Vec2, b: Vec2): Vec2 => { const dy = b.y - a.y; if (Math.abs(dy) < 1e-9) return new Vec2(b.x, yMax); const t = (yMax - a.y) / dy; return new Vec2(a.x + (b.x - a.x) * t, yMax); }; const out: Vec2[] = []; let prev = poly[poly.length - 1]; let prevIn = inside(prev); for (let i = 0; i < poly.length; i++) { const cur = poly[i]; const curIn = inside(cur); if (curIn) { if (!prevIn) out.push(intersectY(prev, cur)); out.push(cur); } else if (prevIn) { out.push(intersectY(prev, cur)); } prev = cur; prevIn = curIn; } return out; } /** * 竖直方向「容差接触」用的 AABB:取自多边形最下一小段真实形状,而不是整体外接矩形(T/L 形角上无砖处不会伸到地面)。 */ private _fallContactSkinAabbFromPoly(selfPoly: Vec2[], skinX: number, skinY: number, upPad: number): { minX: number; minY: number; maxX: number; maxY: number } | null { if (selfPoly.length < 3) return null; let minY = selfPoly[0].y; for (let i = 1; i < selfPoly.length; i++) { const y = selfPoly[i].y; if (y < minY) minY = y; } const band = Math.max(skinY, tetriFloorNode._SIM_CONTACT_SKIN_Y) + 2; const clipped = this._clipPolygonHalfPlaneYMax(selfPoly, minY + band); if (clipped.length < 2) return null; const a = this._aabbFromPoly(clipped); if (!(a.maxX >= a.minX && a.maxY >= a.minY)) return null; return { minX: a.minX - skinX, maxX: a.maxX + skinX, minY: a.minY - skinY, maxY: a.maxY + upPad, }; } /** 轴对齐矩形与凸多边形是否相交(含边相交、顶点在内) */ private _aabbIntersectsPolygon(aabb: { minX: number; minY: number; maxX: number; maxY: number }, poly: Vec2[]): boolean { if (poly.length < 3) return false; const { minX, minY, maxX, maxY } = aabb; for (const p of poly) { if (p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY) return true; } const ax = [ new Vec2(minX, minY), new Vec2(maxX, minY), new Vec2(maxX, maxY), new Vec2(minX, maxY), ]; for (let k = 0; k < 4; k++) { if (this._pointInPolygon(ax[k], poly)) return true; } for (let i = 0; i < poly.length; i++) { const p1 = poly[i]; const p2 = poly[(i + 1) % poly.length]; for (let j = 0; j < 4; j++) { if (this._segmentsIntersect(p1, p2, ax[j], ax[(j + 1) % 4])) return true; } } return false; } /** * 多边形真实重叠,或「self 的 AABB 按 skin 外扩」与对方真实多边形相交(共边/近邻容差)。 * 旧实现用双方 AABB 做容差:倾斜块的世界 AABB 远大于外形,会把远处下落的 floor 误挡停。 * fallContactTightUp:竖直下落时少扩「顶边」,避免与堆叠上方斜块的多边形误交;支撑面侧贴仍用对称外扩。 */ private _pairGeomBlockReason( selfPoly: Vec2[], otherPoly: Vec2[], skinX: number, skinY: number, fallContactTightUp = false, skinBottomFootprint = false, ): 'polygonsStrictOverlap' | 'skinBottomFootprintAabb' | 'skinExpandedSelfAabb' | null { if (this._polygonsOverlap(selfPoly, otherPoly)) return 'polygonsStrictOverlap'; if (!(skinX > 0 || skinY > 0)) return null; const upPad = fallContactTightUp ? tetriFloorNode._FALL_CONTACT_UP_PAD : skinY; if (fallContactTightUp && skinBottomFootprint) { const exp = this._fallContactSkinAabbFromPoly(selfPoly, skinX, skinY, upPad); if (exp && this._aabbIntersectsPolygon(exp, otherPoly)) return 'skinBottomFootprintAabb'; return null; } const a = this._aabbFromPoly(selfPoly); const exp = { minX: a.minX - skinX, maxX: a.maxX + skinX, minY: a.minY - skinY, maxY: a.maxY + upPad, }; if (this._aabbIntersectsPolygon(exp, otherPoly)) return 'skinExpandedSelfAabb'; return null; } private _pairGeomBlocking( selfPoly: Vec2[], otherPoly: Vec2[], skinX: number, skinY: number, fallContactTightUp = false, skinBottomFootprint = false, ): boolean { return this._pairGeomBlockReason(selfPoly, otherPoly, skinX, skinY, fallContactTightUp, skinBottomFootprint) !== null; } /** * 部分地图「地面」节点仅有 UITransform+Sprite,子树无 Collider2D;脚本下落只扫碰撞体会漏掉宽阔地表,方块会从视觉地面穿下去。 * 在允许时,用地面 UITransform 的世界轴对齐盒当作额外挡板(仅用于下落/支撑判定,不用于击杀区严格重叠)。 */ private _getGroundTableUiWorldPolygon(): Vec2[] | null { const map = (gg as any)?.game?.CurentBattle?.TetraMap; const gt = map?._groundTable; if (!gt?.isValid) return null; const gut = gt.getComponent(UITransform); if (!gut?.isValid) return null; const bb = gut.getBoundingBoxToWorld(); const x0 = bb.xMin; const x1 = bb.xMax; const y0 = bb.yMin; const y1 = bb.yMax; if (!(x1 > x0) || !(y1 > y0)) return null; return [ new Vec2(x0, y0), new Vec2(x1, y0), new Vec2(x1, y1), new Vec2(x0, y1), ]; } /** * 与指定根节点子树内已启用的 2D 碰撞体是否阻挡(不含自身)。 * skin 为 0 时仅多边形重叠;大于 0 时额外用 AABB 接触容差。 * fallContactTightUp 为 true 时仅用于「竖直下落挡停」:顶边少扩,减轻斜块误挡。 * skinBottomFootprint 为 true 时(与 fallContactTightUp 同时使用):容差盒取自最下沿带状真实形状,避免 T/L 形整体 AABB 空角误触地;横向推动应传 false 以保留侧向 skin。 * allowGroundUiFootprint:对 TetraMap._groundTable 额外用 UITransform 世界盒参与阻挡(见 _getGroundTableUiWorldPolygon)。 */ private _overlapCollidersUnderRoots( roots: Node[], skinX = 0, skinY = 0, fallContactTightUp = false, skinBottomFootprint = false, allowGroundUiFootprint = false, ): boolean { const map = (gg as any)?.game?.CurentBattle?.TetraMap; const selfCols = this._selfPolyCols?.length ? this._selfPolyCols : (this.node.getComponentsInChildren(PolygonCollider2D) ?? []); const selfPolys: Vec2[][] = []; for (const c of selfCols) { if (!c?.isValid) continue; const p = this._getPolyWorldPoints(c); if (p) selfPolys.push(p); } if (!selfPolys.length) { for (const bc of this.node.getComponentsInChildren(BoxCollider2D) ?? []) { if (!bc?.isValid) continue; const bp = this._getBoxWorldPoints(bc); if (bp) selfPolys.push(bp); } } if (!selfPolys.length) return false; const useRoots = roots.filter((n) => n?.isValid); if (!useRoots.length) return false; for (const r of useRoots) { const polyOthers = r.getComponentsInChildren(PolygonCollider2D) ?? []; for (const oc of polyOthers) { if (!oc?.isValid || !oc.enabled || oc.sensor) continue; if (this._isSelfNodeOrChild(oc.node)) continue; const op = this._getPolyWorldPoints(oc); if (!op) continue; for (const sp of selfPolys) { if (this._pairGeomBlocking(sp, op, skinX, skinY, fallContactTightUp, skinBottomFootprint)) return true; } } const boxOthers = r.getComponentsInChildren(BoxCollider2D) ?? []; for (const bc of boxOthers) { if (!bc?.isValid || !(bc as any).enabled || (bc as any).sensor) continue; if (this._isSelfNodeOrChild(bc.node)) continue; const bp = this._getBoxWorldPoints(bc); if (!bp) continue; for (const sp of selfPolys) { if (this._pairGeomBlocking(sp, bp, skinX, skinY, fallContactTightUp, skinBottomFootprint)) return true; } } if (allowGroundUiFootprint && r === map?._groundTable) { const gp = this._getGroundTableUiWorldPolygon(); if (gp) { for (const sp of selfPolys) { if (this._pairGeomBlocking(sp, gp, skinX, skinY, fallContactTightUp, skinBottomFootprint)) return true; } } } } return false; } /** * 脚本下落几何检测根节点:只用「玩法碰撞」子树。 * 不要挂整棵 GameRoot(含 UI/特效/无关 Box),否则会误挡后走 _pinNow 悬空钉住。 */ private _simFallStrictRoots(): Node[] { const map = (gg as any)?.game?.CurentBattle?.TetraMap; const roots: Node[] = []; if (map?._groundTable?.isValid) roots.push(map._groundTable); if (map?._tetrisTable?.isValid) roots.push(map._tetrisTable); if (map?._floorTaizi?.isValid) roots.push(map._floorTaizi); if (!roots.length) { const scene = this.node.scene; if (scene?.isValid) roots.push(scene); } return roots; } /** 自身多边形在世界坐标的最小 Y(用于判断是否与地面尚无可信接触) */ private _getSelfPolyWorldMinY(): number | null { const cols = this._selfPolyCols?.length ? this._selfPolyCols : (this.node.getComponentsInChildren(PolygonCollider2D) ?? []); let best = Number.POSITIVE_INFINITY; for (const c of cols) { if (!c?.isValid) continue; const pts = this._getPolyWorldPoints(c); if (!pts) continue; for (let i = 0; i < pts.length; i++) { const y = pts[i].y; if (y < best) best = y; } } for (const bc of this.node.getComponentsInChildren(BoxCollider2D) ?? []) { if (!bc?.isValid) continue; const pts = this._getBoxWorldPoints(bc); if (!pts) continue; for (let i = 0; i < pts.length; i++) { const y = pts[i].y; if (y < best) best = y; } } return Number.isFinite(best) ? best : null; } /** * 方块整体明显高于「地面上沿」时,不参与地面相关的几何挡停/容差检测。 * 否则地面碰撞体 + skin 可能在空中误挡;竖直分支若又判不成 stack 支撑且非 kill,会整帧 return 导致卡住(松手后连点左右更明显)。 */ private _shouldIncludeGroundInGeomQueries(): boolean { const map = (gg as any)?.game?.CurentBattle?.TetraMap; const gt = map?._groundTable; if (!gt?.isValid) return true; let groundTop = gt.worldPosition.y; const gut = gt.getComponent(UITransform); if (gut?.isValid) { groundTop = gut.getBoundingBoxToWorld().yMax; } const minY = this._getSelfPolyWorldMinY(); if (minY == null) return true; const cell = map?.blockHeight ?? 36; return minY <= groundTop + cell * 4; } /** 脚本下落用的几何根:可能剔除地面,避免高空误挡 */ private _simFallGeomRoots(): Node[] { const raw = this._simFallStrictRoots(); const map = (gg as any)?.game?.CurentBattle?.TetraMap; const gt = map?._groundTable; if (!gt?.isValid || !this._shouldIncludeGroundInGeomQueries()) { return raw.filter((n) => n?.isValid && n !== gt); } return raw; } /** * 脚本下落:应被地形/方块挡住的判定(严格多边形 或 与方块/台子的接触容差)。 * @param verticalFallStep 为 true 时竖直子步:skin 用底部真实占地,避免凹形整体 AABB 悬停。 */ private _isSimFallGeomBlocked(verticalFallStep = false): boolean { const roots = this._simFallGeomRoots(); if (!roots.length) return false; if (this._overlapCollidersUnderRoots(roots, 0, 0, false, false, true)) return true; const sx = tetriFloorNode._SIM_CONTACT_SKIN_X; const sy = tetriFloorNode._SIM_CONTACT_SKIN_Y; return this._overlapCollidersUnderRoots(roots, sx, sy, true, verticalFallStep, true); } /** 严格多边形重叠(钉住后分离、旧逻辑兼容) */ private _isOverlappingAnyOtherCollider(): boolean { const roots = this._simFallGeomRoots(); if (!roots.length) return false; return this._overlapCollidersUnderRoots(roots, 0, 0); } /** * 与堆叠区 / 台子:重叠或接触容差 → 钉住当底板。 * 不含「地面」:碰到地面与动态块一样销毁(见 _isOverlappingKillSurfaces)。 */ private _isOverlappingSupportSurfaces(): boolean { const map = (gg as any)?.game?.CurentBattle?.TetraMap; const roots: Node[] = []; if (map?._tetrisTable?.isValid) roots.push(map._tetrisTable); if (map?._floorTaizi?.isValid) roots.push(map._floorTaizi); const sx = tetriFloorNode._SIM_CONTACT_SKIN_X; const sy = tetriFloorNode._SIM_CONTACT_SKIN_Y; return roots.length ? this._overlapCollidersUnderRoots(roots, sx, sy, true, true) : false; } private _isOverlappingSupportOnTaizi(): boolean { const map = (gg as any)?.game?.CurentBattle?.TetraMap; if (!map?._floorTaizi?.isValid) return false; const sx = tetriFloorNode._SIM_CONTACT_SKIN_X; const sy = tetriFloorNode._SIM_CONTACT_SKIN_Y; return this._overlapCollidersUnderRoots([map._floorTaizi], sx, sy, true, true); } /** 钉在已有稳定塔顶的无重力板才计入堆高 */ private _inheritStackSupportFromTableOverlap(): void { const map = (gg as any)?.game?.CurentBattle?.TetraMap; const table = map?._tetrisTable; if (!table?.isValid) return; for (const child of table.children) { if (!child?.isValid || child === this.node) continue; const t = child.getComponent(tetriNode) ?? child.getComponentInChildren(tetriNode); if (t?.hasStackSupportStable()) { this._stackSupportStable = true; return; } const f = child.getComponent(tetriFloorNode) ?? child.getComponentInChildren(tetriFloorNode); if (f && f !== this && f.hasStackSupportStable()) { this._stackSupportStable = true; return; } } } /** * 销毁区:地面(与脚本挡停同一套:真实碰撞体 + UI 合成地表 + skin)→ 与 tetriMap 里普通块碰地面一致,红闪 + skill。 * 怪物/落出区:仍仅严格重叠,避免容差误触杀区。 */ private _isOverlappingKillSurfaces(): boolean { const map = (gg as any)?.game?.CurentBattle?.TetraMap; if (map?._monsterTable?.isValid) { if (this._overlapCollidersUnderRoots([map._monsterTable], 0, 0)) return true; } if (map?._groundTable?.isValid && this._shouldIncludeGroundInGeomQueries()) { const sx = tetriFloorNode._SIM_CONTACT_SKIN_X; const sy = tetriFloorNode._SIM_CONTACT_SKIN_Y; if (this._overlapCollidersUnderRoots([map._groundTable], sx, sy, true, true, true)) return true; } return false; } /** 仅与「移除用地面」重叠(不含怪物区),用于雷电副本爱心 */ private _isOverlappingRemovalGroundOnly(): boolean { const map = (gg as any)?.game?.CurentBattle?.TetraMap; if (!map?._groundTable?.isValid || !this._shouldIncludeGroundInGeomQueries()) return false; const sx = tetriFloorNode._SIM_CONTACT_SKIN_X; const sy = tetriFloorNode._SIM_CONTACT_SKIN_Y; return this._overlapCollidersUnderRoots([map._groundTable], sx, sy, true, true, true); } protected onEnable(): void { // 锤子点选改由 UItetriGame 遮罩拾取;不再挂节点 TOUCH } protected onDisable(): void { this.unregisterTouchEvent(); } //注册触摸事件(空实现,避免旧事件) registerTouchEvent(){ this.unregisterTouchEvent(); } unregisterTouchEvent(){ this.node.off(Node.EventType.TOUCH_END, this.click_btnEnd, this); } /** 供锤子遮罩命中后调用 */ 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) const cutY = this.node?.worldPosition.y ?? Number.NEGATIVE_INFINITY; const map = gg.game.CurentBattle?.TetraMap; // 敲掉无重力板:解冻其下沿到「下一层无重力板」之间的普通块;更上层仍由剩余无重力板托住 map?.wakeFrozenStackPhysics(cutY - 40, this.node); map?.invalidateStackTopCache?.(); this.node.destroy(); map?.refreshHighLineFromStack?.(); } update(dt: number): void { if (!this._simFallActive || this._settled) return; if (!this.node?.isValid) return; if (dt <= 0) return; // 强制确保下落期自身 collider 不进入物理世界(防止外部误启用导致冲量) if (this._selfPolyCols?.length) { for (const c of this._selfPolyCols) { if (c?.isValid && c.enabled) c.enabled = false; } } // 水平移动:触摸推动(与普通块的 rb.linearVelocity 类似的“速度推进”手感),与其它方块/地形做几何阻挡 if (this._simVx !== 0) { this.applySimDeltaXWithCollision(this._simVx * dt); } // 积分速度 this._simVy += this._simGravity * dt; if (this._simVy < -this._simMaxFallSpeed) this._simVy = -this._simMaxFallSpeed; // 计算本帧下落位移(只往下) const dy = this._simVy * dt; if (dy >= 0) return; // 子步进下落:避免一次 dy 过大导致“启用 collider 时仍有穿插”,从而把其他刚体顶开形成冲量感 const p0 = this.node.worldPosition; const total = -dy; // 向下距离(正数) const step = 2; // px,每步最大下落距离(越小越不容易与物理引擎判定不一致) const n = Math.max(1, Math.ceil(total / step)); const per = total / n; let curY = p0.y; for (let i = 0; i < n; i++) { curY -= per; this.node.setWorldPosition(new Vec3(p0.x, curY, p0.z)); if (this._isSimFallGeomBlocked(true)) { // 竖直子步:严格多边形 + 底部带状 skin(横向推动仍用整体 AABB skin 便于侧贴) // 区分:堆叠/台子 → 钉住;严格落入杀区 → 销毁 const support = this._isOverlappingSupportSurfaces(); const kill = this._isOverlappingKillSurfaces(); this.node.setWorldPosition(new Vec3(p0.x, curY + per, p0.z)); if (support) { this._separateVerticallyIfOverlapping(); this._pinNow(); return; } if (kill) { const map = (gg as any)?.game?.CurentBattle?.TetraMap; const ln = this._isOverlappingRemovalGroundOnly() && map?._groundTable ? map._groundTable : null; try { map?.showFloorRedFlash?.(ln); } catch (e) { console.error(e); } this.skill(); return; } // 误挡(例如旧版地面 skin 高空误判):勿 return 整帧 update,否则垂直速度一直累积却永不位移 → 悬空卡死 break; } } // 安全兜底:掉出太远则直接销毁(与 _showFloorRedFlash 统一:雷电爱心) if (this.node.worldPosition.y < -2000) { const map = (gg as any)?.game?.CurentBattle?.TetraMap; try { map?.showFloorRedFlash?.(map?._groundTable ?? null); } catch (e) { console.error(e); } this.node.destroy(); } } skill(){ if (this._killSkillDispatched) return; this._killSkillDispatched = true; this._settled = true; this.scheduleOnce(()=>{ if (!this.node?.isValid) return; let box = this.node.getComponent(PolygonCollider2D) ?? this.node.getComponentInChildren(PolygonCollider2D); if (box) { //取消碰撞 box.enabled = false; } // floorNode 不使用刚体(RigidBody2D) if(this.node.isValid){ Tween.stopAllByTarget(this.node); tween(this.node) //.to(0.5, { y: posy-100 }) .to(0.5, { opacity: 50 }) .call(() => { if (!this.node?.isValid) return; gg.game?.CurentBattle?.tryApplyLightningLoveOnRemovalGround(); const map = gg.game?.CurentBattle?.TetraMap; map?.invalidateStackTopCache?.(); this.node.destroy(); map?.refreshHighLineFromStack?.(); }) .start(); } },0) } }