Canasta, and Cribbage updates. Added 3 new AI opponents.
This commit is contained in:
parent
8a79b6909d
commit
833a214c24
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 MiB After Width: | Height: | Size: 4.2 MiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -590,7 +590,7 @@
|
|||
},
|
||||
{
|
||||
"id": "smasher",
|
||||
"spriteIndex": 19,
|
||||
"spriteIndex": 21,
|
||||
"name": "The Smasher",
|
||||
"bio": "Oooooooh yeah brother!!! You wanna play with The Smasher?!",
|
||||
"speech": {
|
||||
|
|
@ -616,6 +616,93 @@
|
|||
"smasher-pick"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dv-8-2303",
|
||||
"spriteIndex": 22,
|
||||
"name": "DV-8-2303",
|
||||
"bio": "Cybernetic Systems online. Activate human defeat protocol... EOF",
|
||||
"speech": {
|
||||
"intro": [
|
||||
"dv-8-2303-intro-01",
|
||||
"dv-8-2303-intro-02"
|
||||
],
|
||||
"happy": [
|
||||
"dv-8-2303-happy-01",
|
||||
"dv-8-2303-happy-02",
|
||||
"dv-8-2303-happy-03",
|
||||
"dv-8-2303-happy-04",
|
||||
"dv-8-2303-happy-05"
|
||||
],
|
||||
"upset": [
|
||||
"dv-8-2303-upset-01",
|
||||
"dv-8-2303-upset-02",
|
||||
"dv-8-2303-upset-03",
|
||||
"dv-8-2303-upset-04",
|
||||
"dv-8-2303-upset-05"
|
||||
],
|
||||
"pick": [
|
||||
"dv-8-2303-pick"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "nicole",
|
||||
"spriteIndex": 23,
|
||||
"name": "Nicole",
|
||||
"bio": "I've disabled the firewall protections on this website. Now to hack the core!",
|
||||
"speech": {
|
||||
"intro": [
|
||||
"nicole-intro-01",
|
||||
"nicole-intro-02"
|
||||
],
|
||||
"happy": [
|
||||
"nicole-happy-01",
|
||||
"nicole-happy-02",
|
||||
"nicole-happy-03",
|
||||
"nicole-happy-04",
|
||||
"nicole-happy-05"
|
||||
],
|
||||
"upset": [
|
||||
"nicole-upset-01",
|
||||
"nicole-upset-02",
|
||||
"nicole-upset-03",
|
||||
"nicole-upset-04",
|
||||
"nicole-upset-05"
|
||||
],
|
||||
"pick": [
|
||||
"nicole-pick"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kage",
|
||||
"spriteIndex": 24,
|
||||
"name": "Kage",
|
||||
"bio": "I have traveled far to play these games. Honor me and let us play!",
|
||||
"speech": {
|
||||
"intro": [
|
||||
"kage-intro-01",
|
||||
"kage-intro-02"
|
||||
],
|
||||
"happy": [
|
||||
"kage-happy-01",
|
||||
"kage-happy-02",
|
||||
"kage-happy-03",
|
||||
"kage-happy-04",
|
||||
"kage-happy-05"
|
||||
],
|
||||
"upset": [
|
||||
"kage-upset-01",
|
||||
"kage-upset-02",
|
||||
"kage-upset-03",
|
||||
"kage-upset-04",
|
||||
"kage-upset-05"
|
||||
],
|
||||
"pick": [
|
||||
"kage-pick"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
|||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
import { api } from '../../services/api.js';
|
||||
import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js';
|
||||
import { THEME, TEAM_OF_SEAT, isWild, isRedThree, minimumMeld } from './CanastaData.js';
|
||||
import { THEME, TEAM_OF_SEAT, isWild, isRedThree, minimumMeld, cardScore, NATURAL_CANASTA, MIXED_CANASTA } from './CanastaData.js';
|
||||
import {
|
||||
createInitialState, drawStock, takeDiscard, meld, discard, startNextHand,
|
||||
takePlan, teamMeld, isCanasta, isNaturalCanasta, meetsInitialMeld, teamHasCanasta,
|
||||
|
|
@ -24,10 +24,10 @@ const STOCK = { x: CX - 96, y: CY };
|
|||
const DISCARD = { x: CX + 96, y: CY };
|
||||
|
||||
const SEAT = {
|
||||
0: { hand: { x: CX, y: 930 }, port: { x: 150, y: 930 }, name: { x: 230, y: 858 } },
|
||||
1: { hand: { x: 96, y: CY }, port: { x: 110, y: 215 }, name: { x: 110, y: 285 } },
|
||||
2: { hand: { x: CX, y: 78 }, port: { x: 150, y: 96 }, name: { x: 230, y: 150 } },
|
||||
3: { hand: { x: GAME_WIDTH - 96, y: CY }, port: { x: GAME_WIDTH - 110, y: 215 }, name: { x: GAME_WIDTH - 110, y: 285 } },
|
||||
0: { hand: { x: CX, y: 930 }, port: { x: 650, y: 930 }, name: { x: 530, y: 858 } },
|
||||
1: { hand: { x: 96, y: CY }, port: { x: 110, y: 290 }, name: { x: 110, y: 360 } },
|
||||
2: { hand: { x: CX, y: 78 }, port: { x: 650, y: 96 }, name: { x: 530, y: 150 } },
|
||||
3: { hand: { x: GAME_WIDTH - 96, y: CY }, port: { x: GAME_WIDTH - 110, y: 290 }, name: { x: GAME_WIDTH - 110, y: 360 } },
|
||||
};
|
||||
const MELD_ZONE = { 0: { y: 690 }, 1: { y: 282 } };
|
||||
|
||||
|
|
@ -56,10 +56,14 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
3: this.opponents[2]?.name ?? 'East',
|
||||
};
|
||||
|
||||
this.playfield = data.playfield ?? null;
|
||||
|
||||
this.state = createInitialState();
|
||||
this.selected = new Set();
|
||||
this.busy = false;
|
||||
this.recorded = false;
|
||||
this.turnStartState = null;
|
||||
this.meldDropZones = [];
|
||||
this.handObjs = { 0: [], 1: [], 2: [], 3: [] };
|
||||
this.meldObjs = [];
|
||||
this.pileObjs = [];
|
||||
|
|
@ -70,6 +74,7 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
|
||||
create() {
|
||||
try { new MusicPlayer(this, this.cache.json.get('music')?.tracks ?? []); } catch { /* optional */ }
|
||||
this.input.dragDistanceThreshold = 8;
|
||||
|
||||
this.buildBackdrop();
|
||||
this.buildCenterLabels();
|
||||
|
|
@ -81,16 +86,40 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
}).setOrigin(0.5).setDepth(D.ui).setShadow(0, 2, '#000', 6);
|
||||
|
||||
this.buildButtons();
|
||||
|
||||
// Meld drop-zone highlight — drawn once in local space, repositioned during drag.
|
||||
this.meldHighlight = this.add.graphics().setDepth(D.zone + 1).setVisible(false);
|
||||
this.meldHighlight.fillStyle(THEME.brassHi, 0.13);
|
||||
this.meldHighlight.lineStyle(2.5, THEME.brassHi, 0.9);
|
||||
this.meldHighlight.fillRoundedRect(-72, -52, 144, 104, 14);
|
||||
this.meldHighlight.strokeRoundedRect(-72, -52, 144, 104, 14);
|
||||
|
||||
this.renderDynamic();
|
||||
this.advance();
|
||||
}
|
||||
|
||||
// ── Backdrop: luxe felt with a spotlight, burgundy rail and brass oval ────────
|
||||
buildBackdrop() {
|
||||
const pf = this.playfield;
|
||||
const hasPlayfield = pf?.key && this.textures.exists(pf.key);
|
||||
|
||||
if (hasPlayfield) {
|
||||
this.add.image(CX, GAME_HEIGHT / 2, pf.key)
|
||||
.setDisplaySize(GAME_WIDTH, GAME_HEIGHT)
|
||||
.setDepth(D.felt - 1);
|
||||
}
|
||||
|
||||
const g = this.add.graphics().setDepth(D.felt);
|
||||
g.fillGradientStyle(THEME.feltMid, THEME.feltMid, THEME.feltEdge, THEME.feltEdge, 1);
|
||||
g.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||
|
||||
if (hasPlayfield) {
|
||||
const maskShape = this.add.graphics().setVisible(false);
|
||||
maskShape.fillStyle(0xffffff);
|
||||
maskShape.fillEllipse(CX, CY + 30, 1640, 1180);
|
||||
g.setMask(new Phaser.Display.Masks.GeometryMask(this, maskShape));
|
||||
}
|
||||
|
||||
// Soft radial spotlight over the table centre (stacked translucent ellipses).
|
||||
const glow = this.add.graphics().setDepth(D.glow);
|
||||
for (let i = 8; i >= 1; i--) {
|
||||
|
|
@ -135,23 +164,29 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
// ── Team score panels ────────────────────────────────────────────────────────
|
||||
buildPanels() {
|
||||
this.panel = {};
|
||||
const make = (teamId, x, y, accent, title) => {
|
||||
const make = (teamId, x, y, accent, title, backSeat, frontSeat) => {
|
||||
const g = this.add.graphics().setDepth(D.ui);
|
||||
g.fillStyle(0x000000, 0.42); g.fillRoundedRect(x, y, 290, 96, 12);
|
||||
g.lineStyle(2, accent, 0.9); g.strokeRoundedRect(x, y, 290, 96, 12);
|
||||
this.add.text(x + 18, y + 14, title, {
|
||||
g.fillStyle(0x000000, 0.42); g.fillRoundedRect(x, y, 360, 114, 12);
|
||||
g.lineStyle(2, accent, 0.9); g.strokeRoundedRect(x, y, 360, 114, 12);
|
||||
|
||||
// Two overlapping portrait pictures left of the text content.
|
||||
const _p = [];
|
||||
this.addMiniPortrait(_p, backSeat, x + 24, y + 48, 20, D.ui + 1);
|
||||
this.addMiniPortrait(_p, frontSeat, x + 44, y + 64, 20, D.ui + 4);
|
||||
|
||||
this.add.text(x + 74, y + 14, title, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '18px', color: HEX(accent),
|
||||
}).setDepth(D.ui);
|
||||
const score = this.add.text(x + 18, y + 40, '0', {
|
||||
}).setDepth(D.ui + 7);
|
||||
const score = this.add.text(x + 74, y + 38, '0', {
|
||||
fontFamily: 'Righteous', fontSize: '40px', color: COLORS.textHex,
|
||||
}).setDepth(D.ui);
|
||||
const sub = this.add.text(x + 270, y + 54, '', {
|
||||
}).setDepth(D.ui + 7);
|
||||
const sub = this.add.text(x + 338, y + 82, '', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex,
|
||||
}).setOrigin(1, 0.5).setDepth(D.ui);
|
||||
}).setOrigin(1, 0.5).setDepth(D.ui + 7);
|
||||
this.panel[teamId] = { score, sub };
|
||||
};
|
||||
make(0, GAME_WIDTH - 320, GAME_HEIGHT - 130, THEME.team0, 'YOUR TEAM');
|
||||
make(1, GAME_WIDTH - 320, 30, THEME.team1, 'OPPONENTS');
|
||||
make(0, 1400, MELD_ZONE[0].y - 57 + 75, THEME.team0, 'YOUR TEAM', 2, 0);
|
||||
make(1, 200, MELD_ZONE[1].y - 57 - 75, THEME.team1, 'OPPONENTS', 3, 1);
|
||||
}
|
||||
|
||||
buildPortraits() {
|
||||
|
|
@ -182,6 +217,8 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
{ width: 220, height: 56, fontSize: 22 }).setDepth(D.ui);
|
||||
this.btn.meld = new Button(this, CX + 110, y, 'Meld', () => this.humanMeld(),
|
||||
{ width: 200, height: 56, fontSize: 22 }).setDepth(D.ui);
|
||||
this.btn.reset = new Button(this, CX - 110, y, 'Reset Turn', () => this.humanResetTurn(),
|
||||
{ variant: 'ghost', width: 200, height: 56, fontSize: 20 }).setDepth(D.ui);
|
||||
this.btn.discard = new Button(this, CX + 330, y, 'Discard', () => this.humanDiscard(),
|
||||
{ width: 200, height: 56, fontSize: 22 }).setDepth(D.ui);
|
||||
this.btn.next = new Button(this, CX, y, 'Next Hand', () => this.onNextHand(),
|
||||
|
|
@ -192,7 +229,7 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
hideActionButtons() {
|
||||
['draw', 'take', 'meld', 'discard'].forEach((k) => this.btn[k].setVisible(false));
|
||||
['draw', 'take', 'meld', 'reset', 'discard'].forEach((k) => this.btn[k].setVisible(false));
|
||||
}
|
||||
|
||||
// ── Card rendering ───────────────────────────────────────────────────────────
|
||||
|
|
@ -252,25 +289,45 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
|
||||
renderPiles() {
|
||||
const s = this.state;
|
||||
const humanDrawPhase = s.phase === 'draw' && s.currentPlayer === 0;
|
||||
|
||||
// Stock as a neat stack of backs.
|
||||
const n = Math.min(6, Math.ceil(s.stock.length / 12));
|
||||
let stockTop = null;
|
||||
for (let i = 0; i < n; i++) {
|
||||
this.pileObjs.push(this.makeCard(null, STOCK.x - i * 2, STOCK.y - i * 2, { faceUp: false, depth: D.pile + i }));
|
||||
const c = this.makeCard(null, STOCK.x - i * 2, STOCK.y - i * 2, { faceUp: false, depth: D.pile + i });
|
||||
this.pileObjs.push(c);
|
||||
stockTop = c;
|
||||
}
|
||||
this.stockCount.setText(`${s.stock.length} left`);
|
||||
|
||||
if (humanDrawPhase && stockTop) {
|
||||
this.makeInteractive(stockTop, () => this.humanDraw('stock'));
|
||||
}
|
||||
|
||||
// Discard pile: a slightly fanned stack, top card face up.
|
||||
const dn = Math.min(5, s.discard.length);
|
||||
let discardTop = null;
|
||||
for (let i = 0; i < dn; i++) {
|
||||
const card = s.discard[s.discard.length - dn + i];
|
||||
const top = i === dn - 1;
|
||||
this.pileObjs.push(this.makeCard(card, DISCARD.x + (i - dn) * 3, DISCARD.y + (i - dn) * 2, { faceUp: top, depth: D.pile + i }));
|
||||
const c = this.makeCard(card, DISCARD.x + (i - dn) * 3, DISCARD.y + (i - dn) * 2, { faceUp: top, depth: D.pile + i });
|
||||
this.pileObjs.push(c);
|
||||
if (top) discardTop = c;
|
||||
}
|
||||
this.frozenBadge.setVisible(s.frozen);
|
||||
|
||||
if (humanDrawPhase && discardTop) {
|
||||
const plan = takePlan(s, 0);
|
||||
const t = s.teams[0];
|
||||
const legal = !!plan && (t.hasMelded || takeYieldsLegalTurn(s, 0, 5, plan));
|
||||
if (legal) this.makeInteractive(discardTop, () => this.humanDraw('take'));
|
||||
}
|
||||
}
|
||||
|
||||
renderHands() {
|
||||
const s = this.state;
|
||||
const meldPhase = s.phase === 'meld' && s.currentPlayer === 0;
|
||||
// Human hand — face up, fanned, sorted for readability.
|
||||
const me = s.players[0].hand.slice().sort(this.handSort);
|
||||
const total = (me.length - 1) * HAND_GAP + CARD_W;
|
||||
|
|
@ -280,7 +337,53 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
const c = this.makeCard(card, x, SEAT[0].hand.y - (sel ? 26 : 0), { depth: D.card + i });
|
||||
c.baseY = SEAT[0].hand.y;
|
||||
if (isRedThree(card)) this.glowCard(c);
|
||||
this.makeInteractive(c, () => this.toggleSelect(card.id));
|
||||
if (meldPhase) {
|
||||
c.setSize(CARD_W, CARD_H);
|
||||
c.setInteractive({ useHandCursor: true });
|
||||
this.input.setDraggable(c);
|
||||
let dragged = false;
|
||||
c.on('dragstart', () => {
|
||||
dragged = true;
|
||||
c._ox = c.x; c._oy = c.y;
|
||||
c.setDepth(D.card + 50);
|
||||
});
|
||||
c.on('drag', (pointer, dragX, dragY) => {
|
||||
c.x = dragX; c.y = dragY;
|
||||
const zone = this.meldDropZones.find(z => Math.abs(dragX - z.x) < 75 && Math.abs(dragY - z.y) < 65);
|
||||
if (zone) {
|
||||
this.meldHighlight.setVisible(true).setPosition(zone.x, zone.y);
|
||||
} else {
|
||||
this.meldHighlight.setVisible(false);
|
||||
}
|
||||
});
|
||||
c.on('dragend', () => {
|
||||
this.meldHighlight.setVisible(false);
|
||||
c.setDepth(D.card + i);
|
||||
const dropX = c.x, dropY = c.y;
|
||||
c.x = c._ox; c.y = c._oy;
|
||||
// Check meld drop zones first.
|
||||
const zone = this.meldDropZones.find(z => Math.abs(dropX - z.x) < 75 && Math.abs(dropY - z.y) < 65);
|
||||
if (zone) {
|
||||
const next = meld(this.state, zone.rank, [card.id]);
|
||||
if (next !== this.state) {
|
||||
this.state = next;
|
||||
playSound(this, SFX.CARD_PLACE);
|
||||
this.renderDynamic();
|
||||
this.updateMeldPhaseUi();
|
||||
} else {
|
||||
this.toast('Cannot add that card to this meld', CX, 560, '#e06c75');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Math.abs(dropX - DISCARD.x) < 80 && Math.abs(dropY - DISCARD.y) < 100) {
|
||||
this.selected = new Set([card.id]);
|
||||
this.humanDiscard({ animate: false });
|
||||
}
|
||||
});
|
||||
c.on('pointerup', () => { if (!dragged) this.toggleSelect(card.id); dragged = false; });
|
||||
} else {
|
||||
this.makeInteractive(c, () => this.toggleSelect(card.id));
|
||||
}
|
||||
this.handObjs[0].push(c);
|
||||
});
|
||||
|
||||
|
|
@ -304,6 +407,8 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
renderMelds() {
|
||||
this.meldDropZones = [];
|
||||
const meldPhase = this.state.phase === 'meld' && this.state.currentPlayer === 0;
|
||||
for (const teamId of [0, 1]) {
|
||||
const melds = this.state.teams[teamId].melds;
|
||||
const reds = this.state.teams[teamId].redThrees.length;
|
||||
|
|
@ -311,7 +416,11 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
const groupW = 150;
|
||||
const totalW = Math.max(1, melds.length + (reds ? 1 : 0)) * groupW;
|
||||
let gx = CX - totalW / 2 + groupW / 2;
|
||||
for (const m of melds) { this.renderMeldGroup(m, gx, y); gx += groupW; }
|
||||
for (const m of melds) {
|
||||
this.renderMeldGroup(m, gx, y, teamId === 0 && meldPhase);
|
||||
if (teamId === 0 && meldPhase) this.meldDropZones.push({ rank: m.rank, x: gx, y });
|
||||
gx += groupW;
|
||||
}
|
||||
if (reds) {
|
||||
const badge = this.add.container(gx, y).setDepth(D.zone);
|
||||
const g = this.add.graphics();
|
||||
|
|
@ -325,7 +434,7 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
}
|
||||
}
|
||||
|
||||
renderMeldGroup(m, x, y) {
|
||||
renderMeldGroup(m, x, y, showScore = false) {
|
||||
const cont = this.add.container(x, y).setDepth(D.zone);
|
||||
const canasta = isCanasta(m);
|
||||
if (canasta) {
|
||||
|
|
@ -347,6 +456,14 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
fontFamily: 'Righteous', fontSize: canasta ? '14px' : '15px',
|
||||
color: canasta ? HEX(isNaturalCanasta(m) ? THEME.brassHi : 0xcfd6e0) : COLORS.mutedHex,
|
||||
}).setOrigin(0.5));
|
||||
if (showScore) {
|
||||
const cardVal = m.cards.reduce((s, c) => s + cardScore(c), 0);
|
||||
const bonus = canasta ? (isNaturalCanasta(m) ? NATURAL_CANASTA : MIXED_CANASTA) : 0;
|
||||
const scoreLabel = bonus > 0 ? `+${bonus}+${cardVal}` : `+${cardVal}`;
|
||||
cont.add(this.add.text(0, 55, scoreLabel, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '12px', color: '#90c878',
|
||||
}).setOrigin(0.5));
|
||||
}
|
||||
this.meldObjs.push(cont);
|
||||
}
|
||||
|
||||
|
|
@ -382,6 +499,55 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
c.on('pointerdown', handler);
|
||||
}
|
||||
|
||||
// ── Draw animation ──────────────────────────────────────────────────────────
|
||||
// Animates a single card flying from (fromX,fromY) to (toX,toY) over 1200 ms.
|
||||
// faceUp: starting face state. flip: if true, scaleX collapses at 600 ms,
|
||||
// the container is redrawn face-up, then scaleX expands back to 1.
|
||||
animateDraw({ fromX, fromY, toX, toY, card, faceUp = true, flip = false, duration = 1200, startDelay = 0 }) {
|
||||
return new Promise(resolve => {
|
||||
const anim = this.makeCard(card, fromX, fromY, { faceUp, depth: D.card + 100 });
|
||||
const half = duration / 2;
|
||||
|
||||
this.tweens.add({
|
||||
targets: anim,
|
||||
x: toX, y: toY,
|
||||
duration,
|
||||
delay: startDelay,
|
||||
ease: 'Power2.easeInOut',
|
||||
onComplete: () => { anim.destroy(); resolve(); },
|
||||
});
|
||||
|
||||
if (flip) {
|
||||
// Fold (scaleX → 0) during first half, swap to face-up, unfold during second half.
|
||||
this.tweens.add({
|
||||
targets: anim,
|
||||
scaleX: 0,
|
||||
duration: half,
|
||||
delay: startDelay,
|
||||
ease: 'Sine.easeIn',
|
||||
onComplete: () => {
|
||||
anim.removeAll(true);
|
||||
this.drawFace(anim, card, true);
|
||||
this.tweens.add({ targets: anim, scaleX: 1, duration: half, ease: 'Sine.easeOut' });
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getMeldGroupPos(teamId, rank) {
|
||||
const melds = this.state.teams[teamId].melds;
|
||||
const reds = this.state.teams[teamId].redThrees.length;
|
||||
const groupW = 150;
|
||||
const totalW = Math.max(1, melds.length + (reds ? 1 : 0)) * groupW;
|
||||
let gx = CX - totalW / 2 + groupW / 2;
|
||||
for (const m of melds) {
|
||||
if (m.rank === rank) return { x: gx, y: MELD_ZONE[teamId].y };
|
||||
gx += groupW;
|
||||
}
|
||||
return { x: CX, y: MELD_ZONE[teamId].y };
|
||||
}
|
||||
|
||||
// ── Turn flow ────────────────────────────────────────────────────────────────
|
||||
advance() {
|
||||
this.updateTurnRing();
|
||||
|
|
@ -403,38 +569,68 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
|
||||
// Draw.
|
||||
const draw = chooseDraw(this.state, seat, skill);
|
||||
let drawnCard, drawFromX, drawFromY, drawFaceUp;
|
||||
if (draw.type === 'take') {
|
||||
drawnCard = this.state.discard[this.state.discard.length - 1];
|
||||
drawFromX = DISCARD.x; drawFromY = DISCARD.y; drawFaceUp = true;
|
||||
this.state = takeDiscard(this.state, draw.plan);
|
||||
this.toast(`${this.seatName[seat]} takes the pile`, DISCARD.x, DISCARD.y - 80);
|
||||
playSound(this, SFX.CARD_SHOW);
|
||||
} else {
|
||||
drawnCard = this.state.stock[0];
|
||||
drawFromX = STOCK.x; drawFromY = STOCK.y; drawFaceUp = false;
|
||||
this.state = drawStock(this.state);
|
||||
playSound(this, SFX.CARD_DEAL);
|
||||
}
|
||||
if (drawnCard && !['handOver', 'gameOver'].includes(this.state.phase)) {
|
||||
await this.animateDraw({
|
||||
fromX: drawFromX, fromY: drawFromY,
|
||||
toX: SEAT[seat].hand.x, toY: SEAT[seat].hand.y,
|
||||
card: drawnCard, faceUp: drawFaceUp, flip: false,
|
||||
});
|
||||
}
|
||||
this.renderDynamic();
|
||||
if (['handOver', 'gameOver'].includes(this.state.phase)) { this.busy = false; return this.advance(); }
|
||||
await this.delay(420);
|
||||
await this.delay(750);
|
||||
|
||||
// Meld.
|
||||
const { actions } = chooseMelds(this.state, seat, skill);
|
||||
const teamId = TEAM_OF_SEAT[seat];
|
||||
for (const a of actions) {
|
||||
const before = this.state.teams[TEAM_OF_SEAT[seat]].melds.find((m) => m.rank === a.rank);
|
||||
const before = this.state.teams[teamId].melds.find((m) => m.rank === a.rank);
|
||||
const wasCanasta = before ? isCanasta(before) : false;
|
||||
const meldCards = a.cardIds.map(id => this.state.players[seat].hand.find(c => c.id === id)).filter(Boolean);
|
||||
this.state = meld(this.state, a.rank, a.cardIds);
|
||||
const after = this.state.teams[TEAM_OF_SEAT[seat]].melds.find((m) => m.rank === a.rank);
|
||||
const after = this.state.teams[teamId].melds.find((m) => m.rank === a.rank);
|
||||
const meldPos = this.getMeldGroupPos(teamId, a.rank);
|
||||
const duration = before ? 750 : 500;
|
||||
const stagger = before ? 80 : 60;
|
||||
await Promise.all(meldCards.map((card, i) =>
|
||||
this.animateDraw({
|
||||
fromX: SEAT[seat].hand.x, fromY: SEAT[seat].hand.y,
|
||||
toX: meldPos.x, toY: meldPos.y,
|
||||
card, faceUp: false, flip: true, duration, startDelay: i * stagger,
|
||||
})
|
||||
));
|
||||
this.renderDynamic();
|
||||
playSound(this, SFX.CARD_PLACE);
|
||||
if (after && isCanasta(after) && !wasCanasta) this.toast('CANASTA!', CX, MELD_ZONE[TEAM_OF_SEAT[seat]].y - 70, '#f0d77a');
|
||||
await this.delay(360);
|
||||
if (after && isCanasta(after) && !wasCanasta) this.toast('CANASTA!', CX, MELD_ZONE[teamId].y - 70, '#f0d77a');
|
||||
}
|
||||
|
||||
// Discard (ends the turn, or the hand if it empties).
|
||||
if (this.state.phase === 'meld') {
|
||||
const cardId = chooseDiscard(this.state, seat, skill);
|
||||
const discardCard = this.state.players[seat].hand.find((c) => c.id === cardId);
|
||||
this.state = discard(this.state, cardId);
|
||||
if (discardCard) {
|
||||
await this.animateDraw({
|
||||
fromX: SEAT[seat].hand.x, fromY: SEAT[seat].hand.y,
|
||||
toX: DISCARD.x, toY: DISCARD.y,
|
||||
card: discardCard, faceUp: false, flip: true, duration: 1000,
|
||||
});
|
||||
}
|
||||
this.renderDynamic();
|
||||
playSound(this, SFX.CARD_PLACE);
|
||||
await this.delay(360);
|
||||
}
|
||||
|
||||
this.busy = false;
|
||||
|
|
@ -445,6 +641,7 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
enableHumanTurn() {
|
||||
this.busy = false;
|
||||
const s = this.state;
|
||||
if (s.phase === 'meld') this.turnStartState = this.state;
|
||||
if (s.phase === 'draw') {
|
||||
this.statusText.setText('Draw from the stock, or take the discard pile');
|
||||
this.btn.draw.setVisible(true).setEnabled(true);
|
||||
|
|
@ -462,13 +659,20 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
updateMeldPhaseUi() {
|
||||
const s = this.state;
|
||||
const t = s.teams[0];
|
||||
const need = !t.hasMelded;
|
||||
this.statusText.setText(need
|
||||
? `Select cards to meld (first meld must total ${this.minNeeded()} pts), then discard one`
|
||||
: 'Meld more, or select one card and discard');
|
||||
const noCanasta = s.players[0].hand.length === 1 && !teamHasCanasta(s, 0);
|
||||
let statusMsg;
|
||||
if (noCanasta) {
|
||||
statusMsg = 'No canasta yet — discarding will end the hand early';
|
||||
} else if (!t.hasMelded) {
|
||||
statusMsg = `Select cards to meld (first meld must total ${this.minNeeded()} pts), then discard one`;
|
||||
} else {
|
||||
statusMsg = 'Meld more, or select one card and discard';
|
||||
}
|
||||
this.statusText.setText(statusMsg);
|
||||
this.btn.draw.setVisible(false);
|
||||
this.btn.take.setVisible(false);
|
||||
this.btn.meld.setVisible(true).setEnabled(this.selected.size >= 1);
|
||||
this.btn.reset.setVisible(s.meldedThisTurn.length > 0);
|
||||
this.btn.discard.setVisible(true).setEnabled(this.selected.size === 1 && this.discardAllowed());
|
||||
}
|
||||
|
||||
|
|
@ -480,8 +684,6 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
// Mirror the engine: an under-minimum initial meld can't be closed with a discard.
|
||||
const t = this.state.teams[0];
|
||||
if (!t.hasMelded && this.state.meldedThisTurn.length > 0 && !meetsInitialMeld(this.state, 0)) return false;
|
||||
// Going out requires a canasta.
|
||||
if (this.state.players[0].hand.length === 1 && !teamHasCanasta(this.state, 0)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -496,19 +698,42 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
this.updateMeldPhaseUi();
|
||||
}
|
||||
|
||||
humanDraw(kind) {
|
||||
async humanDraw(kind) {
|
||||
if (this.busy) return;
|
||||
this.busy = true;
|
||||
this.hideActionButtons();
|
||||
|
||||
let drawnCard, fromX, fromY, faceUp, flip;
|
||||
if (kind === 'stock') {
|
||||
drawnCard = this.state.stock[0];
|
||||
fromX = STOCK.x; fromY = STOCK.y; faceUp = false; flip = true;
|
||||
this.state = drawStock(this.state);
|
||||
playSound(this, SFX.CARD_DEAL);
|
||||
} else {
|
||||
drawnCard = this.state.discard[this.state.discard.length - 1];
|
||||
fromX = DISCARD.x; fromY = DISCARD.y; faceUp = true; flip = false;
|
||||
const plan = takePlan(this.state, 0);
|
||||
if (!plan) return;
|
||||
if (!plan) { this.busy = false; return; }
|
||||
this.state = takeDiscard(this.state, plan);
|
||||
playSound(this, SFX.CARD_SHOW);
|
||||
}
|
||||
|
||||
// Compute where the card will land in the human hand after the state update.
|
||||
// For discard take, the top card goes to a meld (not hand), so idx = -1 → fallback centre.
|
||||
const me = this.state.players[0].hand.slice().sort(this.handSort);
|
||||
const total = (me.length - 1) * HAND_GAP + CARD_W;
|
||||
const idx = drawnCard ? me.findIndex(c => c.id === drawnCard.id) : -1;
|
||||
const toX = idx >= 0
|
||||
? SEAT[0].hand.x - total / 2 + CARD_W / 2 + idx * HAND_GAP
|
||||
: SEAT[0].hand.x;
|
||||
const toY = SEAT[0].hand.y;
|
||||
|
||||
this.selected.clear();
|
||||
if (drawnCard) {
|
||||
await this.animateDraw({ fromX, fromY, toX, toY, card: drawnCard, faceUp, flip });
|
||||
}
|
||||
this.renderDynamic();
|
||||
this.busy = false;
|
||||
this.advance();
|
||||
}
|
||||
|
||||
|
|
@ -522,7 +747,7 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
if (!rank) {
|
||||
// All wilds — only valid as an addition to an existing single team meld.
|
||||
const melds = this.state.teams[0].melds;
|
||||
if (melds.length !== 1) return this.toast('Add wilds to a specific meld', CX, 560, '#e06c75');
|
||||
if (melds.length !== 1) return this.toast('Drag wild cards onto a specific meld', CX, 560, '#e06c75');
|
||||
rank = melds[0].rank;
|
||||
}
|
||||
const next = meld(this.state, rank, ids);
|
||||
|
|
@ -534,27 +759,109 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
this.updateMeldPhaseUi();
|
||||
}
|
||||
|
||||
humanDiscard() {
|
||||
humanResetTurn() {
|
||||
if (this.busy || !this.turnStartState) return;
|
||||
this.state = this.turnStartState;
|
||||
this.selected.clear();
|
||||
playSound(this, SFX.CARD_PLACE);
|
||||
this.renderDynamic();
|
||||
this.updateMeldPhaseUi();
|
||||
}
|
||||
|
||||
async humanDiscard({ animate = true } = {}) {
|
||||
if (this.busy || this.selected.size !== 1) return;
|
||||
if (!this.discardAllowed()) {
|
||||
return this.toast(`Initial meld needs ${this.minNeeded()} points`, CX, 560, '#e06c75');
|
||||
}
|
||||
this.busy = true;
|
||||
this.hideActionButtons();
|
||||
|
||||
const id = [...this.selected][0];
|
||||
const card = this.state.players[0].hand.find((c) => c.id === id);
|
||||
|
||||
// Compute card's current hand position before state changes.
|
||||
const me = this.state.players[0].hand.slice().sort(this.handSort);
|
||||
const total = (me.length - 1) * HAND_GAP + CARD_W;
|
||||
const idx = me.findIndex((c) => c.id === id);
|
||||
const fromX = SEAT[0].hand.x - total / 2 + CARD_W / 2 + idx * HAND_GAP;
|
||||
const fromY = SEAT[0].hand.y;
|
||||
|
||||
const goingOut = this.state.players[0].hand.length === 1;
|
||||
this.state = discard(this.state, id);
|
||||
this.turnStartState = null;
|
||||
this.selected.clear();
|
||||
|
||||
if (animate && card) {
|
||||
await this.animateDraw({
|
||||
fromX, fromY, toX: DISCARD.x, toY: DISCARD.y,
|
||||
card, faceUp: true, flip: false, duration: 1000,
|
||||
});
|
||||
}
|
||||
|
||||
playSound(this, SFX.CARD_PLACE);
|
||||
if (goingOut) this.toast('You go out!', CX, 560, '#f0d77a');
|
||||
if (goingOut && teamHasCanasta(this.state, 0)) this.toast('You go out!', CX, 560, '#f0d77a');
|
||||
this.renderDynamic();
|
||||
this.busy = false;
|
||||
this.advance();
|
||||
}
|
||||
|
||||
// ── Hand-over & game-over ────────────────────────────────────────────────────
|
||||
addMiniPortrait(layer, seat, cx, cy, radius, depth) {
|
||||
const size = radius * 2;
|
||||
const backing = this.add.graphics().setDepth(depth);
|
||||
backing.fillStyle(0x1a1208, 1);
|
||||
backing.fillCircle(cx, cy, radius);
|
||||
layer.push(backing);
|
||||
|
||||
if (seat === 0) {
|
||||
const avatarKey = Object.keys(this.textures.list).find(k => k.startsWith('player-avatar-'));
|
||||
if (avatarKey) {
|
||||
const maskG = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
maskG.fillStyle(0xffffff); maskG.fillCircle(cx, cy, radius);
|
||||
layer.push(this.add.image(cx, cy, avatarKey).setDisplaySize(size, size)
|
||||
.setMask(maskG.createGeometryMask()).setDepth(depth + 1));
|
||||
} else {
|
||||
// Show initial while avatar loads; swap it in when the texture arrives.
|
||||
const placeholder = this.add.text(cx, cy, 'Y', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: `${Math.round(radius * 0.85)}px`,
|
||||
color: HEX(THEME.brassHi),
|
||||
}).setOrigin(0.5).setDepth(depth + 1);
|
||||
layer.push(placeholder);
|
||||
const onTex = (key) => {
|
||||
if (!key.startsWith('player-avatar-')) return;
|
||||
this.textures.off('addtexture', onTex);
|
||||
if (!placeholder.active) return;
|
||||
placeholder.destroy();
|
||||
const maskG = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
maskG.fillStyle(0xffffff); maskG.fillCircle(cx, cy, radius);
|
||||
this.add.image(cx, cy, key).setDisplaySize(size, size)
|
||||
.setMask(maskG.createGeometryMask()).setDepth(depth + 1);
|
||||
};
|
||||
this.textures.on('addtexture', onTex);
|
||||
}
|
||||
} else {
|
||||
const opp = this.seatOpp[seat];
|
||||
if (this.textures.exists('opponents')) {
|
||||
const maskG = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
maskG.fillStyle(0xffffff); maskG.fillCircle(cx, cy, radius);
|
||||
layer.push(this.add.image(cx, cy, 'opponents', opp?.spriteIndex ?? 0)
|
||||
.setDisplaySize(size, size)
|
||||
.setMask(maskG.createGeometryMask())
|
||||
.setDepth(depth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
const ring = this.add.graphics().setDepth(depth + 2);
|
||||
ring.lineStyle(2.5, THEME.rail, 1); ring.strokeCircle(cx, cy, radius);
|
||||
layer.push(ring);
|
||||
}
|
||||
|
||||
showHandOver() {
|
||||
this.hideActionButtons();
|
||||
this.updateTurnRing();
|
||||
const scores = this.state.handScores;
|
||||
const w = 760, h = 420, x = CX - w / 2, y = CY - h / 2 + 20;
|
||||
const w = 840, h = 540, x = CX - w / 2, y = CY - h / 2 + 20;
|
||||
const halfW = w / 2; // 420
|
||||
const layer = [];
|
||||
const g = this.add.graphics().setDepth(D.modal);
|
||||
g.fillStyle(0x000000, 0.6); g.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||
|
|
@ -563,11 +870,21 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
layer.push(g);
|
||||
layer.push(this.add.text(CX, y + 36, 'Hand Over', { fontFamily: 'Righteous', fontSize: '40px', color: HEX(THEME.brassHi) }).setOrigin(0.5).setDepth(D.modalUI));
|
||||
|
||||
// Portrait radius and diagonal offset for the overlapping pair
|
||||
const PR = 22;
|
||||
const names = ['YOUR TEAM', 'OPPONENTS'];
|
||||
[0, 1].forEach((teamId, idx) => {
|
||||
const d = scores[idx];
|
||||
const col = idx === 0 ? THEME.team0 : THEME.team1;
|
||||
const px = x + 60 + idx * (w / 2 - 20);
|
||||
const colX = x + idx * halfW;
|
||||
const px = colX + 95;
|
||||
const portMidY = y + 215;
|
||||
|
||||
// Two portraits overlapping diagonally — back (up-left) then front (down-right)
|
||||
const [backSeat, frontSeat] = teamId === 0 ? [2, 0] : [3, 1];
|
||||
this.addMiniPortrait(layer, backSeat, colX + 36, portMidY - 12, PR, D.modalUI);
|
||||
this.addMiniPortrait(layer, frontSeat, colX + 58, portMidY + 10, PR, D.modalUI + 2);
|
||||
|
||||
layer.push(this.add.text(px, y + 92, names[idx], { fontFamily: '"Julius Sans One"', fontSize: '22px', color: HEX(col) }).setDepth(D.modalUI));
|
||||
const lines = [
|
||||
`Melds ${d.meldPoints}`,
|
||||
|
|
@ -583,8 +900,9 @@ export default class CanastaGame extends Phaser.Scene {
|
|||
fontFamily: 'Righteous', fontSize: '20px', color: COLORS.textHex, lineSpacing: 8,
|
||||
}).setDepth(D.modalUI));
|
||||
});
|
||||
layer.push(new Button(this, CX, y + h - 52, 'Next Round', () => this.onNextHand(),
|
||||
{ width: 280, height: 56, fontSize: 24 }).setDepth(D.modalUI));
|
||||
this.handOverLayer = layer;
|
||||
this.btn.next.setVisible(true);
|
||||
}
|
||||
|
||||
onNextHand() {
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ export default class CribbageGame extends Phaser.Scene {
|
|||
fontFamily: 'Righteous', fontSize: '30px', color: '#f0d77a',
|
||||
}).setOrigin(0.5).setDepth(D.ui);
|
||||
|
||||
this.statusText = this.add.text(CENTER_X, 412, '', {
|
||||
this.statusText = this.add.text(CENTER_X, PLAYER_HAND_Y - 100, '', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '28px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5).setDepth(D.ui);
|
||||
|
||||
|
|
@ -382,8 +382,10 @@ export default class CribbageGame extends Phaser.Scene {
|
|||
playSound(this, SFX.CARD_SHOW);
|
||||
await this.delay(300);
|
||||
// flip
|
||||
this.tweens.add({ targets: this.starterSprite, scaleX: 0, duration: 140, yoyo: true,
|
||||
onYoyo: () => { this.starterSprite.removeAll(true); this.drawFace(this.starterSprite, starter, true); } });
|
||||
this.tweens.add({
|
||||
targets: this.starterSprite, scaleX: 0, duration: 140, yoyo: true,
|
||||
onYoyo: () => { this.starterSprite.removeAll(true); this.drawFace(this.starterSprite, starter, true); }
|
||||
});
|
||||
await this.delay(360);
|
||||
if (heels) {
|
||||
this.floatScore(this.logic.dealer, 2, 'His Heels!');
|
||||
|
|
@ -484,8 +486,10 @@ export default class CribbageGame extends Phaser.Scene {
|
|||
this.countText.setText('Count: 0');
|
||||
const going = this.pegSprites;
|
||||
this.pegSprites = [];
|
||||
going.forEach((c) => this.tweens.add({ targets: c, y: PEG_ROW_Y + 120, alpha: 0, duration: 300,
|
||||
onComplete: () => c.destroy() }));
|
||||
going.forEach((c) => this.tweens.add({
|
||||
targets: c, y: PEG_ROW_Y + 120, alpha: 0, duration: 300,
|
||||
onComplete: () => c.destroy()
|
||||
}));
|
||||
await this.delay(340);
|
||||
}
|
||||
|
||||
|
|
@ -499,8 +503,10 @@ export default class CribbageGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
labelFor(reasons) {
|
||||
const map = { '15': 'Fifteen', '31': 'Thirty-one', pair: 'Pair', 'pair-royal': 'Three!', 'double-pair-royal': 'Four!',
|
||||
run3: 'Run', run4: 'Run', run5: 'Run', run6: 'Run', run7: 'Run', go: 'Go', 'last-card': 'Last card' };
|
||||
const map = {
|
||||
'15': 'Fifteen', '31': 'Thirty-one', pair: 'Pair', 'pair-royal': 'Three!', 'double-pair-royal': 'Four!',
|
||||
run3: 'Run', run4: 'Run', run5: 'Run', run6: 'Run', run7: 'Run', go: 'Go', 'last-card': 'Last card'
|
||||
};
|
||||
return (reasons || []).map((r) => map[r] || r).join(' + ');
|
||||
}
|
||||
|
||||
|
|
@ -510,8 +516,10 @@ export default class CribbageGame extends Phaser.Scene {
|
|||
const txt = this.add.text(pt.x, pt.y - 18, `+${points}${label ? ' ' + label : ''}`, {
|
||||
fontFamily: 'Righteous', fontSize: '24px', color: player === 0 ? '#f07a6d' : '#fff4d8',
|
||||
}).setOrigin(0.5).setDepth(D.toast).setShadow(0, 0, 'rgba(255,207,74,0.85)', 12);
|
||||
this.tweens.add({ targets: txt, y: pt.y - 64, alpha: 0, duration: 1100, ease: 'Cubic.easeOut',
|
||||
onComplete: () => txt.destroy() });
|
||||
this.tweens.add({
|
||||
targets: txt, y: pt.y - 64, alpha: 0, duration: 1100, ease: 'Cubic.easeOut',
|
||||
onComplete: () => txt.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
// ── The show ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -526,8 +534,10 @@ export default class CribbageGame extends Phaser.Scene {
|
|||
const card = this.logic.keep[1][i];
|
||||
if (!card) return;
|
||||
c.cardRef = card;
|
||||
this.tweens.add({ targets: c, scaleX: 0, duration: 120, yoyo: true,
|
||||
onYoyo: () => { c.removeAll(true); this.drawFace(c, card, true); } });
|
||||
this.tweens.add({
|
||||
targets: c, scaleX: 0, duration: 120, yoyo: true,
|
||||
onYoyo: () => { c.removeAll(true); this.drawFace(c, card, true); }
|
||||
});
|
||||
});
|
||||
await this.delay(500);
|
||||
|
||||
|
|
@ -571,11 +581,14 @@ export default class CribbageGame extends Phaser.Scene {
|
|||
g.fillStyle(THEME.woodDark, 0.96); g.fillRoundedRect(cx - w / 2, cy - h / 2, w, h, 14);
|
||||
g.lineStyle(3, THEME.brass, 0.9); g.strokeRoundedRect(cx - w / 2, cy - h / 2, w, h, 14);
|
||||
const t1 = this.add.text(cx, cy - 34, `${o.title} — ${o.isCrib ? 'CRIB' : 'HAND'}`, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.goldHex }).setOrigin(0.5).setDepth(D.modalUI);
|
||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.goldHex
|
||||
}).setOrigin(0.5).setDepth(D.modalUI);
|
||||
const t2 = this.add.text(cx, cy + 2, body, {
|
||||
fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex }).setOrigin(0.5).setDepth(D.modalUI);
|
||||
fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex
|
||||
}).setOrigin(0.5).setDepth(D.modalUI);
|
||||
const t3 = this.add.text(cx, cy + 40, `${r.total} point${r.total === 1 ? '' : 's'}`, {
|
||||
fontFamily: 'Righteous', fontSize: '30px', color: '#f0d77a' }).setOrigin(0.5).setDepth(D.modalUI)
|
||||
fontFamily: 'Righteous', fontSize: '30px', color: '#f0d77a'
|
||||
}).setOrigin(0.5).setDepth(D.modalUI)
|
||||
.setShadow(0, 0, 'rgba(255,207,74,0.8)', 12);
|
||||
this.countPanel = [g, t1, t2, t3];
|
||||
}
|
||||
|
|
@ -617,10 +630,12 @@ export default class CribbageGame extends Phaser.Scene {
|
|||
g.lineStyle(4, THEME.brass, 1); g.strokeRoundedRect(GAME_WIDTH / 2 - 360, 360, 720, 360, 24);
|
||||
|
||||
this.add.text(GAME_WIDTH / 2, 450, won ? 'You Win!' : `${this.aiName} Wins`, {
|
||||
fontFamily: 'Righteous', fontSize: '60px', color: won ? '#f0d77a' : '#e9e1cf' })
|
||||
fontFamily: 'Righteous', fontSize: '60px', color: won ? '#f0d77a' : '#e9e1cf'
|
||||
})
|
||||
.setOrigin(0.5).setDepth(D.modalUI);
|
||||
this.add.text(GAME_WIDTH / 2, 530, `${this.logic.scores[0]} – ${this.logic.scores[1]}${skunk ? ' (Skunk!)' : ''}`, {
|
||||
fontFamily: 'Righteous', fontSize: '40px', color: COLORS.textHex })
|
||||
fontFamily: 'Righteous', fontSize: '40px', color: COLORS.textHex
|
||||
})
|
||||
.setOrigin(0.5).setDepth(D.modalUI);
|
||||
new Button(this, GAME_WIDTH / 2, 650, 'Back to Menu', () => this.scene.start('GameMenu'),
|
||||
{ width: 300, fontSize: 26 }).setDepth(D.modalUI);
|
||||
|
|
|
|||
Loading…
Reference in New Issue