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(); } } }