feat(uno): enable drag-and-drop playing of cards to discard pile

- Add visual highlight for discard zone when card is dragged over it
- Implement logic to detect valid plays against current discard pile
- Trigger play execution (including wild color picker) when dropped on discard
- Refactor `endCardDrag` to handle both reordering and playing paths
This commit is contained in:
Brian Fertig 2026-05-19 19:35:51 -06:00
parent f829ecf93b
commit 85dff94613
1 changed files with 81 additions and 7 deletions

View File

@ -699,7 +699,17 @@ export default class UnoGame extends Phaser.Scene {
slotIndicator.setDepth(D.card - 1);
this.transientObjs.push(slotIndicator);
this.dragState = { cardIdx: handIdx, offsetX, offsetY, prevX: card.x, shadow, slotIndicator, insertIdx, spread, handStartX, handCenterY };
const discardHighlight = this.add.graphics();
discardHighlight.lineStyle(3, COLORS.accent, 0.9);
discardHighlight.strokeRoundedRect(-CARD_W / 2 - 6, -CARD_H / 2 - 6, CARD_W + 12, CARD_H + 12, 12);
discardHighlight.fillStyle(COLORS.accent, 0.18);
discardHighlight.fillRoundedRect(-CARD_W / 2 - 6, -CARD_H / 2 - 6, CARD_W + 12, CARD_H + 12, 12);
discardHighlight.setPosition(DISCARD_POS.x, DISCARD_POS.y);
discardHighlight.setDepth(D.card + 1);
discardHighlight.setAlpha(0);
this.transientObjs.push(discardHighlight);
this.dragState = { cardIdx: handIdx, offsetX, offsetY, prevX: card.x, shadow, slotIndicator, discardHighlight, insertIdx, spread, handStartX, handCenterY, overDiscard: false };
card.setDepth(D.card + 10);
this.tweens.add({ targets: card, scaleX: 1.08, scaleY: 1.08, duration: 120, ease: 'Cubic.easeOut' });
@ -718,12 +728,50 @@ export default class UnoGame extends Phaser.Scene {
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();
const overDiscard = this._isDiscardDropTarget(card.x, card.y);
if (overDiscard !== ds.overDiscard) {
ds.overDiscard = overDiscard;
if (overDiscard) {
this.tweens.killTweensOf(ds.slotIndicator);
ds.slotIndicator.setAlpha(0);
this._settleNonDraggedCards();
this.tweens.add({ targets: ds.discardHighlight, alpha: 1, duration: 150 });
} else {
ds.slotIndicator.setAlpha(1);
this._updateNonDraggedCards();
this.tweens.add({ targets: ds.discardHighlight, alpha: 0, duration: 150 });
}
}
if (!overDiscard) {
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();
}
}
}
_isDiscardDropTarget(cardX, cardY) {
if (!this.isLocalTurn() || this.gs.phase !== 'play') return false;
const me = this.gs.players[0];
const handCard = me.hand[this.dragState.cardIdx];
if (!handCard || !isLegalPlay(this.gs, handCard, me.hand)) return false;
return Math.abs(cardX - DISCARD_POS.x) < CARD_W * 1.2 && Math.abs(cardY - DISCARD_POS.y) < CARD_H * 1.2;
}
_settleNonDraggedCards() {
const ds = this.dragState;
const n = this.gs.players[0].hand.length;
let k = 0;
for (let j = 0; j < n; j++) {
if (j === ds.cardIdx) continue;
const cardObj = this.localHandCards[j];
this.tweens.killTweensOf(cardObj);
this.tweens.add({ targets: cardObj, x: ds.handStartX + k * ds.spread, rotation: 0, scaleX: 1, scaleY: 1, duration: 100, ease: 'Cubic.easeOut' });
k++;
}
}
@ -752,6 +800,32 @@ export default class UnoGame extends Phaser.Scene {
const card = this.localHandCards[ds.cardIdx];
const hand = this.gs.players[0].hand;
const n = hand.length;
this.tweens.killTweensOf(ds.discardHighlight);
ds.discardHighlight.setAlpha(0);
// ── Play path: dropped on discard pile ───────────────────────────────────
if (ds.overDiscard) {
const handCard = hand[ds.cardIdx];
this.tweens.killTweensOf(card);
card.setScale(1);
card.setRotation(0);
ds.shadow.setAlpha(0);
this._settleNonDraggedCards();
this.dragState = null;
if (handCard.isWild) {
this.pendingWildPlayCardId = handCard.id;
this.openColorPicker((color) => {
this.pendingWildPlayCardId = null;
this.executeLocalPlay(handCard.id, color);
});
} else {
this.executeLocalPlay(handCard.id);
}
return;
}
// ── Reorder path: dropped back in hand ───────────────────────────────────
const finalIdx = Phaser.Math.Clamp(Math.round((card.x - ds.handStartX) / ds.spread), 0, n - 1);
this.tweens.killTweensOf(card);