feat: add opponent hand visualization and update AI draw animations
- Introduce mini face-down card fans below opponent portraits to show hand size. - Modify AI draw animations to target specific slots within the mini hand fan. - Adjust layout constants (OPP_STEP) and text positions to accommodate the new UI. - Simplify opponent info text to remove redundant card counts.
This commit is contained in:
parent
402293fea2
commit
ae6c69e25c
|
|
@ -28,8 +28,9 @@ const TTR_CARD_FRAME = {
|
||||||
|
|
||||||
// Right-band + bottom-strip layout (1920×1080). The map occupies x<1400.
|
// Right-band + bottom-strip layout (1920×1080). The map occupies x<1400.
|
||||||
const RB = { x0: 1408, cx: 1660, w: 512 };
|
const RB = { x0: 1408, cx: 1660, w: 512 };
|
||||||
const OPP_COL1_X = 1450, OPP_COL2_X = 1706, OPP_Y0 = 150, OPP_STEP = 100, OPP_R = 32;
|
const OPP_COL1_X = 1450, OPP_COL2_X = 1706, OPP_Y0 = 150, OPP_STEP = 150, OPP_R = 32;
|
||||||
const MK_X = 1476, MK_Y0 = 507, MK_STEP = 104, CARD_W = 78, CARD_H = 112;
|
const MK_X = 1476, MK_Y0 = 507, MK_STEP = 104, CARD_W = 78, CARD_H = 112;
|
||||||
|
const MINI_W = 22, MINI_H = 32, MINI_STEP = 14;
|
||||||
const PILE_X = 1646;
|
const PILE_X = 1646;
|
||||||
const BOT_Y = 980;
|
const BOT_Y = 980;
|
||||||
|
|
||||||
|
|
@ -51,6 +52,8 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
this.playerScoreText = null;
|
this.playerScoreText = null;
|
||||||
this.scoreBadgeContainers = [];
|
this.scoreBadgeContainers = [];
|
||||||
this.oppScoreTexts = {};
|
this.oppScoreTexts = {};
|
||||||
|
this.oppMiniHandGfx = [];
|
||||||
|
this.oppMiniHandCount = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -196,6 +199,10 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
this.scoreBadgeContainers.forEach((c) => c.destroy());
|
this.scoreBadgeContainers.forEach((c) => c.destroy());
|
||||||
this.scoreBadgeContainers = [];
|
this.scoreBadgeContainers = [];
|
||||||
this.oppScoreTexts = {};
|
this.oppScoreTexts = {};
|
||||||
|
this.oppMiniHandGfx.forEach((g) => g?.destroy());
|
||||||
|
this.oppMiniHandGfx = [];
|
||||||
|
this.oppMiniHandCount.forEach((t) => t?.destroy());
|
||||||
|
this.oppMiniHandCount = [];
|
||||||
|
|
||||||
// Player score badge (above human portrait in the bottom HUD)
|
// Player score badge (above human portrait in the bottom HUD)
|
||||||
const pb = this._makeScoreBadge(80, BOT_Y - 64, this.playerColor(0).hex, 16);
|
const pb = this._makeScoreBadge(80, BOT_Y - 64, this.playerColor(0).hex, 16);
|
||||||
|
|
@ -209,10 +216,10 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
this.opponentPortraits.push(portrait);
|
this.opponentPortraits.push(portrait);
|
||||||
const col = PLAYER_COLORS[this.gs.players[seat].colorIndex];
|
const col = PLAYER_COLORS[this.gs.players[seat].colorIndex];
|
||||||
this.add.circle(x, y, OPP_R + 4, col.hex, 0).setStrokeStyle(3, col.hex, 0.95).setDepth(D.hud + 2);
|
this.add.circle(x, y, OPP_R + 4, col.hex, 0).setStrokeStyle(3, col.hex, 0.95).setDepth(D.hud + 2);
|
||||||
this.add.text(x + OPP_R + 12, y - 18, this.pname(seat), {
|
this.add.text(x + OPP_R + 12, y - 10, this.pname(seat), {
|
||||||
fontFamily: 'Righteous', fontSize: '15px', color: COLORS.textHex,
|
fontFamily: 'Righteous', fontSize: '15px', color: COLORS.textHex,
|
||||||
}).setOrigin(0, 0.5).setDepth(D.hud + 1);
|
}).setOrigin(0, 0.5).setDepth(D.hud + 1);
|
||||||
const info = this.add.text(x + OPP_R + 12, y + 6, '', {
|
const info = this.add.text(x + OPP_R + 12, y + 10, '', {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '12px', color: COLORS.mutedHex, lineSpacing: 2,
|
fontFamily: '"Julius Sans One"', fontSize: '12px', color: COLORS.mutedHex, lineSpacing: 2,
|
||||||
}).setOrigin(0, 0.5).setDepth(D.hud + 1);
|
}).setOrigin(0, 0.5).setDepth(D.hud + 1);
|
||||||
this.oppPanelText[seat] = info;
|
this.oppPanelText[seat] = info;
|
||||||
|
|
@ -221,6 +228,16 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
const ob = this._makeScoreBadge(x, y - OPP_R - 14, col.hex, 13);
|
const ob = this._makeScoreBadge(x, y - OPP_R - 14, col.hex, 13);
|
||||||
this.oppScoreTexts[seat] = ob.text;
|
this.oppScoreTexts[seat] = ob.text;
|
||||||
this.scoreBadgeContainers.push(ob.container);
|
this.scoreBadgeContainers.push(ob.container);
|
||||||
|
|
||||||
|
// Mini face-down hand fan below portrait
|
||||||
|
const fanX = x - OPP_R;
|
||||||
|
const fanY = y + OPP_R + 20;
|
||||||
|
const miniGfx = this.add.graphics().setDepth(D.hud + 1);
|
||||||
|
this.oppMiniHandGfx[seat] = miniGfx;
|
||||||
|
const miniCount = this.add.text(fanX + 86, fanY, '×0', {
|
||||||
|
fontFamily: 'Righteous', fontSize: '13px', color: COLORS.mutedHex,
|
||||||
|
}).setOrigin(0, 0.5).setDepth(D.hud + 2);
|
||||||
|
this.oppMiniHandCount[seat] = miniCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,17 +431,22 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
// pendingTickets now set for this AI seat — advance() routes to aiResolveTickets.
|
// pendingTickets now set for this AI seat — advance() routes to aiResolveTickets.
|
||||||
} else { // drawTrain
|
} else { // drawTrain
|
||||||
const { x: dX, y: dY } = this.oppPos(seat);
|
const { x: dX, y: dY } = this.oppPos(seat);
|
||||||
|
const fanX = dX - OPP_R;
|
||||||
|
const fanY = dY + OPP_R + 20;
|
||||||
// First draw
|
// First draw
|
||||||
const faceUpColor1 = a.source === 'faceUp' ? this.gs.faceUp[a.index] : null;
|
const faceUpColor1 = a.source === 'faceUp' ? this.gs.faceUp[a.index] : null;
|
||||||
const srcX1 = a.source === 'faceUp' ? MK_X : PILE_X;
|
const srcX1 = a.source === 'faceUp' ? MK_X : PILE_X;
|
||||||
const srcY1 = a.source === 'faceUp' ? MK_Y0 + a.index * (CARD_W + 10) : MK_Y0;
|
const srcY1 = a.source === 'faceUp' ? MK_Y0 + a.index * (CARD_W + 10) : MK_Y0;
|
||||||
this.gs = L.drawTrainCard(this.gs, seat, a.source, a.index);
|
this.gs = L.drawTrainCard(this.gs, seat, a.source, a.index);
|
||||||
|
const slot1 = Math.min(L.handCount(this.gs.players[seat]) - 1, 4);
|
||||||
playSound(this, SFX.CARD_DEAL);
|
playSound(this, SFX.CARD_DEAL);
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
await this.animateCardDraw({
|
await this.animateCardDraw({
|
||||||
srcX: srcX1, srcY: srcY1, srcAngle: -90,
|
srcX: srcX1, srcY: srcY1, srcAngle: -90,
|
||||||
cardFrame: faceUpColor1 ? TTR_CARD_FRAME[faceUpColor1] : TTR_CARD_FRAME.back,
|
cardFrame: faceUpColor1 ? TTR_CARD_FRAME[faceUpColor1] : TTR_CARD_FRAME.back,
|
||||||
shouldFlip: false, destX: dX, destY: dY, destW: 44, destH: 64,
|
shouldFlip: false,
|
||||||
|
destX: fanX + slot1 * MINI_STEP + MINI_W / 2, destY: fanY,
|
||||||
|
destW: MINI_W, destH: MINI_H,
|
||||||
});
|
});
|
||||||
// Second draw if the turn is still in progress.
|
// Second draw if the turn is still in progress.
|
||||||
if ((this.gs.phase === 'turn' || this.gs.phase === 'lastRound')
|
if ((this.gs.phase === 'turn' || this.gs.phase === 'lastRound')
|
||||||
|
|
@ -435,12 +457,15 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
const srcX2 = b.source === 'faceUp' ? MK_X : PILE_X;
|
const srcX2 = b.source === 'faceUp' ? MK_X : PILE_X;
|
||||||
const srcY2 = b.source === 'faceUp' ? MK_Y0 + b.index * (CARD_W + 10) : MK_Y0;
|
const srcY2 = b.source === 'faceUp' ? MK_Y0 + b.index * (CARD_W + 10) : MK_Y0;
|
||||||
this.gs = L.drawTrainCard(this.gs, seat, b.source, b.index);
|
this.gs = L.drawTrainCard(this.gs, seat, b.source, b.index);
|
||||||
|
const slot2 = Math.min(L.handCount(this.gs.players[seat]) - 1, 4);
|
||||||
playSound(this, SFX.CARD_DEAL);
|
playSound(this, SFX.CARD_DEAL);
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
await this.animateCardDraw({
|
await this.animateCardDraw({
|
||||||
srcX: srcX2, srcY: srcY2, srcAngle: -90,
|
srcX: srcX2, srcY: srcY2, srcAngle: -90,
|
||||||
cardFrame: faceUpColor2 ? TTR_CARD_FRAME[faceUpColor2] : TTR_CARD_FRAME.back,
|
cardFrame: faceUpColor2 ? TTR_CARD_FRAME[faceUpColor2] : TTR_CARD_FRAME.back,
|
||||||
shouldFlip: false, destX: dX, destY: dY, destW: 44, destH: 64,
|
shouldFlip: false,
|
||||||
|
destX: fanX + slot2 * MINI_STEP + MINI_W / 2, destY: fanY,
|
||||||
|
destW: MINI_W, destH: MINI_H,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -576,7 +601,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
const p = this.gs.players[seat];
|
const p = this.gs.players[seat];
|
||||||
const info = this.oppPanelText[seat];
|
const info = this.oppPanelText[seat];
|
||||||
if (info) {
|
if (info) {
|
||||||
info.setText(`Trains ${p.trainsLeft} Cards ${L.handCount(p)}\nTickets ${p.tickets.length}`);
|
info.setText(`Trains ${p.trainsLeft}\nTickets ${p.tickets.length}`);
|
||||||
}
|
}
|
||||||
this.oppScoreTexts[seat]?.setText(String(L.publicScore(this.gs, seat)));
|
this.oppScoreTexts[seat]?.setText(String(L.publicScore(this.gs, seat)));
|
||||||
if (this.gs.currentPlayer === seat) {
|
if (this.gs.currentPlayer === seat) {
|
||||||
|
|
@ -584,6 +609,26 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
this.oppRingGfx.lineStyle(4, COLORS.goldHex, 1);
|
this.oppRingGfx.lineStyle(4, COLORS.goldHex, 1);
|
||||||
this.oppRingGfx.strokeCircle(x, y, OPP_R + 8);
|
this.oppRingGfx.strokeCircle(x, y, OPP_R + 8);
|
||||||
}
|
}
|
||||||
|
// Mini hand fan
|
||||||
|
const gfx = this.oppMiniHandGfx[seat];
|
||||||
|
if (gfx) {
|
||||||
|
const { x, y } = this.oppPos(seat);
|
||||||
|
const fanX = x - OPP_R;
|
||||||
|
const fanY = y + OPP_R + 20;
|
||||||
|
gfx.clear();
|
||||||
|
const count = L.handCount(p);
|
||||||
|
const n = Math.min(count, 5);
|
||||||
|
for (let c = 0; c < n; c++) {
|
||||||
|
const cx = fanX + c * MINI_STEP;
|
||||||
|
gfx.fillStyle(0x1a2750, 1);
|
||||||
|
gfx.fillRoundedRect(cx, fanY - MINI_H / 2, MINI_W, MINI_H, 3);
|
||||||
|
gfx.lineStyle(1.5, 0x8aaad4, 0.9);
|
||||||
|
gfx.strokeRoundedRect(cx, fanY - MINI_H / 2, MINI_W, MINI_H, 3);
|
||||||
|
gfx.lineStyle(1, 0xfdf3d8, 0.12);
|
||||||
|
gfx.strokeRoundedRect(cx + 2, fanY - MINI_H / 2 + 2, MINI_W - 4, MINI_H - 4, 2);
|
||||||
|
}
|
||||||
|
this.oppMiniHandCount[seat]?.setText(`×${count}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue