import { _decorator, CCInteger, CircleCollider2D, Collider2D, Color, Component, Contact2DType, ERigidBody2DType, HingeJoint2D, IPhysics2DContact, Mat4, Node, ParticleSystem2D, PolygonCollider2D, RigidBody2D, Sprite, Tween, tween, UITransform, Vec2, Vec3, v3, } from 'cc'; import { AelementLayer } from './AelementLayer'; import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad'; import { GBundle, GPath } from '../../game/ConfigRes'; import { Aelement } from './Aelement'; const { ccclass, property } = _decorator; /** 滚动中判定为静止的速度阈值 */ const STILL_SPEED_SQ = 900; /** 螺丝无 CircleCollider2D 时的默认半径(与 prefab 一致) */ const SCREW_DEFAULT_RADIUS = 25; /** 广告消除:在 ui_luosidingTemp 上展示螺丝后再播粒子 */ const FORCE_ELIMINATE_SHOW_SEC = 0.28; /** 广告消除:circle 粒子主要消散后即可飞经验(小于节点销毁用的完整 delay) */ const FORCE_ELIMINATE_PARTICLE_TO_EXP_SEC = 0.72; /**元素洞里面的螺丝钉*/ @ccclass('LuosiDing') export class LuosiDing extends Component { @property(CCInteger) luosiType: number = 1; static readonly LUOSI_COLORS = ['e99440', 'ff7bb5', 'ffde25', '2f36ff', 'e13109', 'bfc1c4', '20daff', '0c9140', '8c42ef', '69ef22']; luosiColors = LuosiDing.LUOSI_COLORS; luosiNode:Node = null; luosidingNode:Node = null; /** 已弹出,不再响应点击 */ private _released = false; /** 正在配对移除,避免重复触发 */ private _removing = false; private _popTweening = false; /** 所在木板(bindBoardPhysics 时绑定) */ private _boardAelement: Aelement | null = null; /** 已通知木板该螺丝脱落 */ private _notifiedBoardLeft = false; /** luosi 头抖动基准本地坐标(避免连点抖动后位置漂移) */ private _luosiHeadBasePos = v3(); protected start(): void { this.luosiNode = this.node.getChildByName('luosi'); this.luosidingNode = this.node.getChildByName('luosiding'); if (this.luosiNode?.isValid) { this._luosiHeadBasePos.set(this.luosiNode.position); } } onEnable() { if (!this._released) { this.node.on(Node.EventType.TOUCH_END, this._onTouchEnd, this); } const col = this.getComponent(Collider2D); if (col) { col.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this); } } onDisable() { this.node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this); const col = this.getComponent(Collider2D); if (col) { col.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this); } } /** 仍固定在木板孔洞上(未弹出、未消除) */ isAttachedOnBoard(): boolean { return !this._released && !this._removing && !this._popTweening; } getBoardNode(): Node | null { return this._boardAelement?.node ?? null; } isRemoving(): boolean { return this._removing; } /** 槽内螺丝是否已基本静止(用于卡槽失败检测) */ isNearlyStillInSlot(): boolean { if (!this.isInLuosidingSlot()) return true; return this._isNearlyStill(); } /** 槽内配对消除粒子播完的大致时长(与 _playMatchParticleOn 默认参数一致) */ static estimateSlotPairParticleDelay(): number { const duration = 0.5; const life = 0.8 + 0.4; return duration + life + 0.1; } /** 已弹出并落在 ui_luosiding 槽内 */ isInLuosidingSlot(): boolean { const slot = gg.game.CurentBattle?.ui_luosiding; return !!slot?.isValid && this.node.parent === slot && this._released && !this._removing && !this._popTweening; } /** 设置螺丝种类(1~maxType),并刷新贴图 */ setLuosiType(type: number) { this.luosiType = type; this.refreshSprite(); } /** 打乱后:luosi 头从小缩放到正常 */ playTypeShuffleScaleAnim() { const head = this.luosiNode ?? this.node.getChildByName('luosi'); if (!head?.isValid) return; Tween.stopAllByTarget(head); const target = head.scale.clone(); head.setScale(target.x * 0.15, target.y * 0.15, target.z); tween(head) .to(0.22, { scale: target }, { easing: 'backOut' }) .start(); } /** * 打乱仍挂在木板上的螺丝种类(保持各颜色数量不变,仅重排),并播放缩放动画。 */ static shuffleAttachedTypes(screws: LuosiDing[]) { if (screws.length < 2) return; const types = screws.map(s => s.luosiType); for (let i = types.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [types[i], types[j]] = [types[j], types[i]]; } for (let i = 0; i < screws.length; i++) { screws[i].setLuosiType(types[i]); screws[i].playTypeShuffleScaleAnim(); } } /** 按 luosiType 加载螺丝贴图:texture/螺丝/螺丝N */ refreshSprite() { const sp = this.node.getChildByName('luosi').getComponent(Sprite); if (!sp) return; sp.node.getOrAddComponent(SpriteLoad).setSprite( GPath.LuosiDingTex(String(this.luosiType)), GBundle.jiujiuwoyaCard, ); } /** 显示 faguang 发光节点,duration 秒后自动隐藏;duration<=0 则常亮直到手动 hideFaguang */ showFaguang(duration = 0.2) { const faguang = this.node.getChildByName('faguang'); if (!faguang?.isValid) return; faguang.active = true; this.unschedule(this._hideFaguang); if (duration > 0) { this.scheduleOnce(this._hideFaguang, duration); } } hideFaguang() { this.unschedule(this._hideFaguang); const faguang = this.node.getChildByName('faguang'); if (faguang?.isValid) faguang.active = false; } private _hideFaguang() { this.hideFaguang(); } private _onTouchEnd() { if (!gg.game.CurentBattle.mubanSpawnFinished) { return } if (this._released || this._popTweening || this._removing) return; this.showFaguang(0.2); if (this._isBlockedByUpperBoard()) { this._playBlockedShake(); return; } this._popTweening = true; this.node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this); this._detachHinge(); const rb = this.getComponent(RigidBody2D); if (rb) rb.enabled = false; const luosiParent = gg.game.CurentBattle?.ui_luosiding; if (!luosiParent?.isValid) { console.warn('[LuosiDing] ui_luosiding 未就绪'); return; } const worldPos = this.node.worldPosition.clone(); this.node.setParent(luosiParent); this.node.worldPosition = worldPos; this.node.angle = 0 this._playPopOutAnim(() => { this._popTweening = false; this._releaseScrew(); gg.game.CurentBattle?.onJiujiuwoyaLuosiEnteredSlot?.(this); }); } /** 被上层木板压住:luosiNode 上下抖动提示无法弹出 */ private _playBlockedShake() { const head = this.luosiNode ?? this.node.getChildByName('luosi'); if (!head?.isValid) return; Tween.stopAllByTarget(head); head.setPosition(this._luosiHeadBasePos); const p = this._luosiHeadBasePos; const amp = 8; const step = 0.045; tween(head) .to(step, { position: v3(p.x, p.y + amp, p.z) }) .to(step, { position: v3(p.x, p.y - amp, p.z) }) .to(step, { position: v3(p.x, p.y + amp * 0.5, p.z) }) .to(step, { position: v3(p.x, p.y, p.z) }) .call(() => head.setPosition(this._luosiHeadBasePos)) .start(); } /** * 是否被上层木板挡住(严格几何): * 螺丝 CircleCollider2D 世界圆 与 上层木板 PolygonCollider2D 世界多边形是否相交; * 相交的多块板中取最上层,若最上层不是本螺丝所在木板则不可弹出。 */ private _isBlockedByUpperBoard(): boolean { const ownBoard = this._boardAelement?.node; if (!ownBoard?.isValid) return false; const mubanLayer = gg.game.CurentBattle?.ui_mubanLayer; if (!mubanLayer?.isValid) return false; const ownLayerNode = ownBoard.parent; if (!ownLayerNode?.isValid) return false; const circle = this._getScrewWorldCircle(); if (!circle) return false; const top = this._findTopmostBoardOverlappingCircle( circle, mubanLayer, ownLayerNode.getSiblingIndex(), new Map(), ); return top !== null && top !== ownBoard; } /** 螺丝 CircleCollider2D → 世界圆(中心随刚体节点,半径含缩放) */ private _getScrewWorldCircle(): { center: Vec2; radius: number } | null { const col = this.getComponent(CircleCollider2D); const node = col?.node ?? this.node; if (!node?.isValid) return null; const wm = new Mat4(); node.getWorldMatrix(wm); const offset = col?.offset ?? Vec2.ZERO; const wp = new Vec3(); Vec3.transformMat4(wp, v3(offset.x, offset.y, 0), wm); const ws = node.worldScale; const scale = Math.max(Math.abs(ws.x), Math.abs(ws.y), 1); const radius = SCREW_DEFAULT_RADIUS*scale;//(col?.radius ?? SCREW_DEFAULT_RADIUS) * scale; return { center: new Vec2(wp.x, wp.y), radius }; } /** 与螺丝圆相交的木板中取最上层(layer / 同层 sibling 越大越靠上) */ private _findTopmostBoardOverlappingCircle( circle: { center: Vec2; radius: number }, mubanLayer: Node, ownLayerIdx: number, polyCache: Map, ): Node | null { let topBoard: Node | null = null; let topLayerIdx = -1; let topBoardIdx = -1; const layerChildren = mubanLayer.children; for (let li = ownLayerIdx; li < layerChildren.length; li++) { const layerNode = layerChildren[li]; if (!layerNode?.isValid || !layerNode.getComponent(AelementLayer)) continue; for (const board of layerNode.children) { if (!board?.isValid) continue; const ae = board.getComponent(Aelement); if (!ae || ae.isFalling) continue; const worldPoly = this._getBoardWorldPolyCached(board, polyCache); if (!worldPoly) continue; if (!LuosiDing._circleIntersectsPolygon( circle.center.x, circle.center.y, circle.radius, worldPoly, )) continue; const bi = board.getSiblingIndex(); if (li > topLayerIdx || (li === topLayerIdx && bi > topBoardIdx)) { topLayerIdx = li; topBoardIdx = bi; topBoard = board; } } } return topBoard; } /** 圆与多边形是否相交(圆心在内 / 顶点在圆内 / 边与圆相交) */ private static _circleIntersectsPolygon( cx: number, cy: number, r: number, poly: readonly Vec2[], ): boolean { const center = new Vec2(cx, cy); if (LuosiDing._pointInPolygon(center, poly)) return true; const r2 = r * r; for (const p of poly) { const dx = p.x - cx; const dy = p.y - cy; if (dx * dx + dy * dy <= r2) return true; } for (let i = 0; i < poly.length; i++) { const a = poly[i]; const b = poly[(i + 1) % poly.length]; if (LuosiDing._pointSegmentDistSq(center, a, b) <= r2) return true; } return false; } private static _pointSegmentDistSq(p: Vec2, a: Vec2, b: Vec2): number { const abx = b.x - a.x; const aby = b.y - a.y; const apx = p.x - a.x; const apy = p.y - a.y; const abLenSq = abx * abx + aby * aby; if (abLenSq <= 1e-8) { const dx = p.x - a.x; const dy = p.y - a.y; return dx * dx + dy * dy; } const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / abLenSq)); const qx = a.x + abx * t; const qy = a.y + aby * t; const dx = p.x - qx; const dy = p.y - qy; return dx * dx + dy * dy; } private _getBoardWorldPolyCached( boardNode: Node, cache: Map, ): Vec2[] | null { const key = boardNode.uuid; if (cache.has(key)) return cache.get(key) ?? null; const poly = boardNode.getComponent(PolygonCollider2D); const worldPoly = poly ? LuosiDing._getPolygonWorldPoints(poly) : null; cache.set(key, worldPoly); return worldPoly; } private static _getPolygonWorldPoints(poly: PolygonCollider2D): Vec2[] | null { const pts = poly.points ?? []; if (pts.length < 3 || !poly.node?.isValid) return null; const wm = new Mat4(); poly.node.getWorldMatrix(wm); const wp = new Vec3(); const out: Vec2[] = []; const offset = poly.offset ?? Vec2.ZERO; for (const p of pts) { Vec3.transformMat4(wp, v3(p.x + offset.x, p.y + offset.y, 0), wm); out.push(new Vec2(wp.x, wp.y)); } return out.length >= 3 ? out : null; } private static _pointInPolygon(pt: Vec2, poly: readonly 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; } /** 起螺丝:下压拧动 → 旋出 360° 并弹起 → 轻微回弹 */ private _playPopOutAnim(onDone: () => void) { const p = this.node.position; const baseAngle = this.node.angle; const liftY = 36; let luosiba =this.node.getChildByName('luosiding') let _time = 0.2; luosiba.active = true tween(luosiba) .delay(0.01) .to(0.1,{y:-30}) .start() tween(this.luosiNode) .delay(0.01) .to(_time, { angle: baseAngle + 360, }, { easing: 'backOut' }) .start(); tween(this.node) .delay(0.01) .to(_time, { position: v3(p.x, p.y + liftY, p.z), }, { easing: 'backOut' }) .call(onDone) .start(); } private _detachHinge() { this.detachBoardHinge(); } /** 孔洞内仍固定的螺丝(不含提到 layer 的铰链锚点) */ static countAttachedScrewsInHoles(boardNode: Node): number { if (!boardNode?.isValid) return 0; let count = 0; const aelement = boardNode.getComponent(Aelement); for (const hole of aelement?.getHoleNodes() ?? []) { if (!hole?.isValid) continue; for (const child of hole.children) { if (!child?.isValid) continue; if (child.getComponent(LuosiDing)?.isAttachedOnBoard()) count++; } } return count; } /** 铰链锚点:孔洞内 + 已提到 layer 的螺丝 */ static findPhysicsAnchorScrews(boardNode: Node): LuosiDing[] { if (!boardNode?.isValid) return []; const screws: LuosiDing[] = []; const seen = new Set(); const aelement = boardNode.getComponent(Aelement); const tryAdd = (luosi: LuosiDing | null) => { if (!luosi?.isValid || !luosi.isAttachedOnBoard()) return; const owner = luosi.getBoardNode(); if (!owner?.isValid || owner !== boardNode) return; if (seen.has(luosi)) return; seen.add(luosi); screws.push(luosi); }; for (const hole of aelement?.getHoleNodes() ?? []) { if (!hole?.isValid) continue; for (const child of hole.children) { if (!child?.isValid) continue; tryAdd(child.getComponent(LuosiDing)); } } const layer = boardNode.parent; if (layer?.isValid) { for (const child of layer.children) { if (!child?.isValid || child === boardNode) continue; tryAdd(child.getComponent(LuosiDing)); } } return screws; } /** @deprecated 用 countAttachedScrewsInHoles / findPhysicsAnchorScrews */ static findAttachedScrews(boardNode: Node): LuosiDing[] { return LuosiDing.findPhysicsAnchorScrews(boardNode); } /** 按当前仍固定的螺丝刷新铰链(螺丝提到 layer 作静态锚点) */ static refreshBoardHinges(boardNode: Node) { if (!boardNode?.isValid) return; LuosiDing.detachAllBoardHinges(boardNode); const boardRb = boardNode.getComponent(RigidBody2D); if (!boardRb) return; for (const screw of LuosiDing.findPhysicsAnchorScrews(boardNode)) { if (!screw?.isValid) continue; screw.attachBoardHinge(boardNode, boardRb); } } static detachAllBoardHinges(boardNode: Node) { if (!boardNode?.isValid) return; for (const screw of LuosiDing.findPhysicsAnchorScrews(boardNode)) { if (!screw?.isValid) continue; screw.detachBoardHinge(); } } /** * 将螺丝作为静态锚点,用 HingeJoint2D 约束木板刚体。 * 螺丝需提到与木板同层,避免随木板节点树位移导致锚点失效。 * 提到 layer 后必须插到「所属木板」紧后面,不能落到 children 末尾, * 否则会画在同层其它更上层木板之上(看起来像下层螺丝穿板冒顶)。 */ attachBoardHinge(boardNode: Node, boardRb?: RigidBody2D | null) { if (!boardNode?.isValid || !this.isAttachedOnBoard()) return; const rb = boardRb ?? boardNode.getComponent(RigidBody2D); if (!rb) return; const anchorLayer = boardNode.parent; if (anchorLayer?.isValid && this.node.parent !== anchorLayer) { const wp = this.node.worldPosition.clone(); this.node.setParent(anchorLayer); this.node.worldPosition = wp; } this._placeScrewSiblingJustAboveBoard(boardNode); const screwRb = this.getComponent(RigidBody2D); if (screwRb) { screwRb.enabled = true; screwRb.type = ERigidBody2DType.Static; screwRb.linearVelocity = Vec2.ZERO; screwRb.angularVelocity = 0; } const hinge = this.getComponent(HingeJoint2D); if (hinge) { const boardLocal = new Vec3(); boardNode.inverseTransformPoint(boardLocal, this.node.worldPosition); hinge.anchor = Vec2.ZERO; hinge.connectedAnchor = new Vec2(boardLocal.x, boardLocal.y); hinge.connectedBody = rb; hinge.collideConnected = false; hinge.enabled = true; const apply = (hinge as { apply?: () => void }).apply; if (typeof apply === 'function') apply.call(hinge); } this._setColliderSensor(true); } /** 同层内:螺丝紧挨在所属木板之后,仍低于后续 sibling 木板(上层遮挡) */ private _placeScrewSiblingJustAboveBoard(boardNode: Node) { if (!this.node?.isValid || !boardNode?.isValid) return; if (this.node.parent !== boardNode.parent) return; const boardIdx = boardNode.getSiblingIndex(); const screwIdx = this.node.getSiblingIndex(); // 螺丝已在木板前时,目标索引用 boardIdx(移动后木板会前移);否则用 boardIdx+1 const target = screwIdx < boardIdx ? boardIdx : boardIdx + 1; if (screwIdx !== target) { this.node.setSiblingIndex(target); } } detachBoardHinge() { const hinge = this.getComponent(HingeJoint2D); if (hinge) { hinge.enabled = false; hinge.connectedBody = null; } const screwRb = this.getComponent(RigidBody2D); if (screwRb) { screwRb.enabled = false; screwRb.linearVelocity = Vec2.ZERO; screwRb.angularVelocity = 0; } } /** 通知木板:本螺丝已脱离 */ private _notifyBoardScrewLeft() { if (this._notifiedBoardLeft) return; this._notifiedBoardLeft = true; const board = this._boardAelement; if (!board?.isValid || !board.node?.isValid) return; board.onScrewLeft(); } /** 弹出动画结束:恢复碰撞并启用 Dynamic 下落 */ private _releaseScrew() { if (this._released || this._removing) return; this._released = true; this._notifyBoardScrewLeft(); Tween.stopAllByTarget(this.node); if (this.luosiNode?.isValid) Tween.stopAllByTarget(this.luosiNode); const stem = this.luosidingNode ?? this.node.getChildByName('luosiding'); if (stem?.isValid) Tween.stopAllByTarget(stem); this._setColliderSensor(false); this._applyCollider(); const rb = this.getComponent(RigidBody2D); if (rb) { rb.enabled = true; rb.type = ERigidBody2DType.Dynamic; rb.bullet = true; rb.allowSleep = true; rb.wakeUp?.(); // this.scheduleOnce(()=>{ // this.luosidingNode.active = false; // },0.1) } } /** 挂板:传感器模式,不与其它刚体产生物理碰撞 */ private _setColliderSensor(sensor: boolean) { const col = this.getComponent(Collider2D); if (col) col.sensor = sensor; } private _applyCollider() { const col = this.getComponent(Collider2D); const apply = (col as { apply?: () => void })?.apply; if (typeof apply === 'function') apply.call(col); } private _onBeginContact( _self: Collider2D, other: Collider2D, _contact: IPhysics2DContact | null, ) { if (!this._released || this._removing) return; if(other.node.name.indexOf('floor') > -1){ _self.node.getChildByName('luosiding').active = false; } const otherLuosi = other.node.getComponent(LuosiDing); if (!otherLuosi?._released || otherLuosi._removing) return; _self.node.getChildByName('luosiding').active = false; other.node.getChildByName('luosiding').active = false; if (otherLuosi.luosiType !== this.luosiType) return; LuosiDing.eliminateSlotPair(this, otherLuosi); } private _isNearlyStill(): boolean { const rb = this.getComponent(RigidBody2D); if (!rb?.enabled) return false; const v = rb.linearVelocity; return v.x * v.x + v.y * v.y <= STILL_SPEED_SQ; } /** 按 luosiType 取螺丝主色(luosiColors[luosiType - 1]) */ static getLuosiColorByType(type: number): Color { const hex = LuosiDing.LUOSI_COLORS[type - 1]; if (!hex) return new Color(255, 255, 255, 255); return new Color().fromHEX(hex.startsWith('#') ? hex : `#${hex}`); } private _getLuosiColor(): Color { return LuosiDing.getLuosiColorByType(this.luosiType); } /** 配对消除:circle 粒子,颜色与螺丝一致;返回粒子播完的大致延迟 */ private _playMatchParticleOn(effectParent: Node | null): number { const circleNode = this.node.getChildByName('circle'); if (!circleNode) return 0.3; const ps = circleNode.getComponent(ParticleSystem2D); if (!ps) return 0.3; this.hideFaguang(); this.luosiNode.active = false this.luosidingNode.active = false; const color = this._getLuosiColor(); ps.startColor = color; ps.endColor = color; if (effectParent?.isValid) { const worldPos = circleNode.worldPosition.clone(); circleNode.setParent(effectParent); circleNode.worldPosition = worldPos; } circleNode.active = true; ps.resetSystem(); const life = (ps.life ?? 0.8) + (ps.lifeVar ?? 0.4); const delay = (ps.duration ?? 0.5) + life + 0.1; const host = effectParent?.getComponent(Component); if (host) { host.scheduleOnce(() => { if (circleNode?.isValid) circleNode.destroy(); }, delay); } else { setTimeout(() => { if (circleNode?.isValid) circleNode.destroy(); }, delay * 1000); } return delay; } private _playMatchParticle() { this._playMatchParticleOn(gg.game.CurentBattle?.ui_luosiding ?? null); } /** 广告消除:移到临时层、播粒子并销毁(不经过物理配对) */ private _prepareForceEliminate(tempParent: Node) { if (this._removing) return; const wasOnBoard = this.isAttachedOnBoard(); this._removing = true; this.node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this); const col = this.getComponent(Collider2D); if (col) col.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this); Tween.stopAllByTarget(this.node); if (this.luosiNode?.isValid) Tween.stopAllByTarget(this.luosiNode); const stem = this.luosidingNode ?? this.node.getChildByName('luosiding'); if (stem?.isValid) Tween.stopAllByTarget(stem); this._detachHinge(); const rb = this.getComponent(RigidBody2D); if (rb) rb.enabled = false; const collider = this.getComponent(Collider2D); if (collider) collider.enabled = false; if (wasOnBoard) { this._released = true; this._notifyBoardScrewLeft(); } if (tempParent?.isValid) { const wp = this.node.worldPosition.clone(); this.node.setParent(tempParent); this.node.worldPosition = wp; } } /** 强制消除一对螺丝(槽内+木板或木板成对) */ static forceEliminatePair(a: LuosiDing, b: LuosiDing, tempParent: Node) { if (!a?.isValid || !b?.isValid || a._removing || b._removing) return; a.showFaguang(0); b.showFaguang(0); a._prepareForceEliminate(tempParent); b._prepareForceEliminate(tempParent); // 临时层展示螺丝 → 播 circle 粒子 → 粒子视觉上消散后飞经验(不必等完整销毁 delay) a.scheduleOnce(() => { a._playMatchParticleOn(tempParent); b._playMatchParticleOn(tempParent); a.scheduleOnce(() => { gg.game.CurentBattle?.onJiujiuwoyaLuosiPairRemovedFromSlot?.(a, b); if (a.node?.isValid) a.node.destroy(); if (b.node?.isValid) b.node.destroy(); }, FORCE_ELIMINATE_PARTICLE_TO_EXP_SEC); }, FORCE_ELIMINATE_SHOW_SEC); } /** 槽内/木板同色两颗螺丝配对消除 */ static eliminateSlotPair(a: LuosiDing, b: LuosiDing) { if (!a?.isValid || !b?.isValid || a._removing || b._removing) return; a._removing = b._removing = true; a._playMatchParticle(); b._playMatchParticle(); let finished = 0; const onDone = () => { finished++; if (finished < 2) return; gg.game.CurentBattle?.onJiujiuwoyaLuosiPairRemovedFromSlot?.(a, b); if (a.node?.isValid) a.node.destroy(); if (b.node?.isValid) b.node.destroy(); }; for (const comp of [a, b]) { tween(comp.node) .to(0.12, { scale: v3(0, 0, 1) }) .call(onDone) .start(); } } /** * 挂板:登记引用;铰链仅在木板进入约束物理时按需启用(单颗螺丝摆动 / 多颗固定)。 */ bindBoardPhysics(boardNode: Node) { if (!boardNode?.isValid) return; this._boardAelement = boardNode.getComponent(Aelement); this.detachBoardHinge(); this._setColliderSensor(true); } /** * 生成螺丝种类分配表:长度为 totalCount,每种螺丝数量为偶数,且总和等于 totalCount。 * 实现方式:每次随机选一种螺丝,连续放入 2 个(成对),最后打乱顺序。 */ static buildEvenTypePool(totalCount: number, maxType: number): number[] { if (totalCount <= 0 || maxType <= 0) return []; if (totalCount % 2 !== 0) { console.error(`[LuosiDing] totalCount 必须为偶数,当前=${totalCount}`); return []; } const pool: number[] = []; const types = Array.from({ length: maxType }, (_, i) => i + 1); let remaining = totalCount; while (remaining > 0) { const type = types[Math.floor(Math.random() * types.length)]; pool.push(type, type); remaining -= 2; } for (let i = pool.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [pool[i], pool[j]] = [pool[j], pool[i]]; } return pool; } /** 校验分配表:总数、每种是否为偶数 */ static validateEvenTypePool(pool: number[], expectedTotal: number): boolean { if (pool.length !== expectedTotal) return false; const countMap = new Map(); for (const t of pool) { countMap.set(t, (countMap.get(t) ?? 0) + 1); } for (const [, cnt] of countMap) { if (cnt % 2 !== 0) return false; } return true; } }