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.
696 lines
25 KiB
696 lines
25 KiB
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;
|
|
}
|
|
|
|
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);
|
|
const worldPoly = poly?.enabled ? 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() {
|
|
const hinge = this.getComponent(HingeJoint2D);
|
|
if (!hinge) return;
|
|
hinge.connectedBody = null;
|
|
hinge.enabled = false;
|
|
}
|
|
|
|
/** 通知木板:本螺丝已脱离 */
|
|
private _notifyBoardScrewLeft() {
|
|
if (this._notifiedBoardLeft) return;
|
|
this._notifiedBoardLeft = true;
|
|
this._boardAelement?.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;
|
|
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 (this.isAttachedOnBoard()) {
|
|
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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 开启螺丝刚体,并将铰链关节的 connectedBody 绑定到所在木板的刚体。
|
|
* 锚点按螺丝世界坐标换算到木板本地空间。
|
|
*/
|
|
bindBoardPhysics(boardNode: Node) {
|
|
if (!boardNode?.isValid) return;
|
|
this._boardAelement = boardNode.getComponent(Aelement);
|
|
const boardRb = boardNode.getComponent(RigidBody2D);
|
|
if (!boardRb) {
|
|
console.warn('[LuosiDing] 木板缺少 RigidBody2D', boardNode.name);
|
|
return;
|
|
}
|
|
const screwRb = this.getComponent(RigidBody2D);
|
|
if (screwRb) {
|
|
screwRb.enabled = true;
|
|
screwRb.type = ERigidBody2DType.Static;
|
|
}
|
|
this._setColliderSensor(true);
|
|
|
|
const hinge = this.getComponent(HingeJoint2D);
|
|
if (!hinge) return;
|
|
hinge.connectedBody = boardRb;
|
|
const boardUt = boardNode.getComponent(UITransform);
|
|
if (boardUt) {
|
|
const local = new Vec3();
|
|
boardUt.convertToNodeSpaceAR(this.node.worldPosition, local);
|
|
hinge.connectedAnchor = new Vec2(local.x, local.y);
|
|
hinge.anchor = new Vec2(0, 0);
|
|
}
|
|
hinge.enabled = 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;
|
|
}
|
|
}
|
|
|