feat(uno): implement drag-to-sort for player hand
- Add `pointermove`, `pointerup`, and `pointerdown` handlers for smooth card dragging - Implement visual feedback including card tilt, scale, and insertion slot indicators - Update hand array order and re-render layout upon drop completion - Refactor hover/click logic to support both interaction modes
This commit is contained in:
parent
3d5c49d325
commit
f829ecf93b
|
|
@ -145,6 +145,10 @@ export default class UnoGame extends Phaser.Scene {
|
|||
this.directionArrow = null;
|
||||
this.colorSwatch = null;
|
||||
this.pendingWildPlayCardId = null; // local: card chosen but awaiting color
|
||||
|
||||
this.localHandCards = [];
|
||||
this.dragState = null;
|
||||
this.potentialDrag = null;
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -154,6 +158,7 @@ export default class UnoGame extends Phaser.Scene {
|
|||
this.buildSeatAreas();
|
||||
this.buildCenter();
|
||||
this.buildHUD();
|
||||
this.setupDragHandlers();
|
||||
this.startNewMatch();
|
||||
}
|
||||
|
||||
|
|
@ -565,6 +570,8 @@ export default class UnoGame extends Phaser.Scene {
|
|||
? new Set(legalPlays(this.gs, 0))
|
||||
: null;
|
||||
|
||||
if (seat === 0) this.localHandCards = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const card = player.hand[i];
|
||||
const offset = i - (n - 1) / 2;
|
||||
|
|
@ -583,6 +590,7 @@ export default class UnoGame extends Phaser.Scene {
|
|||
this.cardObjs.set(`hand-${seat}-${card.id}`, c);
|
||||
|
||||
if (seat === 0) {
|
||||
this.localHandCards.push(c);
|
||||
c.setInteractive(
|
||||
new Phaser.Geom.Rectangle(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H),
|
||||
Phaser.Geom.Rectangle.Contains,
|
||||
|
|
@ -590,15 +598,16 @@ export default class UnoGame extends Phaser.Scene {
|
|||
c.input.cursor = 'pointer';
|
||||
const baseY = y;
|
||||
c.on('pointerover', () => {
|
||||
if (this.animating) return;
|
||||
if (this.animating || this.dragState) return;
|
||||
this.tweens.add({ targets: c, y: baseY - 14, duration: 120 });
|
||||
c.setDepth(D.highlight);
|
||||
});
|
||||
c.on('pointerout', () => {
|
||||
if (this.dragState) return;
|
||||
this.tweens.add({ targets: c, y: baseY, duration: 120 });
|
||||
c.setDepth(D.card);
|
||||
});
|
||||
c.on('pointerdown', () => this.onHandCardClick(card.id));
|
||||
c.on('pointerdown', (pointer) => this.onHandPointerDown(i, pointer));
|
||||
if (localLegal && !localLegal.has(card.id)) {
|
||||
c.setAlpha(0.6);
|
||||
}
|
||||
|
|
@ -615,6 +624,156 @@ export default class UnoGame extends Phaser.Scene {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Hand drag-to-sort ─────────────────────────────────────────────────────
|
||||
|
||||
setupDragHandlers() {
|
||||
this.input.on('pointermove', (pointer) => {
|
||||
if (this.potentialDrag && !this.dragState) {
|
||||
const dx = pointer.x - this.potentialDrag.startX;
|
||||
const dy = pointer.y - this.potentialDrag.startY;
|
||||
if (Math.sqrt(dx * dx + dy * dy) >= 8) {
|
||||
const pd = this.potentialDrag;
|
||||
this.potentialDrag = null;
|
||||
this.startCardDrag(pd.handIdx, pd.offsetX, pd.offsetY);
|
||||
}
|
||||
}
|
||||
if (this.dragState) {
|
||||
this.updateCardDrag(pointer);
|
||||
}
|
||||
});
|
||||
|
||||
this.input.on('pointerup', () => {
|
||||
if (this.potentialDrag) {
|
||||
const { cardId } = this.potentialDrag;
|
||||
this.potentialDrag = null;
|
||||
this.onHandCardClick(cardId);
|
||||
return;
|
||||
}
|
||||
if (this.dragState) {
|
||||
this.endCardDrag();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onHandPointerDown(handIdx, pointer) {
|
||||
if (this.dragState || this.animating) return;
|
||||
const card = this.localHandCards[handIdx];
|
||||
if (!card) return;
|
||||
const cardId = this.gs.players[0].hand[handIdx]?.id;
|
||||
if (!cardId) return;
|
||||
this.potentialDrag = {
|
||||
handIdx,
|
||||
cardId,
|
||||
startX: pointer.x,
|
||||
startY: pointer.y,
|
||||
offsetX: pointer.x - card.x,
|
||||
offsetY: pointer.y - card.y,
|
||||
};
|
||||
}
|
||||
|
||||
startCardDrag(handIdx, offsetX, offsetY) {
|
||||
const card = this.localHandCards[handIdx];
|
||||
if (!card) return;
|
||||
const n = this.gs.players[0].hand.length;
|
||||
const layout = slotLayout('bottom', this.gs.players.length);
|
||||
const spread = this.handSpreadFor(0, n);
|
||||
const handStartX = layout.handCenter.x - (n - 1) / 2 * spread;
|
||||
const handCenterY = layout.handCenter.y;
|
||||
const insertIdx = Phaser.Math.Clamp(Math.round((card.x - handStartX) / spread), 0, n - 1);
|
||||
|
||||
this.tweens.killTweensOf(card);
|
||||
|
||||
const shadow = this.add.graphics();
|
||||
shadow.fillStyle(0x000000, 0.35);
|
||||
shadow.fillEllipse(0, 0, CARD_W * 1.1, 28);
|
||||
shadow.setPosition(card.x + 6, card.y + 16);
|
||||
shadow.setDepth(D.card + 9);
|
||||
this.transientObjs.push(shadow);
|
||||
|
||||
const slotIndicator = this.add.graphics();
|
||||
slotIndicator.lineStyle(3, COLORS.accent, 0.75);
|
||||
slotIndicator.strokeRoundedRect(-CARD_W / 2 - 4, -CARD_H / 2 - 4, CARD_W + 8, CARD_H + 8, 10);
|
||||
slotIndicator.fillStyle(COLORS.accent, 0.1);
|
||||
slotIndicator.fillRoundedRect(-CARD_W / 2 - 4, -CARD_H / 2 - 4, CARD_W + 8, CARD_H + 8, 10);
|
||||
slotIndicator.setPosition(handStartX + insertIdx * spread, handCenterY);
|
||||
slotIndicator.setDepth(D.card - 1);
|
||||
this.transientObjs.push(slotIndicator);
|
||||
|
||||
this.dragState = { cardIdx: handIdx, offsetX, offsetY, prevX: card.x, shadow, slotIndicator, insertIdx, spread, handStartX, handCenterY };
|
||||
|
||||
card.setDepth(D.card + 10);
|
||||
this.tweens.add({ targets: card, scaleX: 1.08, scaleY: 1.08, duration: 120, ease: 'Cubic.easeOut' });
|
||||
}
|
||||
|
||||
updateCardDrag(pointer) {
|
||||
const ds = this.dragState;
|
||||
const card = this.localHandCards[ds.cardIdx];
|
||||
const n = this.gs.players[0].hand.length;
|
||||
|
||||
card.x = pointer.x - ds.offsetX;
|
||||
card.y = pointer.y - ds.offsetY;
|
||||
ds.shadow.setPosition(card.x + 6, card.y + 16);
|
||||
|
||||
const tiltRad = Phaser.Math.Clamp((pointer.x - ds.prevX) * 0.008, -0.17, 0.17);
|
||||
card.setRotation(tiltRad);
|
||||
ds.prevX = pointer.x;
|
||||
|
||||
const newInsertIdx = Phaser.Math.Clamp(Math.round((card.x - ds.handStartX) / ds.spread), 0, Math.max(0, n - 1));
|
||||
if (newInsertIdx !== ds.insertIdx) {
|
||||
ds.insertIdx = newInsertIdx;
|
||||
this.tweens.killTweensOf(ds.slotIndicator);
|
||||
this.tweens.add({ targets: ds.slotIndicator, x: ds.handStartX + newInsertIdx * ds.spread, duration: 100, ease: 'Cubic.easeOut' });
|
||||
this._updateNonDraggedCards();
|
||||
}
|
||||
}
|
||||
|
||||
_updateNonDraggedCards() {
|
||||
const ds = this.dragState;
|
||||
const n = this.gs.players[0].hand.length;
|
||||
const others = [];
|
||||
for (let i = 0; i < n; i++) { if (i !== ds.cardIdx) others.push(i); }
|
||||
|
||||
for (let k = 0; k < others.length; k++) {
|
||||
const j = others[k];
|
||||
const finalPos = k < ds.insertIdx ? k : k + 1;
|
||||
const targetX = ds.handStartX + finalPos * ds.spread;
|
||||
const distFromGap = Math.abs(finalPos - ds.insertIdx);
|
||||
const leanDir = finalPos < ds.insertIdx ? -1 : 1;
|
||||
const targetRot = leanDir * Math.max(0, 2 - distFromGap) * 0.04;
|
||||
const targetScale = distFromGap <= 1 ? 0.95 : 1.0;
|
||||
const cardObj = this.localHandCards[j];
|
||||
this.tweens.killTweensOf(cardObj);
|
||||
this.tweens.add({ targets: cardObj, x: targetX, rotation: targetRot, scaleX: targetScale, scaleY: targetScale, duration: 100, ease: 'Cubic.easeOut' });
|
||||
}
|
||||
}
|
||||
|
||||
endCardDrag() {
|
||||
const ds = this.dragState;
|
||||
const card = this.localHandCards[ds.cardIdx];
|
||||
const hand = this.gs.players[0].hand;
|
||||
const n = hand.length;
|
||||
const finalIdx = Phaser.Math.Clamp(Math.round((card.x - ds.handStartX) / ds.spread), 0, n - 1);
|
||||
|
||||
this.tweens.killTweensOf(card);
|
||||
this.tweens.add({ targets: card, x: ds.handStartX + finalIdx * ds.spread, y: ds.handCenterY, rotation: 0, scaleX: 1, scaleY: 1, duration: 220, ease: 'Back.easeOut' });
|
||||
|
||||
const others = [];
|
||||
for (let i = 0; i < n; i++) { if (i !== ds.cardIdx) others.push(i); }
|
||||
for (let k = 0; k < others.length; k++) {
|
||||
const j = others[k];
|
||||
const finalPos = k < finalIdx ? k : k + 1;
|
||||
const cardObj = this.localHandCards[j];
|
||||
this.tweens.killTweensOf(cardObj);
|
||||
this.tweens.add({ targets: cardObj, x: ds.handStartX + finalPos * ds.spread, rotation: 0, scaleX: 1, scaleY: 1, duration: 120, ease: 'Cubic.easeOut' });
|
||||
}
|
||||
|
||||
const [moved] = hand.splice(ds.cardIdx, 1);
|
||||
hand.splice(finalIdx, 0, moved);
|
||||
|
||||
this.dragState = null;
|
||||
this.time.delayedCall(240, () => this.renderAll());
|
||||
}
|
||||
|
||||
renderTurnIndicator() {
|
||||
const seat = this.gs.currentPlayer;
|
||||
const slot = this.slotForSeat[seat];
|
||||
|
|
|
|||
Loading…
Reference in New Issue