feat(phase10): replace static "Lay Down" button with interactive staging area
- Remove the hardcoded "Lay Down Phase" button and `findLaydown` auto-complete. - Add a dynamic staging area at the bottom of the screen that renders empty slots for each phase group. - Enable drag-and-drop of hand cards into staging slots to manually build phases. - Add "Submit Phase" and "Clear" buttons to commit or reset the staged layout. - Implement validation using `validateLaydown` before submitting. - Fix Button hit area coordinates to ensure reliable click detection. - Update status text rendering with a background graphic for better visibility.
This commit is contained in:
parent
3e64c2dafd
commit
208645246c
|
|
@ -5,7 +5,7 @@ import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.
|
||||||
import { auth } from '../../services/auth.js';
|
import { auth } from '../../services/auth.js';
|
||||||
import { api } from '../../services/api.js';
|
import { api } from '../../services/api.js';
|
||||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||||
import { PHASES, getPhase } from './PhaseSpec.js';
|
import { PHASES, getPhase, validateLaydown } from './PhaseSpec.js';
|
||||||
import {
|
import {
|
||||||
applyDrawFromDeck,
|
applyDrawFromDeck,
|
||||||
applyDrawFromDiscard,
|
applyDrawFromDiscard,
|
||||||
|
|
@ -147,11 +147,11 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
this.seatChips = []; // seat → { container, phaseText, scoreText, doneRibbon }
|
this.seatChips = []; // seat → { container, phaseText, scoreText, doneRibbon }
|
||||||
this.playbookOpen = false;
|
this.playbookOpen = false;
|
||||||
this.playbookPanel = null;
|
this.playbookPanel = null;
|
||||||
this.layDownBtn = null;
|
|
||||||
|
|
||||||
this.localHandCards = [];
|
this.localHandCards = [];
|
||||||
this.dragState = null;
|
this.dragState = null;
|
||||||
this.potentialDrag = null;
|
this.potentialDrag = null;
|
||||||
|
this.stagingGroups = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -413,9 +413,10 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildHUD() {
|
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,
|
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'), {
|
new Button(this, 55, GAME_HEIGHT - 20, 'Leave', () => this.scene.start('GameMenu'), {
|
||||||
variant: 'ghost', width: 110, height: 40, fontSize: 18,
|
variant: 'ghost', width: 110, height: 40, fontSize: 18,
|
||||||
|
|
@ -424,11 +425,6 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
variant: 'ghost', width: 110, height: 40, fontSize: 18,
|
variant: 'ghost', width: 110, height: 40, fontSize: 18,
|
||||||
}).setDepth(D.ui);
|
}).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 ───────────────────────────────────────
|
// ── Drag-and-drop hand reordering ───────────────────────────────────────
|
||||||
|
|
@ -533,7 +529,10 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newDropTarget) {
|
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) {
|
if (newInsertIdx !== ds.insertIdx) {
|
||||||
ds.insertIdx = newInsertIdx;
|
ds.insertIdx = newInsertIdx;
|
||||||
this.tweens.killTweensOf(ds.slotIndicator);
|
this.tweens.killTweensOf(ds.slotIndicator);
|
||||||
|
|
@ -545,9 +544,15 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
|
|
||||||
_updateNonDraggedCards() {
|
_updateNonDraggedCards() {
|
||||||
const ds = this.dragState;
|
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 = [];
|
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++) {
|
for (let k = 0; k < others.length; k++) {
|
||||||
const j = others[k];
|
const j = others[k];
|
||||||
|
|
@ -566,10 +571,15 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
|
|
||||||
_settleNonDraggedCards() {
|
_settleNonDraggedCards() {
|
||||||
const ds = this.dragState;
|
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;
|
let k = 0;
|
||||||
for (let j = 0; j < n; j++) {
|
for (let j = 0; j < n; j++) {
|
||||||
if (j === ds.cardIdx) continue;
|
if (j === ds.cardIdx) continue;
|
||||||
|
if (stagedIds.has(hand[j].id)) continue;
|
||||||
const cardObj = this.localHandCards[j];
|
const cardObj = this.localHandCards[j];
|
||||||
this.tweens.killTweensOf(cardObj);
|
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' });
|
this.tweens.add({ targets: cardObj, x: 460 + k * HAND_SPREAD, rotation: 0, scaleX: 1, scaleY: 1, duration: 100, ease: 'Cubic.easeOut' });
|
||||||
|
|
@ -623,7 +633,41 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
return;
|
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) ───────────────────────────────────────────
|
// ── 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);
|
const finalIdx = Phaser.Math.Clamp(Math.round((card.x - 460) / HAND_SPREAD), 0, n - 1);
|
||||||
|
|
||||||
this.selectedHandIdx = null;
|
this.selectedHandIdx = null;
|
||||||
|
|
@ -649,6 +693,124 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
this.time.delayedCall(240, () => this.renderAll());
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btnX = panelX + panelW + 160;
|
||||||
|
const submitBtn = new Button(this, btnX, laidStart.y - 18, 'Submit Phase', () => this.submitStagingLaydown(), {
|
||||||
|
width: 200, height: 44, fontSize: 18,
|
||||||
|
}).setDepth(D.ui);
|
||||||
|
this.transientObjs.push(submitBtn);
|
||||||
|
|
||||||
|
const clearBtn = new Button(this, btnX, laidStart.y + 34, 'Clear', () => this.clearStagingLaydown(), {
|
||||||
|
variant: 'ghost', width: 200, height: 34, fontSize: 14,
|
||||||
|
}).setDepth(D.ui);
|
||||||
|
this.transientObjs.push(clearBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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) {
|
_dropTargetsEqual(a, b) {
|
||||||
if (!a && !b) return true;
|
if (!a && !b) return true;
|
||||||
if (!a || !b) return false;
|
if (!a || !b) return false;
|
||||||
|
|
@ -661,6 +823,17 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
if (!this.isLocalTurn() || !this.gs.drawnThisTurn) return null;
|
if (!this.isLocalTurn() || !this.gs.drawnThisTurn) return null;
|
||||||
const handCard = this.gs.players[0].hand[this.dragState.cardIdx];
|
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
|
// Discard pile — generous hit area
|
||||||
if (Math.abs(cardX - DISCARD_POS.x) < CARD_W && Math.abs(cardY - DISCARD_POS.y) < CARD_H) {
|
if (Math.abs(cardX - DISCARD_POS.x) < CARD_W && Math.abs(cardY - DISCARD_POS.y) < CARD_H) {
|
||||||
return { type: 'discard' };
|
return { type: 'discard' };
|
||||||
|
|
@ -746,6 +919,12 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
g.fillStyle(0xffd700, 0.22);
|
g.fillStyle(0xffd700, 0.22);
|
||||||
g.fillRoundedRect(gc.x - gc.w / 2, gc.y - gc.h / 2, gc.w, gc.h, 8);
|
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);
|
g.setDepth(D.highlight);
|
||||||
ds.actionHighlight = g;
|
ds.actionHighlight = g;
|
||||||
|
|
@ -774,6 +953,7 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
if (this.animating) return;
|
if (this.animating) return;
|
||||||
this.matchOver = false;
|
this.matchOver = false;
|
||||||
if (this.dragState) this.endCardDragImmediate();
|
if (this.dragState) this.endCardDragImmediate();
|
||||||
|
this.stagingGroups = null;
|
||||||
this.clearAllCardObjs();
|
this.clearAllCardObjs();
|
||||||
this.clearHighlights();
|
this.clearHighlights();
|
||||||
this.selectedHandIdx = null;
|
this.selectedHandIdx = null;
|
||||||
|
|
@ -894,6 +1074,7 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
|
|
||||||
renderAll() {
|
renderAll() {
|
||||||
if (this.dragState) this.endCardDragImmediate();
|
if (this.dragState) this.endCardDragImmediate();
|
||||||
|
this.refreshStagingArea();
|
||||||
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++) {
|
||||||
|
|
@ -902,7 +1083,7 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
this.renderSeatChips();
|
this.renderSeatChips();
|
||||||
this.renderTurnIndicator();
|
this.renderTurnIndicator();
|
||||||
this.refreshPlaybook();
|
this.refreshPlaybook();
|
||||||
this.refreshLayDownBtn();
|
this.renderStagingArea();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCenter() {
|
renderCenter() {
|
||||||
|
|
@ -928,19 +1109,42 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
|
|
||||||
if (seat === 0) this.localHandCards = [];
|
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
|
// 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];
|
||||||
let x, y;
|
let x, y, cardScale = 1;
|
||||||
if (layout.handAxis === 'x') {
|
if (seat === 0 && this.stagingGroups) {
|
||||||
x = layout.handStart.x + i * HAND_SPREAD;
|
const staged = stagedMap.get(card.id);
|
||||||
y = layout.handStart.y;
|
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 {
|
} else {
|
||||||
x = layout.handStart.x;
|
if (layout.handAxis === 'x') {
|
||||||
y = layout.handStart.y - i * HAND_SPREAD;
|
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, {
|
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);
|
this.cardObjs.set(`hand-${seat}-${card.id}`, c);
|
||||||
if (seat === 0) {
|
if (seat === 0) {
|
||||||
|
|
@ -1032,16 +1236,22 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
this.turnGlow.setPosition(lay.portrait.x, lay.portrait.y);
|
this.turnGlow.setPosition(lay.portrait.x, lay.portrait.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshLayDownBtn() {
|
refreshStagingArea() {
|
||||||
if (!this.layDownBtn) return;
|
const player = this.gs.players[0];
|
||||||
const isLocal = this.isLocalTurn();
|
const eligible = this.isLocalTurn() && this.gs.drawnThisTurn && !player.laidDown;
|
||||||
const localPlayer = this.gs.players[0];
|
if (!eligible) {
|
||||||
if (!isLocal || !this.gs.drawnThisTurn || localPlayer.laidDown) {
|
this.stagingGroups = null;
|
||||||
this.layDownBtn.setVisible(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const layout = findLaydown(localPlayer.hand, localPlayer.phase);
|
if (!this.stagingGroups) {
|
||||||
this.layDownBtn.setVisible(!!layout);
|
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),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Local input ─────────────────────────────────────────────────────────
|
// ── Local input ─────────────────────────────────────────────────────────
|
||||||
|
|
@ -1101,27 +1311,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() {
|
highlightTargetsForCard() {
|
||||||
this.clearHighlights();
|
this.clearHighlights();
|
||||||
|
|
@ -1440,7 +1629,18 @@ export default class Phase10Game extends Phaser.Scene {
|
||||||
// ── HUD helpers ─────────────────────────────────────────────────────────
|
// ── HUD helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
setStatus(s) {
|
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) {
|
nameForSeat(seat) {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export class Button extends Phaser.GameObjects.Container {
|
||||||
this.add([this.bgRect, this.text]);
|
this.add([this.bgRect, this.text]);
|
||||||
|
|
||||||
const bgHitArea = new Phaser.Geom.Rectangle(-hw, -hh, width, height);
|
const bgHitArea = new Phaser.Geom.Rectangle(-hw, -hh, width, height);
|
||||||
const textHitArea = new Phaser.Geom.Rectangle(0, 0, width, height);
|
const textHitArea = new Phaser.Geom.Rectangle(-hw, -hh, width, height);
|
||||||
const hitCb = Phaser.Geom.Rectangle.Contains;
|
const hitCb = Phaser.Geom.Rectangle.Contains;
|
||||||
this.setSize(width, height);
|
this.setSize(width, height);
|
||||||
this.setInteractive({ useHandCursor: true, hitArea: bgHitArea, hitAreaCallback: hitCb });
|
this.setInteractive({ useHandCursor: true, hitArea: bgHitArea, hitAreaCallback: hitCb });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue