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

961 lines
40 KiB

1 week ago
import { _decorator, BoxCollider2D, Collider2D, Component, Mat4, PolygonCollider2D, tween, Tween, UITransform, Vec2, Vec3, Node } from 'cc';
import { GEvent } from '../../mx/module/event/GEvent';
1 week ago
import { playTetriBlockSpine, TetriBlockSpineState } from './tetriBlockSpine';
5 days ago
import { tetriNode } from './tetriNode';
1 week ago
const { ccclass } = _decorator;
/**使
*
* grouo == floor
* rigidbody2d组件存在group为floor
* polygoncollider2d组件存在group为floor
*
*
* Collider2D SensorSensor 穿
* 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;
5 days ago
/** onLoad 前已请求脚本下落(inactive 入树时 onLoad 晚于 beginPlacementFall) */
private _placementFallPending = false;
private _stackSupportStable = false;
1 week ago
public setConfig(cfg: ITableBattleCube | null) {
this.config = cfg;
this._curentWeaponId = cfg?.weaponid ?? 0;
}
protected onLoad(): void {
5 days ago
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;
}
1 week ago
this._ensureFloorCollidersSolidNoBounce();
1 week ago
this.scheduleOnce(() => {
if (!this.node?.isValid) return;
5 days ago
if (!this._settled || this._simFallActive) return;
1 week ago
playTetriBlockSpine(this.node, TetriBlockSpineState.Shop);
}, 0);
1 week ago
}
/** 实体地板:必须可阻挡 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; }
5 days ago
public hasStackSupportStable(): boolean { return this._stackSupportStable; }
4 days ago
/** 落稳后补标稳定支撑(台子/稳定塔顶),避免无重力砖块未计入堆高 */
public tryPromoteStackSupportStable(): boolean {
if (this._stackSupportStable || !this._settled) return false;
if (this._isOverlappingSupportOnTaizi()) {
this._stackSupportStable = true;
const map = (gg as any)?.game?.CurentBattle?.TetraMap;
map?.invalidateStackTopCache?.();
map?._updateHighLine?.();
return true;
}
this._inheritStackSupportFromTableOverlap();
if (this._stackSupportStable) {
const map = (gg as any)?.game?.CurentBattle?.TetraMap;
map?.invalidateStackTopCache?.();
map?._updateHighLine?.();
return true;
}
return false;
}
1 week ago
/**是否正在脚本下落(用于 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;
5 days ago
this._placementFallPending = true;
1 week ago
this._settled = false;
1 week ago
playTetriBlockSpine(this.node, TetriBlockSpineState.Falling);
1 week ago
this.startSimulatedFall();
}
/**
* TetrisTable beginPlacementFall1
* 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;
5 days ago
this._placementFallPending = false;
1 week ago
this._simFallActive = false;
this._simVy = 0;
this._simVx = 0;
1 week ago
playTetriBlockSpine(this.node, TetriBlockSpineState.Settled);
5 days ago
if (this._isOverlappingSupportOnTaizi()) {
this._stackSupportStable = true;
} else {
this._inheritStackSupportFromTableOverlap();
}
if (this._stackSupportStable) {
const map = (gg as any)?.game?.CurentBattle?.TetraMap;
map?.invalidateStackTopCache?.();
map?._updateHighLine?.();
}
1 week ago
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;
}
}
5 days ago
/**
* 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);
}
1 week ago
/**触摸推动:仅在脚本下落期生效(与 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;
}
/**
* AABBT/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;
}
5 days ago
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;
}
}
}
1 week ago
/**
* + 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)
}
}