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

820 lines
30 KiB

1 week ago
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;
}
4 days ago
getBoardNode(): Node | null {
return this._boardAelement?.node ?? null;
}
1 week ago
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<string, Vec2[] | null>(),
);
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<string, Vec2[] | null>,
): 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<string, Vec2[] | null>,
): Vec2[] | null {
const key = boardNode.uuid;
if (cache.has(key)) return cache.get(key) ?? null;
const poly = boardNode.getComponent(PolygonCollider2D);
4 days ago
const worldPoly = poly ? LuosiDing._getPolygonWorldPoints(poly) : null;
1 week ago
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() {
4 days ago
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<LuosiDing>();
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;
}
1 week ago
const hinge = this.getComponent(HingeJoint2D);
4 days ago
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;
}
1 week ago
}
/** 通知木板:本螺丝已脱离 */
private _notifyBoardScrewLeft() {
if (this._notifiedBoardLeft) return;
this._notifiedBoardLeft = true;
4 days ago
const board = this._boardAelement;
if (!board?.isValid || !board.node?.isValid) return;
board.onScrewLeft();
1 week ago
}
/** 弹出动画结束:恢复碰撞并启用 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;
4 days ago
const wasOnBoard = this.isAttachedOnBoard();
1 week ago
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;
4 days ago
if (wasOnBoard) {
1 week ago
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();
}
}
/**
4 days ago
* /
1 week ago
*/
bindBoardPhysics(boardNode: Node) {
if (!boardNode?.isValid) return;
this._boardAelement = boardNode.getComponent(Aelement);
4 days ago
this.detachBoardHinge();
1 week ago
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<number, number>();
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;
}
}