feat(hearts): enhance card sorting, drag interaction, and pass animations
- Sort player's hand by suit and value for consistent layout - Implement drag-to-sort functionality for the local hand with visual feedback (slot indicators, card displacement, play zone highlighting) - Refactor pass confirmation to include multi-phase animation: outgoing cards fly to target, incoming cards animate in from source, flip to reveal, and settle into sorted positions - Add background to hearts broken indicator for better visibility - Update card interaction handlers to support both click and drag events
This commit is contained in:
parent
3cea4f10b6
commit
4b0551c3cd
Binary file not shown.
Binary file not shown.
|
|
@ -24,6 +24,16 @@
|
||||||
"file": "track05.mp3",
|
"file": "track05.mp3",
|
||||||
"artist": "Guns 'n Roses",
|
"artist": "Guns 'n Roses",
|
||||||
"title": "Thunderbird"
|
"title": "Thunderbird"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "track06.mp3",
|
||||||
|
"artist": "Simon Stalenhag",
|
||||||
|
"title": "Behind the Reactor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "track07.mp3",
|
||||||
|
"artist": "Jean Michel Jarre",
|
||||||
|
"title": "Oblivion"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
playCard,
|
playCard,
|
||||||
legalPlays,
|
legalPlays,
|
||||||
startNextHand,
|
startNextHand,
|
||||||
|
passTargetSeat,
|
||||||
PASS_COUNT,
|
PASS_COUNT,
|
||||||
} from './HeartsLogic.js';
|
} from './HeartsLogic.js';
|
||||||
import { choosePass, choosePlay } from './HeartsAI.js';
|
import { choosePass, choosePlay } from './HeartsAI.js';
|
||||||
|
|
@ -84,6 +85,10 @@ export default class HeartsGame extends Phaser.Scene {
|
||||||
this.passButton = null;
|
this.passButton = null;
|
||||||
this.scoreTexts = [];
|
this.scoreTexts = [];
|
||||||
this.turnGlow = null;
|
this.turnGlow = null;
|
||||||
|
|
||||||
|
this.localHandCards = []; // player 0 card containers in hand order
|
||||||
|
this.dragState = null;
|
||||||
|
this.potentialDrag = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -92,6 +97,7 @@ export default class HeartsGame extends Phaser.Scene {
|
||||||
this.buildSeats();
|
this.buildSeats();
|
||||||
this.buildCenter();
|
this.buildCenter();
|
||||||
this.buildHUD();
|
this.buildHUD();
|
||||||
|
this.setupDragHandlers();
|
||||||
this.startNewMatch();
|
this.startNewMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,7 +135,8 @@ export default class HeartsGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCenter() {
|
buildCenter() {
|
||||||
this.heartsText = this.add.text(CX, CY - 170, '', {
|
this.heartsBg = this.add.graphics().setDepth(D.ui - 1);
|
||||||
|
this.heartsText = this.add.text(CX, CY - 195, '', {
|
||||||
fontFamily: 'Righteous', fontSize: '18px', color: COLORS.dangerHex,
|
fontFamily: 'Righteous', fontSize: '18px', color: COLORS.dangerHex,
|
||||||
}).setOrigin(0.5).setDepth(D.ui);
|
}).setOrigin(0.5).setDepth(D.ui);
|
||||||
|
|
||||||
|
|
@ -181,6 +188,11 @@ export default class HeartsGame extends Phaser.Scene {
|
||||||
this.updateHeartsIndicator();
|
this.updateHeartsIndicator();
|
||||||
playSound(this, SFX.CARD_SHUFFLE);
|
playSound(this, SFX.CARD_SHUFFLE);
|
||||||
await this.dealAnimation();
|
await this.dealAnimation();
|
||||||
|
const SUIT_ORDER = { c: 0, d: 1, s: 2, h: 3 };
|
||||||
|
this.gs.players[0].hand.sort((a, b) => {
|
||||||
|
const sd = SUIT_ORDER[a.suit] - SUIT_ORDER[b.suit];
|
||||||
|
return sd !== 0 ? sd : a.value - b.value;
|
||||||
|
});
|
||||||
this.renderHands();
|
this.renderHands();
|
||||||
this.updateScores();
|
this.updateScores();
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
|
|
@ -222,18 +234,111 @@ export default class HeartsGame extends Phaser.Scene {
|
||||||
|
|
||||||
async confirmPass() {
|
async confirmPass() {
|
||||||
if (this.passSelection.size !== PASS_COUNT) return;
|
if (this.passSelection.size !== PASS_COUNT) return;
|
||||||
const ids = [...this.passSelection];
|
const outgoingIds = [...this.passSelection];
|
||||||
this.passButton?.destroy();
|
this.passButton?.destroy();
|
||||||
this.passButton = null;
|
this.passButton = null;
|
||||||
this.passSelection.clear();
|
this.passSelection.clear();
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
|
|
||||||
const dir = DIRECTION_LABEL[this.gs.passDirection];
|
const dir = this.gs.passDirection;
|
||||||
this.gs = selectPass(this.gs, 0, ids); // all four are ready → pass resolves
|
const targetSeat = passTargetSeat(0, dir);
|
||||||
this.showBanner(`Cards passed ${dir}.`);
|
|
||||||
|
// Find which opponent passes TO player 0.
|
||||||
|
let sourceSeat = 1;
|
||||||
|
for (let s = 1; s < 4; s++) {
|
||||||
|
if (passTargetSeat(s, dir) === 0) { sourceSeat = s; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture incoming card data before state is applied.
|
||||||
|
const incomingIds = new Set(this.gs.pendingPass[sourceSeat]);
|
||||||
|
const incomingCards = this.gs.players[sourceSeat].hand.filter((c) => incomingIds.has(c.id));
|
||||||
|
|
||||||
|
const targetLay = slotLayout(SLOTS[targetSeat]);
|
||||||
|
const sourceLay = slotLayout(SLOTS[sourceSeat]);
|
||||||
|
|
||||||
|
// ── Phase 1: Player's cards fly to the target opponent ───────────────────
|
||||||
|
const outgoingSprites = outgoingIds
|
||||||
|
.map((id) => this.handCardObjs.get(`0-${id}`))
|
||||||
|
.filter(Boolean);
|
||||||
|
for (const s of outgoingSprites) s.setDepth(D.ui + 5);
|
||||||
|
playSound(this, SFX.CARD_DEAL);
|
||||||
|
await Promise.all(outgoingSprites.map((s) =>
|
||||||
|
this.tweenTo(s, targetLay.portrait.x, targetLay.portrait.y, 380)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Apply state and sort the final hand.
|
||||||
|
this.gs = selectPass(this.gs, 0, outgoingIds);
|
||||||
|
const SUIT_ORDER = { c: 0, d: 1, s: 2, h: 3 };
|
||||||
|
this.gs.players[0].hand.sort((a, b) => {
|
||||||
|
const sd = SUIT_ORDER[a.suit] - SUIT_ORDER[b.suit];
|
||||||
|
return sd !== 0 ? sd : a.value - b.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const s of outgoingSprites) s.destroy();
|
||||||
this.renderHands();
|
this.renderHands();
|
||||||
await this.delay(900);
|
// Keep incoming card positions reserved but invisible until they animate in.
|
||||||
this.hideBanner();
|
for (const card of incomingCards) {
|
||||||
|
this.handCardObjs.get(`0-${card.id}`)?.setAlpha(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Phase 2: Face-down cards fly in from source opponent ─────────────────
|
||||||
|
const stagingY = slotLayout('bottom').handCenter.y - 180;
|
||||||
|
const stagingSprites = incomingCards.map((card) => {
|
||||||
|
const s = this.makeCardSprite(card, sourceLay.portrait.x, sourceLay.portrait.y, { faceUp: false });
|
||||||
|
s.setDepth(D.ui + 5);
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
|
||||||
|
playSound(this, SFX.CARD_DEAL);
|
||||||
|
await Promise.all(stagingSprites.map((s, i) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: s,
|
||||||
|
x: CX + (i - 1) * (HAND_SPREAD + 12),
|
||||||
|
y: stagingY,
|
||||||
|
delay: i * 90,
|
||||||
|
duration: 380,
|
||||||
|
ease: 'Cubic.easeOut',
|
||||||
|
onComplete: resolve,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
// ── Phase 3: Flip each card to reveal face-up ─────────────────────────────
|
||||||
|
for (let i = 0; i < stagingSprites.length; i++) {
|
||||||
|
const sprite = stagingSprites[i];
|
||||||
|
const card = incomingCards[i];
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: sprite, scaleX: 0, duration: 140, ease: 'Linear',
|
||||||
|
onComplete: () => {
|
||||||
|
this.renderCardFace(sprite, card, true);
|
||||||
|
this.tweens.add({ targets: sprite, scaleX: 1, duration: 140, ease: 'Linear', onComplete: resolve });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
playSound(this, SFX.CARD_PLACE);
|
||||||
|
if (i < stagingSprites.length - 1) await this.delay(70);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.delay(420);
|
||||||
|
|
||||||
|
// ── Phase 4: Cards slide into their sorted positions in the hand ──────────
|
||||||
|
await Promise.all(stagingSprites.map((sprite, i) => {
|
||||||
|
const target = this.handCardObjs.get(`0-${incomingCards[i].id}`);
|
||||||
|
if (!target) { sprite.destroy(); return Promise.resolve(); }
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: sprite, x: target.x, y: target.y, duration: 320, ease: 'Cubic.easeOut',
|
||||||
|
onComplete: () => {
|
||||||
|
target.setAlpha(1);
|
||||||
|
sprite.destroy();
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
await this.advance();
|
await this.advance();
|
||||||
}
|
}
|
||||||
|
|
@ -423,6 +528,7 @@ export default class HeartsGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSeatHand(seat) {
|
renderSeatHand(seat) {
|
||||||
|
if (seat === 0) this.localHandCards = [];
|
||||||
const player = this.gs.players[seat];
|
const player = this.gs.players[seat];
|
||||||
const lay = slotLayout(SLOTS[seat]);
|
const lay = slotLayout(SLOTS[seat]);
|
||||||
const n = player.hand.length;
|
const n = player.hand.length;
|
||||||
|
|
@ -447,13 +553,21 @@ export default class HeartsGame extends Phaser.Scene {
|
||||||
const interactive = selecting || (playable && this.legalIds.has(card.id));
|
const interactive = selecting || (playable && this.legalIds.has(card.id));
|
||||||
const dimmed = playable && !this.legalIds.has(card.id);
|
const dimmed = playable && !this.legalIds.has(card.id);
|
||||||
if (dimmed) sprite.setAlpha(0.45);
|
if (dimmed) sprite.setAlpha(0.45);
|
||||||
if (interactive) {
|
|
||||||
sprite.setInteractive(new Phaser.Geom.Rectangle(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H), Phaser.Geom.Rectangle.Contains);
|
sprite.setInteractive(new Phaser.Geom.Rectangle(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H), Phaser.Geom.Rectangle.Contains);
|
||||||
sprite.input.cursor = 'pointer';
|
sprite.input.cursor = interactive ? 'pointer' : 'default';
|
||||||
sprite.on('pointerover', () => { if (!this.busy) { sprite.y -= 10; } });
|
sprite.on('pointerover', () => { if (!this.busy && !this.dragState) sprite.y -= 10; });
|
||||||
sprite.on('pointerout', () => { if (!this.busy) { sprite.y += 10; } });
|
sprite.on('pointerout', () => { if (!this.busy && !this.dragState) sprite.y += 10; });
|
||||||
sprite.on('pointerdown', () => this.onCardClick(card.id));
|
sprite.on('pointerdown', (pointer) => {
|
||||||
}
|
if (this.busy || this.dragState) return;
|
||||||
|
this.potentialDrag = {
|
||||||
|
handIdx: i, cardId: card.id, isInteractive: interactive,
|
||||||
|
startX: pointer.x, startY: pointer.y,
|
||||||
|
offsetX: pointer.x - sprite.x, offsetY: pointer.y - sprite.y,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.localHandCards.push(sprite);
|
||||||
if (selected) this.ringCard(x, y);
|
if (selected) this.ringCard(x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -516,7 +630,15 @@ export default class HeartsGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHeartsIndicator() {
|
updateHeartsIndicator() {
|
||||||
this.heartsText?.setText(this.gs.heartsBroken ? '♥ Hearts broken' : '');
|
const broken = this.gs.heartsBroken;
|
||||||
|
this.heartsText?.setText(broken ? '♥ Hearts broken' : '');
|
||||||
|
this.heartsBg?.clear();
|
||||||
|
if (broken) {
|
||||||
|
const t = this.heartsText;
|
||||||
|
const pad = 8;
|
||||||
|
this.heartsBg.fillStyle(0x000000, 0.55);
|
||||||
|
this.heartsBg.fillRoundedRect(t.x - t.width / 2 - pad, t.y - t.height / 2 - pad, t.width + pad * 2, t.height + pad * 2, 6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTurnIndicator() {
|
updateTurnIndicator() {
|
||||||
|
|
@ -592,6 +714,9 @@ export default class HeartsGame extends Phaser.Scene {
|
||||||
// ── Cleanup helpers ─────────────────────────────────────────────────────────────
|
// ── Cleanup helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
clearHandObjs() {
|
clearHandObjs() {
|
||||||
|
this.dragState = null;
|
||||||
|
this.potentialDrag = null;
|
||||||
|
this.localHandCards = [];
|
||||||
for (const c of this.handCardObjs.values()) c.destroy();
|
for (const c of this.handCardObjs.values()) c.destroy();
|
||||||
this.handCardObjs.clear();
|
this.handCardObjs.clear();
|
||||||
}
|
}
|
||||||
|
|
@ -606,6 +731,204 @@ export default class HeartsGame extends Phaser.Scene {
|
||||||
this.trickSprites.clear();
|
this.trickSprites.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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, isInteractive } = this.potentialDrag;
|
||||||
|
this.potentialDrag = null;
|
||||||
|
if (isInteractive) this.onCardClick(cardId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.dragState) this.endCardDrag();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startCardDrag(handIdx, offsetX, offsetY) {
|
||||||
|
const card = this.localHandCards[handIdx];
|
||||||
|
if (!card) return;
|
||||||
|
const hand = this.gs.players[0].hand;
|
||||||
|
const n = hand.length;
|
||||||
|
const lay = slotLayout('bottom');
|
||||||
|
const handStartX = lay.handCenter.x - (n - 1) / 2 * HAND_SPREAD;
|
||||||
|
const handCenterY = lay.handCenter.y;
|
||||||
|
const insertIdx = Phaser.Math.Clamp(Math.round((card.x - handStartX) / HAND_SPREAD), 0, n - 1);
|
||||||
|
const isLegal = this.gs.phase === 'playing' && this.awaitingHuman && this.legalIds.has(hand[handIdx].id);
|
||||||
|
|
||||||
|
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.transient.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 * HAND_SPREAD, handCenterY);
|
||||||
|
slotIndicator.setDepth(D.card - 1);
|
||||||
|
this.transient.push(slotIndicator);
|
||||||
|
|
||||||
|
let playZoneHighlight = null;
|
||||||
|
if (isLegal) {
|
||||||
|
const pt = slotLayout('bottom').trick;
|
||||||
|
playZoneHighlight = this.add.graphics();
|
||||||
|
playZoneHighlight.lineStyle(4, COLORS.accent, 0.9);
|
||||||
|
playZoneHighlight.strokeRoundedRect(-CARD_W / 2 - 6, -CARD_H / 2 - 6, CARD_W + 12, CARD_H + 12, 12);
|
||||||
|
playZoneHighlight.fillStyle(COLORS.accent, 0.18);
|
||||||
|
playZoneHighlight.fillRoundedRect(-CARD_W / 2 - 6, -CARD_H / 2 - 6, CARD_W + 12, CARD_H + 12, 12);
|
||||||
|
playZoneHighlight.setPosition(pt.x, pt.y);
|
||||||
|
playZoneHighlight.setDepth(D.card + 1);
|
||||||
|
playZoneHighlight.setAlpha(0);
|
||||||
|
this.transient.push(playZoneHighlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragState = { cardIdx: handIdx, offsetX, offsetY, shadow, slotIndicator, insertIdx, handStartX, handCenterY, isLegal, playZoneHighlight, overPlayZone: false };
|
||||||
|
|
||||||
|
card.setDepth(D.card + 10);
|
||||||
|
this.tweens.add({ targets: card, scaleX: 1.08, scaleY: 1.08, duration: 120, ease: 'Cubic.easeOut' });
|
||||||
|
}
|
||||||
|
|
||||||
|
_isPlayZone(x, y) {
|
||||||
|
const pt = slotLayout('bottom').trick;
|
||||||
|
return Math.abs(x - pt.x) < CARD_W * 1.5 && Math.abs(y - pt.y) < CARD_H * 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 overPlayZone = ds.isLegal && this._isPlayZone(card.x, card.y);
|
||||||
|
if (overPlayZone !== ds.overPlayZone) {
|
||||||
|
ds.overPlayZone = overPlayZone;
|
||||||
|
if (overPlayZone) {
|
||||||
|
this.tweens.killTweensOf(ds.slotIndicator);
|
||||||
|
ds.slotIndicator.setAlpha(0);
|
||||||
|
this._settleNonDraggedCards();
|
||||||
|
this.tweens.add({ targets: ds.playZoneHighlight, alpha: 1, duration: 150 });
|
||||||
|
} else {
|
||||||
|
ds.slotIndicator.setAlpha(1);
|
||||||
|
this._updateNonDraggedCards();
|
||||||
|
this.tweens.add({ targets: ds.playZoneHighlight, alpha: 0, duration: 150 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!overPlayZone) {
|
||||||
|
const newInsertIdx = Phaser.Math.Clamp(Math.round((card.x - ds.handStartX) / HAND_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 * 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 = ds.handStartX + 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_settleNonDraggedCards() {
|
||||||
|
const ds = this.dragState;
|
||||||
|
const hand = this.gs.players[0].hand;
|
||||||
|
const n = hand.length;
|
||||||
|
const remaining = n - 1;
|
||||||
|
const settleStartX = slotLayout('bottom').handCenter.x - (remaining - 1) / 2 * HAND_SPREAD;
|
||||||
|
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: settleStartX + k * HAND_SPREAD, rotation: 0, scaleX: 1, scaleY: 1, duration: 150, ease: 'Cubic.easeOut' });
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endCardDrag() {
|
||||||
|
const ds = this.dragState;
|
||||||
|
const card = this.localHandCards[ds.cardIdx];
|
||||||
|
const hand = this.gs.players[0].hand;
|
||||||
|
const n = hand.length;
|
||||||
|
|
||||||
|
// ── Play path: dropped on the play zone ───────────────────────────────────
|
||||||
|
if (ds.overPlayZone) {
|
||||||
|
const handCard = hand[ds.cardIdx];
|
||||||
|
this.tweens.killTweensOf(card);
|
||||||
|
card.setScale(1);
|
||||||
|
card.setRotation(0);
|
||||||
|
this.dragState = null;
|
||||||
|
this.playHuman(handCard.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Reorder path: dropped back in hand ────────────────────────────────────
|
||||||
|
const movedCardId = hand[ds.cardIdx].id;
|
||||||
|
const finalIdx = Phaser.Math.Clamp(Math.round((card.x - ds.handStartX) / HAND_SPREAD), 0, n - 1);
|
||||||
|
const isSelected = this.gs.phase === 'passing' && this.passSelection.has(movedCardId);
|
||||||
|
const finalY = ds.handCenterY - (isSelected ? 26 : 0);
|
||||||
|
|
||||||
|
this.tweens.killTweensOf(card);
|
||||||
|
this.tweens.add({ targets: card, x: ds.handStartX + finalIdx * HAND_SPREAD, y: finalY, 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 otherIsSelected = this.gs.phase === 'passing' && this.passSelection.has(hand[j].id);
|
||||||
|
const cardObj = this.localHandCards[j];
|
||||||
|
this.tweens.killTweensOf(cardObj);
|
||||||
|
this.tweens.add({ targets: cardObj, x: ds.handStartX + finalPos * HAND_SPREAD, y: ds.handCenterY - (otherIsSelected ? 26 : 0), rotation: 0, scaleX: 1, scaleY: 1, duration: 120, ease: 'Cubic.easeOut' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [moved] = hand.splice(ds.cardIdx, 1);
|
||||||
|
hand.splice(finalIdx, 0, moved);
|
||||||
|
const [movedCard] = this.localHandCards.splice(ds.cardIdx, 1);
|
||||||
|
this.localHandCards.splice(finalIdx, 0, movedCard);
|
||||||
|
|
||||||
|
this.dragState = null;
|
||||||
|
this.time.delayedCall(240, () => { if (!this.dragState) this.renderHands(); });
|
||||||
|
}
|
||||||
|
|
||||||
tweenTo(target, x, y, duration) {
|
tweenTo(target, x, y, duration) {
|
||||||
return new Promise((resolve) => this.tweens.add({ targets: target, x, y, duration, ease: 'Cubic.easeOut', onComplete: resolve }));
|
return new Promise((resolve) => this.tweens.add({ targets: target, x, y, duration, ease: 'Cubic.easeOut', onComplete: resolve }));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue