feat(dominion): add hand drag-and-drop and AI opponent animations
- Implement drag-and-drop reordering for the player's hand with a visual play drop zone. - Add smooth, multi-phase animations for AI opponents (play, draw, cleanup, gain). - Refactor hand rendering to preserve manual order and improve playability highlighting. - Update card draw animations to target existing sprite coordinates directly.
This commit is contained in:
parent
7e3c7d8e3d
commit
97d748b2f5
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 4.9 MiB |
|
|
@ -24,6 +24,7 @@ const PLAY_W = 78, PLAY_H = 112;
|
|||
|
||||
const DECK_PILE_X = 240, DECK_PILE_Y = 968;
|
||||
const DISCARD_PILE_X = 1704, DISCARD_PILE_Y = 968; // right edge aligns with opponent portrait center (x=1770)
|
||||
const OPP_W = 36, OPP_H = 52;
|
||||
|
||||
const AI_STEP_MS = 420;
|
||||
const AI_PENDING_MS = 520;
|
||||
|
|
@ -70,6 +71,14 @@ export default class DominionGame extends Phaser.Scene {
|
|||
this._pendingAnimState = null;
|
||||
this._animatingIids = new Set();
|
||||
this.inPlaySprites = [];
|
||||
this._dragState = null;
|
||||
this._dragPotential = null;
|
||||
this._dragJustEnded = false;
|
||||
this._dragDropZone = null;
|
||||
this._dragDropLabel = null;
|
||||
this._handOrder = null;
|
||||
this.oppHandSprites = {};
|
||||
this.oppInPlaySprites = {};
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -83,7 +92,14 @@ export default class DominionGame extends Phaser.Scene {
|
|||
this.input.on('pointermove', (p) => {
|
||||
this.lastPointer = { x: p.x, y: p.y };
|
||||
if (this.hoverVisible) this.positionHover(p.x, p.y);
|
||||
if (this._dragState) this._onDragMove(p);
|
||||
else this._checkDragStart(p);
|
||||
});
|
||||
this.input.on('pointerup', (p) => {
|
||||
this._dragPotential = null;
|
||||
if (this._dragState) this._onDragUp(p);
|
||||
});
|
||||
this.input.on('gameout', () => this._cancelDrag());
|
||||
|
||||
this.events.once('shutdown', () => {
|
||||
this.portraits.forEach((pt) => pt?.destroy?.());
|
||||
|
|
@ -174,6 +190,8 @@ export default class DominionGame extends Phaser.Scene {
|
|||
this.handSprites = [];
|
||||
this.supplySprites = [];
|
||||
this.inPlaySprites = [];
|
||||
this.oppHandSprites = {};
|
||||
this.oppInPlaySprites = {};
|
||||
|
||||
this.renderSupply();
|
||||
this.renderInPlay();
|
||||
|
|
@ -271,6 +289,10 @@ export default class DominionGame extends Phaser.Scene {
|
|||
this.dynamicLayer.add(hit);
|
||||
this.attachHover(hit, def);
|
||||
this.inPlaySprites.push({ iid: c.iid, id: c.id, x: startX + i * (PLAY_W + gap), y: 610, face });
|
||||
if (gs.turn !== 0) {
|
||||
if (!this.oppInPlaySprites[gs.turn]) this.oppInPlaySprites[gs.turn] = [];
|
||||
this.oppInPlaySprites[gs.turn].push({ iid: c.iid, id: c.id, x: startX + i * (PLAY_W + gap), y: 610, face });
|
||||
}
|
||||
});
|
||||
const lbl = this.add.text(CX, 540, `${this.seatName(gs.turn)} — in play`, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex,
|
||||
|
|
@ -280,7 +302,9 @@ export default class DominionGame extends Phaser.Scene {
|
|||
|
||||
renderHand() {
|
||||
const gs = this.gs;
|
||||
const hand = gs.players[0].hand;
|
||||
const rawHand = gs.players[0].hand;
|
||||
this._reconcileHandOrder(rawHand);
|
||||
const hand = this._getOrderedHand(rawHand);
|
||||
const gap = Math.min(18, (1300 - hand.length * HAND_W) / Math.max(1, hand.length - 1));
|
||||
const step = HAND_W + Math.max(-HAND_W * 0.45, gap);
|
||||
const totalW = (hand.length - 1) * step + HAND_W;
|
||||
|
|
@ -294,24 +318,34 @@ export default class DominionGame extends Phaser.Scene {
|
|||
hand.forEach((c, i) => {
|
||||
const def = getCard(c.id);
|
||||
const x = startX + i * step;
|
||||
const isPlayableAction = canPlayAction && legalAct.has(c.iid);
|
||||
const isPlayableTreasure = canPlayTreasure && isType(c.id, 'treasure');
|
||||
const face = this.buildCardFace(HAND_W, HAND_H, def);
|
||||
face.setPosition(x, baseY).setDepth(D.hand + i);
|
||||
if (this._animatingIids.has(c.iid)) face.setAlpha(0);
|
||||
else if (this._dragState?.iid === c.iid) face.setAlpha(0.2);
|
||||
this.dynamicLayer.add(face);
|
||||
const hit = this.add.rectangle(x, baseY, HAND_W, HAND_H, 0x000000, 0).setDepth(D.hand + i + 1);
|
||||
this.dynamicLayer.add(hit);
|
||||
this.attachHover(hit, def);
|
||||
this.handSprites.push({ iid: c.iid, id: c.id, def, x, baseY, face, hit });
|
||||
hit.setInteractive({ useHandCursor: true });
|
||||
const hs = { iid: c.iid, id: c.id, def, x, baseY, face, hit, isPlayableAction, isPlayableTreasure };
|
||||
this.handSprites.push(hs);
|
||||
|
||||
if (canPlayAction && legalAct.has(c.iid)) {
|
||||
hit.setInteractive({ useHandCursor: true });
|
||||
hit.on('pointerup', () => this.humanPlayAction(c.iid));
|
||||
this.highlightFace(face, COLORS.accent);
|
||||
} else if (canPlayTreasure && isType(c.id, 'treasure')) {
|
||||
hit.setInteractive({ useHandCursor: true });
|
||||
hit.on('pointerup', () => this.humanPlayTreasure(c.iid));
|
||||
this.highlightFace(face, COLORS.gold);
|
||||
}
|
||||
if (isPlayableAction) this.highlightFace(face, COLORS.accent);
|
||||
else if (isPlayableTreasure) this.highlightFace(face, COLORS.gold);
|
||||
|
||||
hit.on('pointerdown', (ptr) => {
|
||||
if (this._animating) return;
|
||||
this._dragPotential = { hs, startX: ptr.x, startY: ptr.y };
|
||||
if (this.hoverTimer) { this.hoverTimer.remove(); this.hoverTimer = null; }
|
||||
this.hideHover();
|
||||
});
|
||||
hit.on('pointerup', () => {
|
||||
if (this._dragJustEnded) return;
|
||||
if (isPlayableAction) this.humanPlayAction(c.iid);
|
||||
else if (isPlayableTreasure) this.humanPlayTreasure(c.iid);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -370,8 +404,8 @@ export default class DominionGame extends Phaser.Scene {
|
|||
}).setOrigin(0.5).setDepth(D.hud));
|
||||
|
||||
// Mini face-down cards representing the opponent's hand
|
||||
const OPP_W = 36, OPP_H = 52;
|
||||
const handSize = p.hand.length;
|
||||
this.oppHandSprites[seat] = [];
|
||||
if (handSize > 0) {
|
||||
const gap = Math.min(4, (200 - handSize * OPP_W) / Math.max(1, handSize - 1));
|
||||
const step = OPP_W + Math.max(-OPP_W * 0.6, gap);
|
||||
|
|
@ -379,9 +413,11 @@ export default class DominionGame extends Phaser.Scene {
|
|||
const startX = s.x - totalW / 2 + OPP_W / 2;
|
||||
const cardY = s.y + s.r + 75;
|
||||
for (let j = 0; j < handSize; j++) {
|
||||
const c = p.hand[j];
|
||||
const mini = this.buildCardFace(OPP_W, OPP_H, null, { faceDown: true });
|
||||
mini.setPosition(startX + j * step, cardY).setDepth(D.hud + j);
|
||||
this.dynamicLayer.add(mini);
|
||||
this.oppHandSprites[seat].push({ iid: c.iid, id: c.id, x: startX + j * step, y: cardY, face: mini });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -672,6 +708,43 @@ export default class DominionGame extends Phaser.Scene {
|
|||
return;
|
||||
}
|
||||
|
||||
// AI opponent animations — detect changes for each non-human seat
|
||||
for (let seat = 1; seat < this.playerCount; seat++) {
|
||||
const seatLog = newLog.filter(e => e.seat === seat);
|
||||
const turnEndEvt = seatLog.find(e => e.kind === 'turnEnd');
|
||||
const playEvts = seatLog.filter(e => e.kind === 'play' || e.kind === 'playTreasure');
|
||||
const drawEvt = seatLog.find(e => e.kind === 'draw');
|
||||
const gainEvt = seatLog.find(e => e.kind === 'gain');
|
||||
|
||||
if (turnEndEvt) {
|
||||
const prevP = prev.players[seat];
|
||||
const cleanup = [...prevP.inPlay, ...prevP.hand];
|
||||
this._animOppCleanup(seat, cleanup, s.players[seat].hand, s);
|
||||
return;
|
||||
}
|
||||
if (playEvts.length > 0) {
|
||||
const prevHand = prev.players[seat].hand;
|
||||
const newInPlay = s.players[seat].inPlay;
|
||||
const played = prevHand.filter(c => newInPlay.find(ip => ip.iid === c.iid));
|
||||
const drawn = s.players[seat].hand.filter(c => !prevHand.find(h => h.iid === c.iid));
|
||||
const humanGainEvt = newLog.find(e => e.kind === 'gain' && e.seat === 0 && e.dest !== 'hand');
|
||||
if (played.length > 0) {
|
||||
this._animOppPlayCards(seat, played, drawn, s, humanGainEvt);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (drawEvt) {
|
||||
const prevHand = prev.players[seat].hand;
|
||||
const drawn = s.players[seat].hand.filter(c => !prevHand.find(h => h.iid === c.iid));
|
||||
if (drawn.length > 0) { this._animOppDraw(seat, drawn.length, s); return; }
|
||||
}
|
||||
if (gainEvt) {
|
||||
const sp = this.supplySprites.find(sp => sp.id === gainEvt.id);
|
||||
this._animOppGain(seat, gainEvt.id, sp?.x ?? CX, sp?.y ?? 300, s);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.gs = s;
|
||||
this.clearPrompt();
|
||||
this.render();
|
||||
|
|
@ -1065,6 +1138,198 @@ export default class DominionGame extends Phaser.Scene {
|
|||
} catch (_) { /* offline / not signed in */ }
|
||||
}
|
||||
|
||||
// ── AI opponent animations ────────────────────────────────────────────────────
|
||||
|
||||
_animOppPlayCards(seat, playedCards, drawnCards, newState, humanGainEvt = null) {
|
||||
this._animating = true;
|
||||
const slot = this.oppSlot(seat - 1);
|
||||
|
||||
// Capture source positions before render destroys sprite refs
|
||||
const sources = playedCards.map(card => {
|
||||
const hs = (this.oppHandSprites[seat] ?? []).find(s => s.iid === card.iid);
|
||||
return { iid: card.iid, id: card.id, x: hs?.x ?? slot.x, y: hs?.y ?? (slot.y + slot.r + 75) };
|
||||
});
|
||||
|
||||
this.gs = newState;
|
||||
this.clearPrompt();
|
||||
this.render();
|
||||
|
||||
// Hide newly-played in-play sprites — ghost will reveal them on arrival
|
||||
for (const card of playedCards) {
|
||||
const sp = (this.oppInPlaySprites[seat] ?? []).find(s => s.iid === card.iid);
|
||||
if (sp) sp.face.setAlpha(0);
|
||||
}
|
||||
|
||||
// Target positions in the in-play area
|
||||
const newInPlay = newState.players[seat].inPlay;
|
||||
const ipGap = 8;
|
||||
const ipTotal = Math.min(newInPlay.length, 12) * (PLAY_W + ipGap);
|
||||
const ipStartX = CX - ipTotal / 2 + PLAY_W / 2;
|
||||
const targets = playedCards.map(card => {
|
||||
const idx = newInPlay.findIndex(c => c.iid === card.iid);
|
||||
return { x: ipStartX + Math.max(0, idx) * (PLAY_W + ipGap), y: 610 };
|
||||
});
|
||||
|
||||
let idx = 0;
|
||||
const next = () => {
|
||||
if (idx >= sources.length) {
|
||||
if (drawnCards.length > 0) { this._animOppDraw(seat, drawnCards.length, newState, humanGainEvt); return; }
|
||||
if (humanGainEvt) { this._chainHumanGain(humanGainEvt, newState); return; }
|
||||
this._animating = false;
|
||||
if (this._pendingAnimState) { const s = this._pendingAnimState; this._pendingAnimState = null; this.setState(s); }
|
||||
else this.scheduleAdvance(10);
|
||||
return;
|
||||
}
|
||||
const src = sources[idx];
|
||||
const tgt = targets[idx++];
|
||||
const sp = (this.oppInPlaySprites[seat] ?? []).find(s => s.iid === src.iid);
|
||||
this._animOppOnePlay(src, tgt.x, tgt.y, getCard(src.id), () => {
|
||||
if (sp) sp.face.setAlpha(1);
|
||||
next();
|
||||
});
|
||||
};
|
||||
next();
|
||||
}
|
||||
|
||||
_animOppOnePlay(src, tx, ty, def, onComplete) {
|
||||
// Phase 1 (150ms): face-down mini unfolds from scale 0
|
||||
const fd = this.buildCardFace(OPP_W, OPP_H, null, { faceDown: true });
|
||||
fd.setPosition(src.x, src.y).setScale(0, 1);
|
||||
this.animLayer.add(fd);
|
||||
this.tweens.add({
|
||||
targets: fd, scaleX: 1, duration: 150, ease: 'Sine.easeOut',
|
||||
onComplete: () => {
|
||||
// Phase 2 (150ms): fold back — flip illusion
|
||||
this.tweens.add({
|
||||
targets: fd, scaleX: 0, duration: 150, ease: 'Sine.easeIn',
|
||||
onComplete: () => {
|
||||
fd.destroy();
|
||||
// Phase 3 (350ms): face-up, grow from mini scale to full, fly to target
|
||||
const fu = this.buildCardFace(PLAY_W, PLAY_H, def);
|
||||
fu.setPosition(src.x, src.y).setScale(OPP_W / PLAY_W, OPP_H / PLAY_H);
|
||||
this.animLayer.add(fu);
|
||||
this.tweens.add({
|
||||
targets: fu, x: tx, y: ty, scaleX: 1, scaleY: 1,
|
||||
duration: 350, ease: 'Cubic.easeOut',
|
||||
onComplete: () => { fu.destroy(); onComplete(); },
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_animOppDraw(seat, count, newState, humanGainEvt = null) {
|
||||
if (this.gs !== newState) {
|
||||
this.gs = newState;
|
||||
this.clearPrompt();
|
||||
this.render();
|
||||
}
|
||||
const slot = this.oppSlot(seat - 1);
|
||||
const handSprites = this.oppHandSprites[seat] ?? [];
|
||||
// The last `count` sprites are the newly drawn ones
|
||||
const targets = handSprites.slice(-count);
|
||||
targets.forEach(sp => { if (sp?.face) sp.face.setAlpha(0); });
|
||||
|
||||
let idx = 0;
|
||||
const next = () => {
|
||||
if (idx >= count) {
|
||||
handSprites.forEach(sp => { if (sp?.face) sp.face.setAlpha(1); });
|
||||
if (humanGainEvt) { this._chainHumanGain(humanGainEvt, newState); return; }
|
||||
this._animating = false;
|
||||
if (this._pendingAnimState) { const s = this._pendingAnimState; this._pendingAnimState = null; this.setState(s); }
|
||||
else this.scheduleAdvance(10);
|
||||
return;
|
||||
}
|
||||
const tgt = targets[idx++];
|
||||
const tx = tgt?.x ?? slot.x, ty = tgt?.y ?? (slot.y + slot.r + 75);
|
||||
const mini = this.buildCardFace(OPP_W, OPP_H, null, { faceDown: true });
|
||||
mini.setPosition(slot.x, slot.y);
|
||||
this.animLayer.add(mini);
|
||||
this.tweens.add({
|
||||
targets: mini, x: tx, y: ty, duration: 320, ease: 'Cubic.easeOut',
|
||||
onComplete: () => {
|
||||
mini.destroy();
|
||||
if (tgt?.face) tgt.face.setAlpha(1);
|
||||
next();
|
||||
},
|
||||
});
|
||||
};
|
||||
next();
|
||||
}
|
||||
|
||||
_animOppCleanup(seat, cleanupCards, newCards, newState) {
|
||||
this._animating = true;
|
||||
const slot = this.oppSlot(seat - 1);
|
||||
|
||||
// Capture sources before any render wipes the sprite arrays
|
||||
const sources = cleanupCards.map(card => {
|
||||
const ip = (this.oppInPlaySprites[seat] ?? []).find(s => s.iid === card.iid);
|
||||
if (ip) return { iid: card.iid, id: card.id, x: ip.x, y: ip.y, face: ip.face };
|
||||
const hs = (this.oppHandSprites[seat] ?? []).find(s => s.iid === card.iid);
|
||||
if (hs) return { iid: card.iid, id: card.id, x: hs.x, y: hs.y, face: hs.face };
|
||||
return { iid: card.iid, id: card.id, x: slot.x, y: slot.y, face: null };
|
||||
});
|
||||
|
||||
this.clearPrompt();
|
||||
|
||||
let idx = 0;
|
||||
const nextCleanup = () => {
|
||||
if (idx >= sources.length) {
|
||||
this.gs = newState;
|
||||
this.render();
|
||||
if (newCards.length > 0) {
|
||||
this._animOppDraw(seat, newCards.length, newState);
|
||||
} else {
|
||||
this._animating = false;
|
||||
if (this._pendingAnimState) { const s = this._pendingAnimState; this._pendingAnimState = null; this.setState(s); }
|
||||
else this.scheduleAdvance(10);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const src = sources[idx++];
|
||||
if (src.face) src.face.setAlpha(0);
|
||||
const card = this.buildCardFace(OPP_W, OPP_H, null, { faceDown: true });
|
||||
card.setPosition(src.x, src.y);
|
||||
this.animLayer.add(card);
|
||||
this.tweens.add({
|
||||
targets: card, x: slot.x, y: slot.y, duration: 280, ease: 'Cubic.easeIn',
|
||||
onComplete: () => { card.destroy(); nextCleanup(); },
|
||||
});
|
||||
};
|
||||
nextCleanup();
|
||||
}
|
||||
|
||||
_animOppGain(seat, cardId, srcX, srcY, newState) {
|
||||
this._animating = true;
|
||||
this.gs = newState;
|
||||
this.clearPrompt();
|
||||
this.render();
|
||||
const slot = this.oppSlot(seat - 1);
|
||||
const src = { iid: -1, id: cardId, x: srcX, y: srcY };
|
||||
this._animDiscardCard(src, slot.x, slot.y, () => {
|
||||
this._animating = false;
|
||||
if (this._pendingAnimState) { const s = this._pendingAnimState; this._pendingAnimState = null; this.setState(s); }
|
||||
else this.scheduleAdvance(10);
|
||||
});
|
||||
}
|
||||
|
||||
_chainHumanGain(gainEvt, newState) {
|
||||
const prevDiscard = this.gs.players[0].discard;
|
||||
const newDiscard = newState.players[0].discard;
|
||||
const gained = gainEvt.dest === 'discard'
|
||||
? newDiscard.find(c => !prevDiscard.find(d => d.iid === c.iid))
|
||||
: newState.players[0].deck.find(c => !this.gs.players[0].deck.find(d => d.iid === c.iid));
|
||||
if (gained) {
|
||||
const sp = this.supplySprites.find(s => s.id === gainEvt.id);
|
||||
this._animGainCard(gained, sp?.x ?? CX, sp?.y ?? 300, gainEvt.dest, newState);
|
||||
} else {
|
||||
this._animating = false;
|
||||
if (this._pendingAnimState) { const s = this._pendingAnimState; this._pendingAnimState = null; this.setState(s); }
|
||||
else this.scheduleAdvance(10);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Draw animations ───────────────────────────────────────────────────────────
|
||||
|
||||
_animDiscardThenDraw(discardedCards, drawnCards, newState) {
|
||||
|
|
@ -1169,12 +1434,129 @@ export default class DominionGame extends Phaser.Scene {
|
|||
});
|
||||
}
|
||||
|
||||
_calcHandPositions(hand) {
|
||||
const gap = Math.min(18, (1300 - hand.length * HAND_W) / Math.max(1, hand.length - 1));
|
||||
// ── Drag and drop ─────────────────────────────────────────────────────────────
|
||||
|
||||
_getOrderedHand(hand) {
|
||||
if (!this._handOrder || this._handOrder.length === 0) return [...hand];
|
||||
const orderMap = new Map(this._handOrder.map((iid, i) => [iid, i]));
|
||||
return [...hand].sort((a, b) => (orderMap.get(a.iid) ?? 999) - (orderMap.get(b.iid) ?? 999));
|
||||
}
|
||||
|
||||
_reconcileHandOrder(hand) {
|
||||
if (!this._handOrder) return;
|
||||
const existing = new Set(hand.map(c => c.iid));
|
||||
this._handOrder = this._handOrder.filter(iid => existing.has(iid));
|
||||
for (const c of hand) {
|
||||
if (!this._handOrder.includes(c.iid)) this._handOrder.push(c.iid);
|
||||
}
|
||||
if (this._handOrder.length === 0) this._handOrder = null;
|
||||
}
|
||||
|
||||
_checkDragStart(p) {
|
||||
const dp = this._dragPotential;
|
||||
if (!dp) return;
|
||||
const dx = p.x - dp.startX, dy = p.y - dp.startY;
|
||||
if (Math.sqrt(dx * dx + dy * dy) > 8) {
|
||||
this._startDrag(dp.hs, p);
|
||||
this._dragPotential = null;
|
||||
}
|
||||
}
|
||||
|
||||
_startDrag(hs, pointer) {
|
||||
if (this._animating) return;
|
||||
const offsetX = hs.x - pointer.x;
|
||||
const offsetY = hs.baseY - pointer.y;
|
||||
this._dragState = {
|
||||
iid: hs.iid, id: hs.id, def: hs.def,
|
||||
isPlayableAction: hs.isPlayableAction,
|
||||
isPlayableTreasure: hs.isPlayableTreasure,
|
||||
offsetX, offsetY,
|
||||
};
|
||||
hs.face.setAlpha(0.2);
|
||||
const ghost = this.buildCardFace(HAND_W, HAND_H, hs.def);
|
||||
ghost.setPosition(pointer.x + offsetX, pointer.y + offsetY);
|
||||
ghost.setDepth(D.hand + 100);
|
||||
this.animLayer.add(ghost);
|
||||
this._dragState.ghost = ghost;
|
||||
if (hs.isPlayableAction || hs.isPlayableTreasure) this._showPlayDropZone();
|
||||
}
|
||||
|
||||
_onDragMove(p) {
|
||||
const ds = this._dragState;
|
||||
if (!ds?.ghost) return;
|
||||
ds.ghost.setPosition(p.x + ds.offsetX, p.y + ds.offsetY);
|
||||
}
|
||||
|
||||
_onDragUp(p) {
|
||||
const ds = this._dragState;
|
||||
if (!ds) return;
|
||||
this._dragJustEnded = true;
|
||||
this.time.delayedCall(0, () => { this._dragJustEnded = false; });
|
||||
this._hidePlayDropZone();
|
||||
ds.ghost.destroy();
|
||||
this._dragState = null;
|
||||
const cardCenterY = p.y + ds.offsetY;
|
||||
if (cardCenterY < 870 && (ds.isPlayableAction || ds.isPlayableTreasure)) {
|
||||
const hs = this.handSprites.find(s => s.iid === ds.iid);
|
||||
if (hs) hs.face.setAlpha(1);
|
||||
if (ds.isPlayableAction) this.humanPlayAction(ds.iid);
|
||||
else this.humanPlayTreasure(ds.iid);
|
||||
} else {
|
||||
this._reorderHand(ds.iid, p.x + ds.offsetX);
|
||||
}
|
||||
}
|
||||
|
||||
_cancelDrag() {
|
||||
const ds = this._dragState;
|
||||
if (!ds) return;
|
||||
this._hidePlayDropZone();
|
||||
ds.ghost.destroy();
|
||||
this._dragState = null;
|
||||
this._dragPotential = null;
|
||||
const hs = this.handSprites.find(s => s.iid === ds.iid);
|
||||
if (hs) hs.face.setAlpha(1);
|
||||
}
|
||||
|
||||
_reorderHand(iid, dropX) {
|
||||
const ordered = this._getOrderedHand(this.gs.players[0].hand);
|
||||
const rest = ordered.filter(c => c.iid !== iid);
|
||||
const gap = Math.min(18, (1300 - rest.length * HAND_W) / Math.max(1, rest.length - 1));
|
||||
const step = HAND_W + Math.max(-HAND_W * 0.45, gap);
|
||||
const total = (hand.length - 1) * step + HAND_W;
|
||||
const total = (rest.length - 1) * step + HAND_W;
|
||||
const startX = CX - total / 2 + HAND_W / 2;
|
||||
return hand.map((_, i) => startX + i * step);
|
||||
let insertIdx = rest.length;
|
||||
for (let i = 0; i < rest.length; i++) {
|
||||
if (dropX < startX + i * step) { insertIdx = i; break; }
|
||||
}
|
||||
this._handOrder = [
|
||||
...rest.slice(0, insertIdx).map(c => c.iid),
|
||||
iid,
|
||||
...rest.slice(insertIdx).map(c => c.iid),
|
||||
];
|
||||
this.render();
|
||||
}
|
||||
|
||||
_showPlayDropZone() {
|
||||
const g = this.add.graphics();
|
||||
g.fillStyle(COLORS.gold, 0.08);
|
||||
g.lineStyle(2, COLORS.gold, 0.55);
|
||||
g.fillRoundedRect(CX - 700, 490, 1400, 340, 20);
|
||||
g.strokeRoundedRect(CX - 700, 490, 1400, 340, 20);
|
||||
g.setDepth(D.inplay - 1);
|
||||
this.animLayer.add(g);
|
||||
this._dragDropZone = g;
|
||||
const lbl = this.add.text(CX, 660, '▲ Drop here to play', {
|
||||
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5).setAlpha(0.7).setDepth(D.inplay);
|
||||
this.animLayer.add(lbl);
|
||||
this._dragDropLabel = lbl;
|
||||
}
|
||||
|
||||
_hidePlayDropZone() {
|
||||
this._dragDropZone?.destroy();
|
||||
this._dragDropZone = null;
|
||||
this._dragDropLabel?.destroy();
|
||||
this._dragDropLabel = null;
|
||||
}
|
||||
|
||||
_animDrawCards(drawnCards, newState) {
|
||||
|
|
@ -1184,18 +1566,15 @@ export default class DominionGame extends Phaser.Scene {
|
|||
this.clearPrompt();
|
||||
this.render();
|
||||
|
||||
const hand = newState.players[0].hand;
|
||||
const positions = this._calcHandPositions(hand);
|
||||
|
||||
let idx = 0;
|
||||
const animateNext = () => {
|
||||
if (idx >= drawnCards.length) { this._finishDrawAnim(); return; }
|
||||
const card = drawnCards[idx++];
|
||||
const handIdx = hand.findIndex(c => c.iid === card.iid);
|
||||
const tx = positions[handIdx];
|
||||
const sprite = this.handSprites.find(s => s.iid === card.iid);
|
||||
const tx = sprite?.x ?? CX;
|
||||
this._animateOneCard(card, tx, 968, () => {
|
||||
const sprite = this.handSprites.find(s => s.iid === card.iid);
|
||||
if (sprite) sprite.face.setAlpha(1);
|
||||
const s2 = this.handSprites.find(s => s.iid === card.iid);
|
||||
if (s2) s2.face.setAlpha(1);
|
||||
this._animatingIids.delete(card.iid);
|
||||
animateNext();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue