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.
940 lines
39 KiB
940 lines
39 KiB
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Canvas/分辨率变化后:floor 无 RigidBody2D,仅靠 PolygonCollider2D 挡 Dynamic。
|
|
* 父节点缩放不会把新位姿写进 Box2D,表现为无重力板「失效」、其它块穿透。
|
|
*/
|
|
public refreshPhysicsCollidersAfterViewportChange(): void {
|
|
if (!this.node?.isValid || !this._settled) return;
|
|
this._ensureFloorCollidersSolidNoBounce();
|
|
const cols = this.node.getComponentsInChildren(PolygonCollider2D) ?? [];
|
|
for (let i = 0; i < cols.length; i++) {
|
|
const c = cols[i];
|
|
if (!c?.isValid) continue;
|
|
const was = c.enabled;
|
|
c.enabled = false;
|
|
c.enabled = was;
|
|
if (typeof c.apply === 'function') c.apply();
|
|
}
|
|
const p = this.node.position;
|
|
this.node.setPosition(p.x, p.y, p.z);
|
|
}
|
|
|
|
/**触摸推动:仅在脚本下落期生效(与 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)
|
|
}
|
|
}
|
|
|
|
|
|
|