|
|
|
|
@ -5,7 +5,7 @@ import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.
|
|
|
|
|
import { auth } from '../../services/auth.js';
|
|
|
|
|
import { api } from '../../services/api.js';
|
|
|
|
|
import { playSound, SFX } from '../../ui/Sounds.js';
|
|
|
|
|
import { PHASES, getPhase } from './PhaseSpec.js';
|
|
|
|
|
import { PHASES, getPhase, validateLaydown } from './PhaseSpec.js';
|
|
|
|
|
import {
|
|
|
|
|
applyDrawFromDeck,
|
|
|
|
|
applyDrawFromDiscard,
|
|
|
|
|
@ -147,11 +147,13 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
this.seatChips = []; // seat → { container, phaseText, scoreText, doneRibbon }
|
|
|
|
|
this.playbookOpen = false;
|
|
|
|
|
this.playbookPanel = null;
|
|
|
|
|
this.layDownBtn = null;
|
|
|
|
|
|
|
|
|
|
this.localHandCards = [];
|
|
|
|
|
this.dragState = null;
|
|
|
|
|
this.potentialDrag = null;
|
|
|
|
|
this.stagingGroups = null;
|
|
|
|
|
this.submitBtn = null;
|
|
|
|
|
this.clearBtn = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
create() {
|
|
|
|
|
@ -413,9 +415,10 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buildHUD() {
|
|
|
|
|
this.statusText = this.add.text(CX, 36, '', {
|
|
|
|
|
this.statusBg = this.add.graphics().setDepth(D.ui - 1).setVisible(false);
|
|
|
|
|
this.statusText = this.add.text(1052, 870, '', {
|
|
|
|
|
fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex,
|
|
|
|
|
}).setOrigin(0.5).setDepth(D.ui);
|
|
|
|
|
}).setOrigin(0, 0.5).setDepth(D.ui);
|
|
|
|
|
|
|
|
|
|
new Button(this, 55, GAME_HEIGHT - 20, 'Leave', () => this.scene.start('GameMenu'), {
|
|
|
|
|
variant: 'ghost', width: 110, height: 40, fontSize: 18,
|
|
|
|
|
@ -424,11 +427,6 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
variant: 'ghost', width: 110, height: 40, fontSize: 18,
|
|
|
|
|
}).setDepth(D.ui);
|
|
|
|
|
|
|
|
|
|
// "Lay Down" button — hidden until the local player can lay down.
|
|
|
|
|
this.layDownBtn = new Button(this, CX, CY + 200, 'Lay Down Phase', () => this.onLayDownClick(), {
|
|
|
|
|
width: 280, height: 56, fontSize: 22,
|
|
|
|
|
}).setDepth(D.ui);
|
|
|
|
|
this.layDownBtn.setVisible(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Drag-and-drop hand reordering ───────────────────────────────────────
|
|
|
|
|
@ -533,7 +531,10 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!newDropTarget) {
|
|
|
|
|
const newInsertIdx = Phaser.Math.Clamp(Math.round((card.x - 460) / HAND_SPREAD), 0, n - 1);
|
|
|
|
|
const stagedCount = this.stagingGroups
|
|
|
|
|
? this.stagingGroups.reduce((s, g) => s + g.slots.filter((id) => id !== null).length, 0) : 0;
|
|
|
|
|
const effectiveN = n - stagedCount;
|
|
|
|
|
const newInsertIdx = Phaser.Math.Clamp(Math.round((card.x - 460) / HAND_SPREAD), 0, Math.max(0, effectiveN - 1));
|
|
|
|
|
if (newInsertIdx !== ds.insertIdx) {
|
|
|
|
|
ds.insertIdx = newInsertIdx;
|
|
|
|
|
this.tweens.killTweensOf(ds.slotIndicator);
|
|
|
|
|
@ -545,9 +546,15 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
|
|
|
|
|
_updateNonDraggedCards() {
|
|
|
|
|
const ds = this.dragState;
|
|
|
|
|
const n = this.gs.players[0].hand.length;
|
|
|
|
|
const hand = this.gs.players[0].hand;
|
|
|
|
|
const n = hand.length;
|
|
|
|
|
const stagedIds = this.stagingGroups
|
|
|
|
|
? new Set(this.stagingGroups.flatMap((g) => g.slots.filter((id) => id !== null)))
|
|
|
|
|
: new Set();
|
|
|
|
|
const others = [];
|
|
|
|
|
for (let i = 0; i < n; i++) { if (i !== ds.cardIdx) others.push(i); }
|
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
|
|
|
if (i !== ds.cardIdx && !stagedIds.has(hand[i].id)) others.push(i);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let k = 0; k < others.length; k++) {
|
|
|
|
|
const j = others[k];
|
|
|
|
|
@ -566,10 +573,15 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
|
|
|
|
|
_settleNonDraggedCards() {
|
|
|
|
|
const ds = this.dragState;
|
|
|
|
|
const n = this.gs.players[0].hand.length;
|
|
|
|
|
const hand = this.gs.players[0].hand;
|
|
|
|
|
const n = hand.length;
|
|
|
|
|
const stagedIds = this.stagingGroups
|
|
|
|
|
? new Set(this.stagingGroups.flatMap((g) => g.slots.filter((id) => id !== null)))
|
|
|
|
|
: new Set();
|
|
|
|
|
let k = 0;
|
|
|
|
|
for (let j = 0; j < n; j++) {
|
|
|
|
|
if (j === ds.cardIdx) continue;
|
|
|
|
|
if (stagedIds.has(hand[j].id)) continue;
|
|
|
|
|
const cardObj = this.localHandCards[j];
|
|
|
|
|
this.tweens.killTweensOf(cardObj);
|
|
|
|
|
this.tweens.add({ targets: cardObj, x: 460 + k * HAND_SPREAD, rotation: 0, scaleX: 1, scaleY: 1, duration: 100, ease: 'Cubic.easeOut' });
|
|
|
|
|
@ -623,7 +635,41 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Stage path ────────────────────────────────────────────────────────
|
|
|
|
|
if (dropTarget?.type === 'stage') {
|
|
|
|
|
const { groupIdx } = dropTarget;
|
|
|
|
|
const g = this.stagingGroups[groupIdx];
|
|
|
|
|
const emptySlotIdx = g.slots.indexOf(null);
|
|
|
|
|
if (emptySlotIdx !== -1) {
|
|
|
|
|
const handCard = this.gs.players[0].hand[ds.cardIdx];
|
|
|
|
|
for (const sg of this.stagingGroups) {
|
|
|
|
|
const old = sg.slots.indexOf(handCard.id);
|
|
|
|
|
if (old !== -1) { sg.slots[old] = null; break; }
|
|
|
|
|
}
|
|
|
|
|
g.slots[emptySlotIdx] = handCard.id;
|
|
|
|
|
const pos = this._getStagingSlotPos(groupIdx, emptySlotIdx);
|
|
|
|
|
const scale = LAIDOWN_CARD_W / CARD_W;
|
|
|
|
|
this.tweens.killTweensOf(card);
|
|
|
|
|
this._settleNonDraggedCards();
|
|
|
|
|
this.dragState = null;
|
|
|
|
|
this.tweens.add({
|
|
|
|
|
targets: card, x: pos.x, y: pos.y, rotation: 0, scaleX: scale, scaleY: scale,
|
|
|
|
|
duration: 160, ease: 'Cubic.easeOut',
|
|
|
|
|
onComplete: () => this.renderAll(),
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Reorder path (existing) ───────────────────────────────────────────
|
|
|
|
|
// Clear staging slot if this card was staged
|
|
|
|
|
if (this.stagingGroups) {
|
|
|
|
|
const handCard = this.gs.players[0].hand[ds.cardIdx];
|
|
|
|
|
for (const sg of this.stagingGroups) {
|
|
|
|
|
const old = sg.slots.indexOf(handCard.id);
|
|
|
|
|
if (old !== -1) { sg.slots[old] = null; break; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const finalIdx = Phaser.Math.Clamp(Math.round((card.x - 460) / HAND_SPREAD), 0, n - 1);
|
|
|
|
|
|
|
|
|
|
this.selectedHandIdx = null;
|
|
|
|
|
@ -649,6 +695,115 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
this.time.delayedCall(240, () => this.renderAll());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_getStagingSlotPos(gi, si) {
|
|
|
|
|
const { laidStart } = slotLayout('bottom');
|
|
|
|
|
let cx = laidStart.x;
|
|
|
|
|
for (let k = 0; k < gi; k++) {
|
|
|
|
|
cx += this.stagingGroups[k].count * (LAIDOWN_CARD_W * 0.75) + (LAIDOWN_CARD_W * 0.25) + GROUP_GAP;
|
|
|
|
|
}
|
|
|
|
|
return { x: cx + si * (LAIDOWN_CARD_W * 0.75) + LAIDOWN_CARD_W / 2, y: laidStart.y };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_getStagingGroupBounds(gi) {
|
|
|
|
|
const { laidStart } = slotLayout('bottom');
|
|
|
|
|
let cx = laidStart.x;
|
|
|
|
|
for (let k = 0; k < gi; k++) {
|
|
|
|
|
cx += this.stagingGroups[k].count * (LAIDOWN_CARD_W * 0.75) + (LAIDOWN_CARD_W * 0.25) + GROUP_GAP;
|
|
|
|
|
}
|
|
|
|
|
const gw = this.stagingGroups[gi].count * (LAIDOWN_CARD_W * 0.75) + (LAIDOWN_CARD_W * 0.25);
|
|
|
|
|
return { cx: cx + gw / 2, cy: laidStart.y, hw: gw / 2 + 16, hh: LAIDOWN_CARD_H / 2 + 16 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderStagingArea() {
|
|
|
|
|
if (!this.stagingGroups) return;
|
|
|
|
|
const { laidStart } = slotLayout('bottom');
|
|
|
|
|
|
|
|
|
|
let totalW = 0;
|
|
|
|
|
for (let gi = 0; gi < this.stagingGroups.length; gi++) {
|
|
|
|
|
if (gi > 0) totalW += GROUP_GAP;
|
|
|
|
|
totalW += this.stagingGroups[gi].count * (LAIDOWN_CARD_W * 0.75) + (LAIDOWN_CARD_W * 0.25);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PX = 14, LABEL_H = 22, PY = 8;
|
|
|
|
|
const panelX = laidStart.x - PX;
|
|
|
|
|
const panelY = laidStart.y - LAIDOWN_CARD_H / 2 - LABEL_H - PY;
|
|
|
|
|
const panelW = totalW + PX * 2;
|
|
|
|
|
const panelH = LAIDOWN_CARD_H + LABEL_H + PY * 2;
|
|
|
|
|
|
|
|
|
|
const bg = this.add.graphics().setDepth(D.card - 2);
|
|
|
|
|
bg.fillStyle(COLORS.panel, 0.92);
|
|
|
|
|
bg.fillRoundedRect(panelX, panelY, panelW, panelH, 10);
|
|
|
|
|
bg.lineStyle(2, COLORS.accent, 0.6);
|
|
|
|
|
bg.strokeRoundedRect(panelX, panelY, panelW, panelH, 10);
|
|
|
|
|
this.transientObjs.push(bg);
|
|
|
|
|
|
|
|
|
|
let cx = laidStart.x;
|
|
|
|
|
for (let gi = 0; gi < this.stagingGroups.length; gi++) {
|
|
|
|
|
const g = this.stagingGroups[gi];
|
|
|
|
|
const gw = g.count * (LAIDOWN_CARD_W * 0.75) + (LAIDOWN_CARD_W * 0.25);
|
|
|
|
|
const kindLabel = g.kind === 'set' ? `Set of ${g.count}`
|
|
|
|
|
: g.kind === 'run' ? `Run of ${g.count}`
|
|
|
|
|
: `${g.count} of one color`;
|
|
|
|
|
const lbl = this.add.text(cx + gw / 2, panelY + PY + 2, kindLabel, {
|
|
|
|
|
fontFamily: 'Righteous', fontSize: '13px', color: COLORS.goldHex,
|
|
|
|
|
}).setOrigin(0.5, 0).setDepth(D.card - 1);
|
|
|
|
|
this.transientObjs.push(lbl);
|
|
|
|
|
|
|
|
|
|
for (let si = 0; si < g.count; si++) {
|
|
|
|
|
if (g.slots[si] === null) {
|
|
|
|
|
const pos = this._getStagingSlotPos(gi, si);
|
|
|
|
|
const sg = this.add.graphics().setDepth(D.card - 1);
|
|
|
|
|
sg.lineStyle(2, COLORS.accent, 0.35);
|
|
|
|
|
sg.strokeRoundedRect(pos.x - LAIDOWN_CARD_W / 2, pos.y - LAIDOWN_CARD_H / 2, LAIDOWN_CARD_W, LAIDOWN_CARD_H, 6);
|
|
|
|
|
sg.fillStyle(COLORS.accent, 0.04);
|
|
|
|
|
sg.fillRoundedRect(pos.x - LAIDOWN_CARD_W / 2, pos.y - LAIDOWN_CARD_H / 2, LAIDOWN_CARD_W, LAIDOWN_CARD_H, 6);
|
|
|
|
|
this.transientObjs.push(sg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cx += gw + GROUP_GAP;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
submitStagingLaydown() {
|
|
|
|
|
const player = this.gs.players[0];
|
|
|
|
|
for (let gi = 0; gi < this.stagingGroups.length; gi++) {
|
|
|
|
|
if (this.stagingGroups[gi].slots.some((s) => s === null)) {
|
|
|
|
|
this.setStatus(`Group ${gi + 1} needs ${this.stagingGroups[gi].count} cards — fill all slots.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const resolved = this.stagingGroups.map((g) => ({
|
|
|
|
|
kind: g.kind,
|
|
|
|
|
cards: g.slots.map((id) => player.hand.find((c) => c.id === id)),
|
|
|
|
|
}));
|
|
|
|
|
const vr = validateLaydown(player.phase, resolved);
|
|
|
|
|
if (!vr.ok) {
|
|
|
|
|
this.setStatus(`Invalid: ${vr.reason}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const next = applyLaydown(this.gs, this.stagingGroups.map((g) => ({
|
|
|
|
|
kind: g.kind, cardIds: g.slots.slice(),
|
|
|
|
|
})));
|
|
|
|
|
if (next === this.gs) {
|
|
|
|
|
this.setStatus("Couldn't lay down — check your cards.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.stagingGroups = null;
|
|
|
|
|
this._destroyStagingButtons();
|
|
|
|
|
this.gs = next;
|
|
|
|
|
this.selectedHandIdx = null;
|
|
|
|
|
this.clearHighlights();
|
|
|
|
|
this.renderAll();
|
|
|
|
|
this.setStatus('Laid down! Now hit or discard.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearStagingLaydown() {
|
|
|
|
|
if (!this.stagingGroups) return;
|
|
|
|
|
for (const g of this.stagingGroups) g.slots.fill(null);
|
|
|
|
|
this.renderAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_dropTargetsEqual(a, b) {
|
|
|
|
|
if (!a && !b) return true;
|
|
|
|
|
if (!a || !b) return false;
|
|
|
|
|
@ -661,6 +816,17 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
if (!this.isLocalTurn() || !this.gs.drawnThisTurn) return null;
|
|
|
|
|
const handCard = this.gs.players[0].hand[this.dragState.cardIdx];
|
|
|
|
|
|
|
|
|
|
// Staging area drop targets
|
|
|
|
|
if (this.stagingGroups && !this.gs.players[0].laidDown) {
|
|
|
|
|
for (let gi = 0; gi < this.stagingGroups.length; gi++) {
|
|
|
|
|
if (!this.stagingGroups[gi].slots.some((s) => s === null)) continue;
|
|
|
|
|
const b = this._getStagingGroupBounds(gi);
|
|
|
|
|
if (Math.abs(cardX - b.cx) < b.hw && Math.abs(cardY - b.cy) < b.hh) {
|
|
|
|
|
return { type: 'stage', groupIdx: gi };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Discard pile — generous hit area
|
|
|
|
|
if (Math.abs(cardX - DISCARD_POS.x) < CARD_W && Math.abs(cardY - DISCARD_POS.y) < CARD_H) {
|
|
|
|
|
return { type: 'discard' };
|
|
|
|
|
@ -746,6 +912,12 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
g.fillStyle(0xffd700, 0.22);
|
|
|
|
|
g.fillRoundedRect(gc.x - gc.w / 2, gc.y - gc.h / 2, gc.w, gc.h, 8);
|
|
|
|
|
}
|
|
|
|
|
} else if (dropTarget.type === 'stage') {
|
|
|
|
|
const b = this._getStagingGroupBounds(dropTarget.groupIdx);
|
|
|
|
|
g.lineStyle(4, COLORS.accent, 1);
|
|
|
|
|
g.strokeRoundedRect(b.cx - b.hw, b.cy - b.hh, b.hw * 2, b.hh * 2, 8);
|
|
|
|
|
g.fillStyle(COLORS.accent, 0.18);
|
|
|
|
|
g.fillRoundedRect(b.cx - b.hw, b.cy - b.hh, b.hw * 2, b.hh * 2, 8);
|
|
|
|
|
}
|
|
|
|
|
g.setDepth(D.highlight);
|
|
|
|
|
ds.actionHighlight = g;
|
|
|
|
|
@ -774,6 +946,8 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
if (this.animating) return;
|
|
|
|
|
this.matchOver = false;
|
|
|
|
|
if (this.dragState) this.endCardDragImmediate();
|
|
|
|
|
this.stagingGroups = null;
|
|
|
|
|
this._destroyStagingButtons();
|
|
|
|
|
this.clearAllCardObjs();
|
|
|
|
|
this.clearHighlights();
|
|
|
|
|
this.selectedHandIdx = null;
|
|
|
|
|
@ -894,6 +1068,7 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
|
|
|
|
|
renderAll() {
|
|
|
|
|
if (this.dragState) this.endCardDragImmediate();
|
|
|
|
|
this.refreshStagingArea();
|
|
|
|
|
this.clearAllCardObjs();
|
|
|
|
|
this.renderCenter();
|
|
|
|
|
for (let seat = 0; seat < this.gs.players.length; seat++) {
|
|
|
|
|
@ -902,7 +1077,7 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
this.renderSeatChips();
|
|
|
|
|
this.renderTurnIndicator();
|
|
|
|
|
this.refreshPlaybook();
|
|
|
|
|
this.refreshLayDownBtn();
|
|
|
|
|
this.renderStagingArea();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderCenter() {
|
|
|
|
|
@ -928,19 +1103,42 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
|
|
|
|
|
if (seat === 0) this.localHandCards = [];
|
|
|
|
|
|
|
|
|
|
// Build staged-card lookup for seat 0
|
|
|
|
|
const stagedMap = new Map(); // cardId → { gi, si }
|
|
|
|
|
if (seat === 0 && this.stagingGroups) {
|
|
|
|
|
for (let gi = 0; gi < this.stagingGroups.length; gi++) {
|
|
|
|
|
this.stagingGroups[gi].slots.forEach((id, si) => {
|
|
|
|
|
if (id !== null) stagedMap.set(id, { gi, si });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let handPosCounter = 0;
|
|
|
|
|
|
|
|
|
|
// Hand
|
|
|
|
|
for (let i = 0; i < player.hand.length; i++) {
|
|
|
|
|
const card = player.hand[i];
|
|
|
|
|
let x, y;
|
|
|
|
|
if (layout.handAxis === 'x') {
|
|
|
|
|
x = layout.handStart.x + i * HAND_SPREAD;
|
|
|
|
|
y = layout.handStart.y;
|
|
|
|
|
let x, y, cardScale = 1;
|
|
|
|
|
if (seat === 0 && this.stagingGroups) {
|
|
|
|
|
const staged = stagedMap.get(card.id);
|
|
|
|
|
if (staged) {
|
|
|
|
|
const pos = this._getStagingSlotPos(staged.gi, staged.si);
|
|
|
|
|
x = pos.x; y = pos.y; cardScale = LAIDOWN_CARD_W / CARD_W;
|
|
|
|
|
} else {
|
|
|
|
|
x = layout.handStart.x + handPosCounter * HAND_SPREAD;
|
|
|
|
|
y = layout.handStart.y;
|
|
|
|
|
handPosCounter++;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
x = layout.handStart.x;
|
|
|
|
|
y = layout.handStart.y - i * HAND_SPREAD;
|
|
|
|
|
if (layout.handAxis === 'x') {
|
|
|
|
|
x = layout.handStart.x + i * HAND_SPREAD;
|
|
|
|
|
y = layout.handStart.y;
|
|
|
|
|
} else {
|
|
|
|
|
x = layout.handStart.x;
|
|
|
|
|
y = layout.handStart.y - i * HAND_SPREAD;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const c = this.makeCardSprite(card, x, y, {
|
|
|
|
|
faceUp: layout.handFaceUp, rotation: layout.rotateCards,
|
|
|
|
|
faceUp: layout.handFaceUp, rotation: layout.rotateCards, scale: cardScale,
|
|
|
|
|
});
|
|
|
|
|
this.cardObjs.set(`hand-${seat}-${card.id}`, c);
|
|
|
|
|
if (seat === 0) {
|
|
|
|
|
@ -1032,16 +1230,48 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
this.turnGlow.setPosition(lay.portrait.x, lay.portrait.y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refreshLayDownBtn() {
|
|
|
|
|
if (!this.layDownBtn) return;
|
|
|
|
|
const isLocal = this.isLocalTurn();
|
|
|
|
|
const localPlayer = this.gs.players[0];
|
|
|
|
|
if (!isLocal || !this.gs.drawnThisTurn || localPlayer.laidDown) {
|
|
|
|
|
this.layDownBtn.setVisible(false);
|
|
|
|
|
refreshStagingArea() {
|
|
|
|
|
const player = this.gs.players[0];
|
|
|
|
|
const eligible = this.isLocalTurn() && this.gs.drawnThisTurn && !player.laidDown;
|
|
|
|
|
if (!eligible) {
|
|
|
|
|
this.stagingGroups = null;
|
|
|
|
|
this._destroyStagingButtons();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const layout = findLaydown(localPlayer.hand, localPlayer.phase);
|
|
|
|
|
this.layDownBtn.setVisible(!!layout);
|
|
|
|
|
if (!this.stagingGroups) {
|
|
|
|
|
const spec = getPhase(player.phase);
|
|
|
|
|
if (!spec) return;
|
|
|
|
|
this.stagingGroups = spec.groups.map((g) => ({
|
|
|
|
|
kind: g.kind,
|
|
|
|
|
count: g.count,
|
|
|
|
|
slots: new Array(g.count).fill(null),
|
|
|
|
|
}));
|
|
|
|
|
this._createStagingButtons();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_createStagingButtons() {
|
|
|
|
|
this._destroyStagingButtons();
|
|
|
|
|
const { laidStart } = slotLayout('bottom');
|
|
|
|
|
let totalW = 0;
|
|
|
|
|
for (let i = 0; i < this.stagingGroups.length; i++) {
|
|
|
|
|
if (i > 0) totalW += GROUP_GAP;
|
|
|
|
|
totalW += this.stagingGroups[i].count * (LAIDOWN_CARD_W * 0.75) + (LAIDOWN_CARD_W * 0.25);
|
|
|
|
|
}
|
|
|
|
|
const panelX = laidStart.x - 14;
|
|
|
|
|
const panelW = totalW + 28;
|
|
|
|
|
const btnX = panelX + panelW + 160;
|
|
|
|
|
this.submitBtn = new Button(this, btnX, laidStart.y - 18, 'Submit Phase', () => this.submitStagingLaydown(), {
|
|
|
|
|
width: 200, height: 44, fontSize: 18,
|
|
|
|
|
}).setDepth(D.ui);
|
|
|
|
|
this.clearBtn = new Button(this, btnX, laidStart.y + 34, 'Clear', () => this.clearStagingLaydown(), {
|
|
|
|
|
variant: 'ghost', width: 200, height: 34, fontSize: 14,
|
|
|
|
|
}).setDepth(D.ui);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_destroyStagingButtons() {
|
|
|
|
|
if (this.submitBtn) { this.submitBtn.destroy(); this.submitBtn = null; }
|
|
|
|
|
if (this.clearBtn) { this.clearBtn.destroy(); this.clearBtn = null; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Local input ─────────────────────────────────────────────────────────
|
|
|
|
|
@ -1101,27 +1331,6 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onLayDownClick() {
|
|
|
|
|
if (!this.isLocalTurn() || this.animating) return;
|
|
|
|
|
const player = this.gs.players[0];
|
|
|
|
|
if (player.laidDown) return;
|
|
|
|
|
const layout = findLaydown(player.hand, player.phase);
|
|
|
|
|
if (!layout) {
|
|
|
|
|
this.setStatus("You don't have the cards for this phase yet.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const groups = layout.map((g) => ({ kind: g.kind, cardIds: g.cards.map((c) => c.id) }));
|
|
|
|
|
const next = applyLaydown(this.gs, groups);
|
|
|
|
|
if (next === this.gs) {
|
|
|
|
|
this.setStatus("Couldn't lay down — invalid grouping.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.gs = next;
|
|
|
|
|
this.selectedHandIdx = null;
|
|
|
|
|
this.clearHighlights();
|
|
|
|
|
this.renderAll();
|
|
|
|
|
this.setStatus('Laid down! Now hit or discard.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
highlightTargetsForCard() {
|
|
|
|
|
this.clearHighlights();
|
|
|
|
|
@ -1440,7 +1649,18 @@ export default class Phase10Game extends Phaser.Scene {
|
|
|
|
|
// ── HUD helpers ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
setStatus(s) {
|
|
|
|
|
if (this.statusText) this.statusText.setText(s);
|
|
|
|
|
if (!this.statusText) return;
|
|
|
|
|
this.statusText.setText(s);
|
|
|
|
|
if (!s) {
|
|
|
|
|
this.statusBg.setVisible(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const pad = 12;
|
|
|
|
|
const w = this.statusText.width + pad * 2;
|
|
|
|
|
this.statusBg.clear();
|
|
|
|
|
this.statusBg.fillStyle(0x000000, 0.5);
|
|
|
|
|
this.statusBg.fillRoundedRect(1040, 838, w, 64, 8);
|
|
|
|
|
this.statusBg.setVisible(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nameForSeat(seat) {
|
|
|
|
|
|