feat: add drag-and-drop reordering for Phase10 hand cards
- Implement pointer-based drag handlers for Phase10 player hand. - Add visual feedback including card scaling, rotation tilt, shadow, and slot indicators during drag. - Reorder hand array and animate cards to new positions on drop. - Prevent conflicts by ending active drags during scene transitions or state updates.
This commit is contained in:
parent
5b65134ecc
commit
83ada10456
|
|
@ -146,6 +146,10 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
this.playbookOpen = false;
|
this.playbookOpen = false;
|
||||||
this.playbookPanel = null;
|
this.playbookPanel = null;
|
||||||
this.layDownBtn = null;
|
this.layDownBtn = null;
|
||||||
|
|
||||||
|
this.localHandCards = [];
|
||||||
|
this.dragState = null;
|
||||||
|
this.potentialDrag = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -155,6 +159,7 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
this.buildCenter();
|
this.buildCenter();
|
||||||
this.buildPlaybook();
|
this.buildPlaybook();
|
||||||
this.buildHUD();
|
this.buildHUD();
|
||||||
|
this.setupDragHandlers();
|
||||||
this.startNewMatch();
|
this.startNewMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -423,11 +428,172 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
this.layDownBtn.setVisible(false);
|
this.layDownBtn.setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Drag-and-drop hand reordering ───────────────────────────────────────
|
||||||
|
|
||||||
|
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', (pointer) => {
|
||||||
|
if (this.potentialDrag) {
|
||||||
|
const handIdx = this.potentialDrag.handIdx;
|
||||||
|
this.potentialDrag = null;
|
||||||
|
this.onHandClick(handIdx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.dragState) {
|
||||||
|
this.endCardDrag();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onHandPointerDown(handIdx, pointer) {
|
||||||
|
if (this.dragState) return;
|
||||||
|
const card = this.localHandCards[handIdx];
|
||||||
|
if (!card) return;
|
||||||
|
this.potentialDrag = {
|
||||||
|
handIdx,
|
||||||
|
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 insertIdx = Phaser.Math.Clamp(Math.round((card.x - 460) / HAND_SPREAD), 0, n - 1);
|
||||||
|
|
||||||
|
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(460 + insertIdx * HAND_SPREAD, 990);
|
||||||
|
slotIndicator.setDepth(D.card - 1);
|
||||||
|
this.transientObjs.push(slotIndicator);
|
||||||
|
|
||||||
|
this.dragState = { cardIdx: handIdx, offsetX, offsetY, prevX: card.x, shadow, slotIndicator, insertIdx };
|
||||||
|
|
||||||
|
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 - 460) / HAND_SPREAD), 0, n - 1);
|
||||||
|
if (newInsertIdx !== ds.insertIdx) {
|
||||||
|
ds.insertIdx = newInsertIdx;
|
||||||
|
this.tweens.killTweensOf(ds.slotIndicator);
|
||||||
|
this.tweens.add({ targets: ds.slotIndicator, x: 460 + newInsertIdx * HAND_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 = 460 + finalPos * HAND_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 n = this.gs.players[0].hand.length;
|
||||||
|
const finalIdx = Phaser.Math.Clamp(Math.round((card.x - 460) / HAND_SPREAD), 0, n - 1);
|
||||||
|
|
||||||
|
this.selectedHandIdx = null;
|
||||||
|
|
||||||
|
this.tweens.killTweensOf(card);
|
||||||
|
this.tweens.add({ targets: card, x: 460 + finalIdx * HAND_SPREAD, y: 990, 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: 460 + finalPos * HAND_SPREAD, rotation: 0, scaleX: 1, scaleY: 1, duration: 120, ease: 'Cubic.easeOut' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hand = this.gs.players[0].hand;
|
||||||
|
const [moved] = hand.splice(ds.cardIdx, 1);
|
||||||
|
hand.splice(finalIdx, 0, moved);
|
||||||
|
|
||||||
|
this.dragState = null;
|
||||||
|
this.time.delayedCall(240, () => this.renderAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
endCardDragImmediate() {
|
||||||
|
if (!this.dragState) return;
|
||||||
|
const ds = this.dragState;
|
||||||
|
const card = this.localHandCards[ds.cardIdx];
|
||||||
|
this.tweens.killTweensOf(card);
|
||||||
|
this.tweens.killTweensOf(ds.slotIndicator);
|
||||||
|
|
||||||
|
const hand = this.gs.players[0].hand;
|
||||||
|
const [moved] = hand.splice(ds.cardIdx, 1);
|
||||||
|
hand.splice(ds.insertIdx, 0, moved);
|
||||||
|
|
||||||
|
this.selectedHandIdx = null;
|
||||||
|
this.dragState = null;
|
||||||
|
this.potentialDrag = null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Match lifecycle ──────────────────────────────────────────────────────
|
// ── Match lifecycle ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
startNewMatch() {
|
startNewMatch() {
|
||||||
if (this.animating) return;
|
if (this.animating) return;
|
||||||
this.matchOver = false;
|
this.matchOver = false;
|
||||||
|
if (this.dragState) this.endCardDragImmediate();
|
||||||
this.clearAllCardObjs();
|
this.clearAllCardObjs();
|
||||||
this.clearHighlights();
|
this.clearHighlights();
|
||||||
this.selectedHandIdx = null;
|
this.selectedHandIdx = null;
|
||||||
|
|
@ -541,11 +707,13 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
this.cardObjs.clear();
|
this.cardObjs.clear();
|
||||||
for (const o of this.transientObjs) o.destroy();
|
for (const o of this.transientObjs) o.destroy();
|
||||||
this.transientObjs = [];
|
this.transientObjs = [];
|
||||||
|
this.localHandCards = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Rendering ────────────────────────────────────────────────────────────
|
// ── Rendering ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
renderAll() {
|
renderAll() {
|
||||||
|
if (this.dragState) this.endCardDragImmediate();
|
||||||
this.clearAllCardObjs();
|
this.clearAllCardObjs();
|
||||||
this.renderCenter();
|
this.renderCenter();
|
||||||
for (let seat = 0; seat < this.gs.players.length; seat++) {
|
for (let seat = 0; seat < this.gs.players.length; seat++) {
|
||||||
|
|
@ -578,6 +746,8 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
const slot = this.slotForSeat[seat];
|
const slot = this.slotForSeat[seat];
|
||||||
const layout = slotLayout(slot);
|
const layout = slotLayout(slot);
|
||||||
|
|
||||||
|
if (seat === 0) this.localHandCards = [];
|
||||||
|
|
||||||
// Hand
|
// Hand
|
||||||
for (let i = 0; i < player.hand.length; i++) {
|
for (let i = 0; i < player.hand.length; i++) {
|
||||||
const card = player.hand[i];
|
const card = player.hand[i];
|
||||||
|
|
@ -594,9 +764,10 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
this.cardObjs.set(`hand-${seat}-${card.id}`, c);
|
this.cardObjs.set(`hand-${seat}-${card.id}`, c);
|
||||||
if (seat === 0) {
|
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);
|
c.setInteractive(new Phaser.Geom.Rectangle(-CARD_W/2, -CARD_H/2, CARD_W, CARD_H), Phaser.Geom.Rectangle.Contains);
|
||||||
c.input.cursor = 'pointer';
|
c.input.cursor = 'pointer';
|
||||||
c.on('pointerdown', () => this.onHandClick(i));
|
c.on('pointerdown', (pointer) => this.onHandPointerDown(i, pointer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue