|
|
|
|
import {
|
|
|
|
|
_decorator, CCInteger, Collider2D, Color, Component, Contact2DType,
|
|
|
|
|
ERigidBody2DType, IPhysics2DContact, Node, PolygonCollider2D, RigidBody2D, Sprite, Vec2,
|
|
|
|
|
} from 'cc';
|
|
|
|
|
import { LuosiDing } from './LuosiDing';
|
|
|
|
|
const { ccclass, property } = _decorator;
|
|
|
|
|
|
|
|
|
|
/** 螺丝全部拔完后木板掉落时的整体色调 */
|
|
|
|
|
const BOARD_FALL_GRAY = '#646464';
|
|
|
|
|
/** 进入自由掉落后:本地 Y 低于此值(相对 layer)即可销毁 */
|
|
|
|
|
const BOARD_FALL_DESTROY_LOCAL_Y = -60;
|
|
|
|
|
/** 进入自由掉落后最长存活(秒) */
|
|
|
|
|
const BOARD_FALL_MAX_LIFE_SEC = 2.5;
|
|
|
|
|
/** 碰到 floor 后延迟销毁(秒) */
|
|
|
|
|
const BOARD_FALL_FLOOR_DESTROY_DELAY = 0.15;
|
|
|
|
|
|
|
|
|
|
/**木板子元素*/
|
|
|
|
|
@ccclass('Aelement')
|
|
|
|
|
export class Aelement extends Component {
|
|
|
|
|
|
|
|
|
|
@property(CCInteger)
|
|
|
|
|
aelementHoleNum: number = 2;
|
|
|
|
|
|
|
|
|
|
/** 生成时孔洞螺丝总数 */
|
|
|
|
|
private _initialScrewCount = 0;
|
|
|
|
|
/** 木板上尚未脱落的螺丝数量(仅按 onScrewLeft 递减,不含铰链锚点) */
|
|
|
|
|
private _remainingScrews = 0;
|
|
|
|
|
/** 螺丝全部脱落后,开始检测木板掉落 */
|
|
|
|
|
private _fallCheckEnabled = false;
|
|
|
|
|
/** 已启用铰链约束物理(仅剩一颗螺丝) */
|
|
|
|
|
private _constrainedPhysicsActive = false;
|
|
|
|
|
/** 进入自由掉落累计时间 */
|
|
|
|
|
private _fallLifeAcc = 0;
|
|
|
|
|
private _fallContactBound = false;
|
|
|
|
|
private _physicsActivating = false;
|
|
|
|
|
|
|
|
|
|
/** 木板是否已进入掉落态(置灰),不再参与遮挡其它螺丝 */
|
|
|
|
|
get isFalling(): boolean {
|
|
|
|
|
return this._fallCheckEnabled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 仅剩一颗螺丝、绕铰链摆动中 */
|
|
|
|
|
get isSwinging(): boolean {
|
|
|
|
|
return this._constrainedPhysicsActive && !this._fallCheckEnabled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 获取木板上所有孔洞节点(孔洞1、孔洞2…) */
|
|
|
|
|
getHoleNodes(): Node[] {
|
|
|
|
|
return this.node.children
|
|
|
|
|
.filter((c) => c.name.startsWith('孔洞'))
|
|
|
|
|
.sort((a, b) => a.name.localeCompare(b.name, 'zh'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 生成螺丝后登记数量 */
|
|
|
|
|
initScrewCount(count: number) {
|
|
|
|
|
this._initialScrewCount = count;
|
|
|
|
|
this._remainingScrews = count;
|
|
|
|
|
this._fallCheckEnabled = false;
|
|
|
|
|
this._constrainedPhysicsActive = false;
|
|
|
|
|
this._fallLifeAcc = 0;
|
|
|
|
|
this._physicsActivating = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 螺丝从木板上脱落(弹出或消除)时由 LuosiDing 调用 */
|
|
|
|
|
onScrewLeft() {
|
|
|
|
|
if (this._remainingScrews > 0) {
|
|
|
|
|
this._remainingScrews--;
|
|
|
|
|
}
|
|
|
|
|
this._applyPhysicsForScrewCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 复活 / 广告消钉后:按孔洞内实际螺丝数恢复 */
|
|
|
|
|
reconcilePhysicsState() {
|
|
|
|
|
if (!this.node?.isValid) return;
|
|
|
|
|
this._remainingScrews = LuosiDing.countAttachedScrewsInHoles(this.node);
|
|
|
|
|
this._applyPhysicsForScrewCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 物理策略(贴近 prefab,不注入角速度):
|
|
|
|
|
* - 2+ 颗螺丝:静态节点树固定,不进物理(避免过约束抖动)
|
|
|
|
|
* - 1 颗螺丝:Dynamic + 单铰链,绕螺丝重心自然摆动
|
|
|
|
|
* - 0 颗螺丝:脱离铰链,重力自由滑落
|
|
|
|
|
*/
|
|
|
|
|
private _applyPhysicsForScrewCount() {
|
|
|
|
|
if (this._remainingScrews > 1) {
|
|
|
|
|
this._fallCheckEnabled = false;
|
|
|
|
|
this._fallLifeAcc = 0;
|
|
|
|
|
this._constrainedPhysicsActive = false;
|
|
|
|
|
this._unbindFallContact();
|
|
|
|
|
LuosiDing.detachAllBoardHinges(this.node);
|
|
|
|
|
this.prepareColliderForGameplay();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this._remainingScrews === 1) {
|
|
|
|
|
this._enterConstrainedPhysics();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this._initialScrewCount > 0) {
|
|
|
|
|
this._enterFreeFall();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _enterConstrainedPhysics() {
|
|
|
|
|
this._fallCheckEnabled = false;
|
|
|
|
|
this._fallLifeAcc = 0;
|
|
|
|
|
this._unbindFallContact();
|
|
|
|
|
this.beginConstrainedPhysics();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _enterFreeFall() {
|
|
|
|
|
if (!this.node?.isValid) return;
|
|
|
|
|
if (!this._fallCheckEnabled) {
|
|
|
|
|
this._applyFallGrayColor();
|
|
|
|
|
}
|
|
|
|
|
this._fallCheckEnabled = true;
|
|
|
|
|
this._constrainedPhysicsActive = false;
|
|
|
|
|
this._fallLifeAcc = 0;
|
|
|
|
|
LuosiDing.detachAllBoardHinges(this.node);
|
|
|
|
|
this.beginFallingPhysics();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 螺丝拔完开始掉落时,将木板及子节点 Sprite 整体置灰 */
|
|
|
|
|
private _applyFallGrayColor() {
|
|
|
|
|
const gray = new Color().fromHEX(BOARD_FALL_GRAY);
|
|
|
|
|
const visit = (n: Node) => {
|
|
|
|
|
const sp = n.getComponent(Sprite);
|
|
|
|
|
if (sp) sp.color = gray;
|
|
|
|
|
for (const child of n.children) visit(child);
|
|
|
|
|
};
|
|
|
|
|
visit(this.node);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 玩法阶段:刚体与碰撞体均不进物理世界;Polygon 顶点仍可供 LuosiDing 几何遮挡检测。
|
|
|
|
|
*/
|
|
|
|
|
prepareColliderForGameplay() {
|
|
|
|
|
this.unschedule(this._activateConstrainedRigidbody);
|
|
|
|
|
this.unschedule(this._activateFallRigidbody);
|
|
|
|
|
this._physicsActivating = false;
|
|
|
|
|
this._unbindFallContact();
|
|
|
|
|
LuosiDing.detachAllBoardHinges(this.node);
|
|
|
|
|
this._constrainedPhysicsActive = false;
|
|
|
|
|
const rb = this.node.getComponent(RigidBody2D);
|
|
|
|
|
if (rb) {
|
|
|
|
|
rb.enabled = false;
|
|
|
|
|
rb.linearVelocity = Vec2.ZERO;
|
|
|
|
|
rb.angularVelocity = 0;
|
|
|
|
|
}
|
|
|
|
|
const poly = this.node.getComponent(PolygonCollider2D);
|
|
|
|
|
if (poly) {
|
|
|
|
|
poly.enabled = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 仅剩一颗螺丝:Dynamic + 单铰链,受重力自然摆 */
|
|
|
|
|
beginConstrainedPhysics() {
|
|
|
|
|
if (this._physicsActivating) return;
|
|
|
|
|
this.unschedule(this._activateConstrainedRigidbody);
|
|
|
|
|
this.scheduleOnce(this._activateConstrainedRigidbody, 0.06);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _activateConstrainedRigidbody = () => {
|
|
|
|
|
if (!this.node?.isValid || this._fallCheckEnabled || this._remainingScrews !== 1) return;
|
|
|
|
|
this._physicsActivating = true;
|
|
|
|
|
|
|
|
|
|
const rb = this.node.getComponent(RigidBody2D);
|
|
|
|
|
if (!rb) {
|
|
|
|
|
this._physicsActivating = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rb.enabled) {
|
|
|
|
|
rb.enabled = false;
|
|
|
|
|
}
|
|
|
|
|
// 传感器:不与堆叠木板/斜坡产生分离冲量,只靠铰链+重力按重心摆
|
|
|
|
|
this._enableBoardCollider(true);
|
|
|
|
|
|
|
|
|
|
rb.enabled = true;
|
|
|
|
|
rb.type = ERigidBody2DType.Dynamic;
|
|
|
|
|
rb.gravityScale = 5;
|
|
|
|
|
rb.linearDamping = 0.2;
|
|
|
|
|
rb.angularDamping = 5;
|
|
|
|
|
rb.fixedRotation = false;
|
|
|
|
|
rb.allowSleep = true;
|
|
|
|
|
rb.bullet = false;
|
|
|
|
|
rb.enabledContactListener = false;
|
|
|
|
|
rb.linearVelocity = Vec2.ZERO;
|
|
|
|
|
rb.angularVelocity = 0;
|
|
|
|
|
|
|
|
|
|
LuosiDing.refreshBoardHinges(this.node);
|
|
|
|
|
rb.wakeUp();
|
|
|
|
|
this._constrainedPhysicsActive = true;
|
|
|
|
|
this._physicsActivating = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 螺丝全部拔完后脱离铰链,沿斜坡自然滑落 */
|
|
|
|
|
beginFallingPhysics() {
|
|
|
|
|
LuosiDing.detachAllBoardHinges(this.node);
|
|
|
|
|
this.unschedule(this._activateFallRigidbody);
|
|
|
|
|
this.scheduleOnce(this._activateFallRigidbody, 0.06);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _activateFallRigidbody = () => {
|
|
|
|
|
if (!this._fallCheckEnabled || !this.node?.isValid) return;
|
|
|
|
|
this._physicsActivating = true;
|
|
|
|
|
|
|
|
|
|
const rb = this.node.getComponent(RigidBody2D);
|
|
|
|
|
if (!rb) {
|
|
|
|
|
this._physicsActivating = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rb.enabled) {
|
|
|
|
|
rb.enabled = false;
|
|
|
|
|
}
|
|
|
|
|
this._enableBoardCollider(false);
|
|
|
|
|
this._bindFallContact();
|
|
|
|
|
|
|
|
|
|
rb.enabled = true;
|
|
|
|
|
rb.type = ERigidBody2DType.Dynamic;
|
|
|
|
|
rb.gravityScale = 5;
|
|
|
|
|
rb.linearDamping = 0.2;
|
|
|
|
|
rb.angularDamping = 5;
|
|
|
|
|
rb.fixedRotation = false;
|
|
|
|
|
rb.allowSleep = true;
|
|
|
|
|
rb.bullet = false;
|
|
|
|
|
rb.enabledContactListener = true;
|
|
|
|
|
rb.linearVelocity = Vec2.ZERO;
|
|
|
|
|
rb.angularVelocity = 0;
|
|
|
|
|
rb.wakeUp();
|
|
|
|
|
this._physicsActivating = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private _bindFallContact() {
|
|
|
|
|
if (this._fallContactBound) return;
|
|
|
|
|
const col = this.node.getComponent(Collider2D);
|
|
|
|
|
if (!col) return;
|
|
|
|
|
col.on(Contact2DType.BEGIN_CONTACT, this._onFallFloorContact, this);
|
|
|
|
|
this._fallContactBound = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _unbindFallContact() {
|
|
|
|
|
if (!this._fallContactBound) return;
|
|
|
|
|
const col = this.node.getComponent(Collider2D);
|
|
|
|
|
col?.off(Contact2DType.BEGIN_CONTACT, this._onFallFloorContact, this);
|
|
|
|
|
this._fallContactBound = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _scheduleFloorDestroy = () => {
|
|
|
|
|
this._destroyFallenBoard();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private _onFallFloorContact(_self: Collider2D, other: Collider2D, _contact: IPhysics2DContact | null) {
|
|
|
|
|
if (!this._fallCheckEnabled || !other?.node?.isValid) return;
|
|
|
|
|
const n = other.node.name;
|
|
|
|
|
const p = other.node.parent?.name ?? '';
|
|
|
|
|
if (n.indexOf('floor') >= 0 || p === 'ui_floorNode') {
|
|
|
|
|
this.scheduleOnce(this._scheduleFloorDestroy, BOARD_FALL_FLOOR_DESTROY_DELAY);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _enableBoardCollider(sensor: boolean) {
|
|
|
|
|
const poly = this.node.getComponent(PolygonCollider2D);
|
|
|
|
|
if (!poly) return;
|
|
|
|
|
poly.enabled = true;
|
|
|
|
|
poly.sensor = sensor;
|
|
|
|
|
poly.friction = 0.4;
|
|
|
|
|
poly.restitution = 0;
|
|
|
|
|
const apply = (poly as { apply?: () => void }).apply;
|
|
|
|
|
if (typeof apply === 'function') apply.call(poly);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _destroyFallenBoard() {
|
|
|
|
|
if (!this.node?.isValid) return;
|
|
|
|
|
this.unschedule(this._scheduleFloorDestroy);
|
|
|
|
|
this._unbindFallContact();
|
|
|
|
|
const rb = this.node.getComponent(RigidBody2D);
|
|
|
|
|
if (rb) rb.enabled = false;
|
|
|
|
|
this.node.destroy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDestroy() {
|
|
|
|
|
this._unbindFallContact();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update(dt: number) {
|
|
|
|
|
if (!this.node?.isValid) return;
|
|
|
|
|
|
|
|
|
|
if (!this._fallCheckEnabled) {
|
|
|
|
|
if (this._remainingScrews <= 0 && this._initialScrewCount > 0
|
|
|
|
|
&& LuosiDing.countAttachedScrewsInHoles(this.node) === 0) {
|
|
|
|
|
this._enterFreeFall();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._fallLifeAcc += dt;
|
|
|
|
|
|
|
|
|
|
if (this._fallLifeAcc >= BOARD_FALL_MAX_LIFE_SEC) {
|
|
|
|
|
this._destroyFallenBoard();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.node.position.y < BOARD_FALL_DESTROY_LOCAL_Y) {
|
|
|
|
|
this._destroyFallenBoard();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rb = this.node.getComponent(RigidBody2D);
|
|
|
|
|
if (!rb?.enabled && !this._physicsActivating) {
|
|
|
|
|
this._activateFallRigidbody();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|