feat: polish Ticket to Ride visuals and add animated game menu tabs
- Introduce muted palette and stroke outlines for unclaimed route segments so claimed routes pop - Refactor busy state handling to toggle dim overlay and suppress hand render flicker during animations - Improve modal layout using right-band centering, responsive font sizing, and word wrapping - Block input briefly after modal close to prevent accidental clicks - Add animated tab icons to the game menu with scale transitions and pulsing glow effects - Load new tab-icons assets in the preload scene
This commit is contained in:
parent
2521146579
commit
eca8013fd4
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
|
@ -26,6 +26,19 @@ const TTR_CARD_FRAME = {
|
||||||
purple: 5, black: 6, white: 7, locomotive: 8, back: 9,
|
purple: 5, black: 6, white: 7, locomotive: 8, back: 9,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Muted palette for unclaimed route segments — desaturated so claimed (player-coloured) routes pop.
|
||||||
|
const TTR_ROUTE_HEX = {
|
||||||
|
red: 0x8c5050, // dusty brick
|
||||||
|
orange: 0x8c6030, // muted amber
|
||||||
|
yellow: 0x7a6820, // olive
|
||||||
|
green: 0x4a6840, // sage
|
||||||
|
blue: 0x3a5470, // slate
|
||||||
|
purple: 0x5a3a6c, // dusty plum
|
||||||
|
black: 0x383838, // dark charcoal
|
||||||
|
white: 0xd4cfc8, // bright warm gray
|
||||||
|
gray: 0x706860, // neutral warm gray
|
||||||
|
};
|
||||||
|
|
||||||
// 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 = 150, OPP_R = 32;
|
const OPP_COL1_X = 1450, OPP_COL2_X = 1706, OPP_Y0 = 150, OPP_STEP = 150, OPP_R = 32;
|
||||||
|
|
@ -42,7 +55,8 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
this.opponents = data.opponents ?? [];
|
this.opponents = data.opponents ?? [];
|
||||||
this.playfield = data.playfield ?? null;
|
this.playfield = data.playfield ?? null;
|
||||||
this.gs = null;
|
this.gs = null;
|
||||||
this.busy = false;
|
this._setBusy(false);
|
||||||
|
this._suppressHandRender = false;
|
||||||
this.opponentPortraits = [];
|
this.opponentPortraits = [];
|
||||||
this.modalObjs = [];
|
this.modalObjs = [];
|
||||||
this.marketObjs = [];
|
this.marketObjs = [];
|
||||||
|
|
@ -79,14 +93,14 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
buildTrainCarTexture() {
|
buildTrainCarTexture() {
|
||||||
const g = this.make.graphics({ x: 0, y: 0, add: false });
|
const g = this.make.graphics({ x: 0, y: 0, add: false });
|
||||||
g.fillStyle(0xffffff, 1); g.fillRoundedRect(1, 3, 118, 34, 4); // body
|
g.fillStyle(0xffffff, 1); g.fillRoundedRect(1, 3, 118, 34, 4); // body
|
||||||
g.fillStyle(0xdddddd, 1); g.fillRect(5, 3, 110, 9); // roof
|
g.fillStyle(0xffffff, 1); g.fillRect(5, 3, 110, 9); // roof (white = full tint brightness)
|
||||||
g.fillStyle(0x181818, 1); g.fillRect(14, 14, 92, 12); // window glass
|
g.fillStyle(0x181818, 1); g.fillRect(14, 14, 92, 12); // window glass (dark contrast detail)
|
||||||
g.fillStyle(0xdddddd, 1); g.fillRect(42, 14, 4, 12); // mullion 1
|
g.fillStyle(0xffffff, 1); g.fillRect(42, 14, 4, 12); // mullion 1
|
||||||
g.fillRect(74, 14, 4, 12); // mullion 2
|
g.fillRect(74, 14, 4, 12); // mullion 2
|
||||||
g.fillStyle(0xaaaaaa, 1); g.fillRoundedRect(10, 30, 22, 9, 2); // left truck
|
g.fillStyle(0xeeeeee, 1); g.fillRoundedRect(10, 30, 22, 9, 2); // left truck
|
||||||
g.fillRoundedRect(88, 30, 22, 9, 2); // right truck
|
g.fillRoundedRect(88, 30, 22, 9, 2); // right truck
|
||||||
g.fillStyle(0x999999, 1); g.fillRect(30, 29, 60, 4); // underframe
|
g.fillStyle(0xeeeeee, 1); g.fillRect(30, 29, 60, 4); // underframe
|
||||||
g.fillStyle(0xeeeeee, 1); g.fillRect(1, 13, 3, 14); // left end cap
|
g.fillStyle(0xffffff, 1); g.fillRect(1, 13, 3, 14); // left end cap
|
||||||
g.fillRect(116, 13, 3, 14); // right end cap
|
g.fillRect(116, 13, 3, 14); // right end cap
|
||||||
g.generateTexture('ttr-train-car', 120, 40);
|
g.generateTexture('ttr-train-car', 120, 40);
|
||||||
g.destroy();
|
g.destroy();
|
||||||
|
|
@ -122,6 +136,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
this.midCache = ROUTES.map((r) => routeMidpoint(r));
|
this.midCache = ROUTES.map((r) => routeMidpoint(r));
|
||||||
|
|
||||||
this.routeGfx = this.add.graphics().setDepth(D.route);
|
this.routeGfx = this.add.graphics().setDepth(D.route);
|
||||||
|
this.trainStrokeGfx = this.add.graphics().setDepth(D.train + 1);
|
||||||
this.hoverGfx = this.add.graphics().setDepth(D.hover);
|
this.hoverGfx = this.add.graphics().setDepth(D.hover);
|
||||||
|
|
||||||
// Invisible rotated hit-zone per route.
|
// Invisible rotated hit-zone per route.
|
||||||
|
|
@ -200,6 +215,17 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex,
|
fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex,
|
||||||
}).setOrigin(0.5).setDepth(D.hud);
|
}).setOrigin(0.5).setDepth(D.hud);
|
||||||
this.makeZone(PILE_X, tY, CARD_W, CARD_H, () => this.onTicketDeckClick());
|
this.makeZone(PILE_X, tY, CARD_W, CARD_H, () => this.onTicketDeckClick());
|
||||||
|
|
||||||
|
const dimTop = MK_Y0 - 70;
|
||||||
|
const dimBot = MK_Y0 + MK_STEP * 3 + CARD_H / 2 + 30;
|
||||||
|
this.drawAreaDimOverlay = this.add.rectangle(
|
||||||
|
RB.cx, (dimTop + dimBot) / 2, RB.w, dimBot - dimTop, 0x000000, 0.55
|
||||||
|
).setDepth(D.hud + 5).setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setBusy(val) {
|
||||||
|
this.busy = val;
|
||||||
|
this.drawAreaDimOverlay?.setVisible(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
makeZone(x, y, w, h, fn) {
|
makeZone(x, y, w, h, fn) {
|
||||||
|
|
@ -325,7 +351,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
|
|
||||||
// ── new match / turn driver ───────────────────────────────────────────────────
|
// ── new match / turn driver ───────────────────────────────────────────────────
|
||||||
startNewMatch() {
|
startNewMatch() {
|
||||||
this.busy = false;
|
this._setBusy(false);
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
const playerCount = Math.min(5, Math.max(2, 1 + this.opponents.length));
|
const playerCount = Math.min(5, Math.max(2, 1 + this.opponents.length));
|
||||||
this.gs = L.createInitialState(playerCount);
|
this.gs = L.createInitialState(playerCount);
|
||||||
|
|
@ -382,7 +408,8 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
const refillHappened = newGs.faceUp.length === origFaceUpLength;
|
const refillHappened = newGs.faceUp.length === origFaceUpLength;
|
||||||
const refillSlot = origFaceUpLength - 1;
|
const refillSlot = origFaceUpLength - 1;
|
||||||
const refillColor = refillHappened ? newGs.faceUp[refillSlot] : undefined;
|
const refillColor = refillHappened ? newGs.faceUp[refillSlot] : undefined;
|
||||||
this.busy = true;
|
this._suppressHandRender = true;
|
||||||
|
this._setBusy(true);
|
||||||
this.gs = newGs;
|
this.gs = newGs;
|
||||||
this.marketSlotObjs[i]?.forEach((o) => o.setVisible(false));
|
this.marketSlotObjs[i]?.forEach((o) => o.setVisible(false));
|
||||||
this.renderAllExceptMarket();
|
this.renderAllExceptMarket();
|
||||||
|
|
@ -394,8 +421,9 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
await this.animateMarketSlideUp(i, origFaceUpLength - 1);
|
await this.animateMarketSlideUp(i, origFaceUpLength - 1);
|
||||||
if (refillColor !== undefined) await this.animateRefillMarketSlot(refillSlot, refillColor);
|
if (refillColor !== undefined) await this.animateRefillMarketSlot(refillSlot, refillColor);
|
||||||
|
this._suppressHandRender = false;
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
this.busy = false;
|
this._setBusy(false);
|
||||||
this.advance();
|
this.advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -404,7 +432,8 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
if (!L.canDrawTrains(this.gs)) { this.flash('No cards left to draw'); return; }
|
if (!L.canDrawTrains(this.gs)) { this.flash('No cards left to draw'); return; }
|
||||||
const newGs = L.drawTrainCard(this.gs, 0, 'deck');
|
const newGs = L.drawTrainCard(this.gs, 0, 'deck');
|
||||||
const drawnColor = HAND_ORDER.find((k) => newGs.players[0].hand[k] > this.gs.players[0].hand[k]);
|
const drawnColor = HAND_ORDER.find((k) => newGs.players[0].hand[k] > this.gs.players[0].hand[k]);
|
||||||
this.busy = true;
|
this._suppressHandRender = true;
|
||||||
|
this._setBusy(true);
|
||||||
this.gs = newGs;
|
this.gs = newGs;
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
playSound(this, SFX.CARD_DEAL);
|
playSound(this, SFX.CARD_DEAL);
|
||||||
|
|
@ -413,7 +442,8 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
srcX: PILE_X, srcY: MK_Y0, srcAngle: -90, cardFrame: TTR_CARD_FRAME[drawnColor],
|
srcX: PILE_X, srcY: MK_Y0, srcAngle: -90, cardFrame: TTR_CARD_FRAME[drawnColor],
|
||||||
shouldFlip: true, destX: dest.x, destY: dest.y, destW: 97, destH: 140,
|
shouldFlip: true, destX: dest.x, destY: dest.y, destW: 97, destH: 140,
|
||||||
});
|
});
|
||||||
this.busy = false;
|
this._suppressHandRender = false;
|
||||||
|
this._setBusy(false);
|
||||||
this.advance();
|
this.advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -442,11 +472,11 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
if (opts.length === 1) {
|
if (opts.length === 1) {
|
||||||
const payment = opts[0];
|
const payment = opts[0];
|
||||||
const newGs = L.claimRoute(this.gs, 0, id, payment);
|
const newGs = L.claimRoute(this.gs, 0, id, payment);
|
||||||
this.busy = true;
|
this._setBusy(true);
|
||||||
await this.animateClaimRoute(0, id, payment);
|
await this.animateClaimRoute(0, id, payment);
|
||||||
this.gs = newGs;
|
this.gs = newGs;
|
||||||
playSound(this, SFX.CARD_PLACE);
|
playSound(this, SFX.CARD_PLACE);
|
||||||
this.busy = false;
|
this._setBusy(false);
|
||||||
this.advance();
|
this.advance();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -455,7 +485,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
|
|
||||||
// ── AI driver ─────────────────────────────────────────────────────────────────
|
// ── AI driver ─────────────────────────────────────────────────────────────────
|
||||||
async aiTurn() {
|
async aiTurn() {
|
||||||
this.busy = true;
|
this._setBusy(true);
|
||||||
const seat = this.gs.currentPlayer;
|
const seat = this.gs.currentPlayer;
|
||||||
this.showTurnBanner(`${this.pname(seat)}'s Turn`);
|
this.showTurnBanner(`${this.pname(seat)}'s Turn`);
|
||||||
await this.delay(650);
|
await this.delay(650);
|
||||||
|
|
@ -486,6 +516,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
const refillHappened1 = firstFaceUp && this.gs.faceUp.length === origFaceUpLen1;
|
const refillHappened1 = firstFaceUp && this.gs.faceUp.length === origFaceUpLen1;
|
||||||
const refillColor1 = refillHappened1 ? this.gs.faceUp[origFaceUpLen1 - 1] : undefined;
|
const refillColor1 = refillHappened1 ? this.gs.faceUp[origFaceUpLen1 - 1] : undefined;
|
||||||
playSound(this, SFX.CARD_DEAL);
|
playSound(this, SFX.CARD_DEAL);
|
||||||
|
this._suppressHandRender = true;
|
||||||
if (firstFaceUp) this.renderAllExceptMarket(); else this.renderAll();
|
if (firstFaceUp) this.renderAllExceptMarket(); else this.renderAll();
|
||||||
await this.animateCardDraw({
|
await this.animateCardDraw({
|
||||||
srcX: srcX1, srcY: srcY1, srcAngle: -90,
|
srcX: srcX1, srcY: srcY1, srcAngle: -90,
|
||||||
|
|
@ -494,6 +525,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
destX: fanX + slot1 * MINI_STEP + MINI_W / 2, destY: fanY,
|
destX: fanX + slot1 * MINI_STEP + MINI_W / 2, destY: fanY,
|
||||||
destW: MINI_W, destH: MINI_H,
|
destW: MINI_W, destH: MINI_H,
|
||||||
});
|
});
|
||||||
|
this._suppressHandRender = false;
|
||||||
if (firstFaceUp) {
|
if (firstFaceUp) {
|
||||||
await this.animateMarketSlideUp(a.index, origFaceUpLen1 - 1);
|
await this.animateMarketSlideUp(a.index, origFaceUpLen1 - 1);
|
||||||
if (refillColor1 !== undefined) await this.animateRefillMarketSlot(origFaceUpLen1 - 1, refillColor1);
|
if (refillColor1 !== undefined) await this.animateRefillMarketSlot(origFaceUpLen1 - 1, refillColor1);
|
||||||
|
|
@ -515,6 +547,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
const refillHappened2 = secondFaceUp && this.gs.faceUp.length === origFaceUpLen2;
|
const refillHappened2 = secondFaceUp && this.gs.faceUp.length === origFaceUpLen2;
|
||||||
const refillColor2 = refillHappened2 ? this.gs.faceUp[origFaceUpLen2 - 1] : undefined;
|
const refillColor2 = refillHappened2 ? this.gs.faceUp[origFaceUpLen2 - 1] : undefined;
|
||||||
playSound(this, SFX.CARD_DEAL);
|
playSound(this, SFX.CARD_DEAL);
|
||||||
|
this._suppressHandRender = true;
|
||||||
if (secondFaceUp) this.renderAllExceptMarket(); else this.renderAll();
|
if (secondFaceUp) this.renderAllExceptMarket(); else this.renderAll();
|
||||||
await this.animateCardDraw({
|
await this.animateCardDraw({
|
||||||
srcX: srcX2, srcY: srcY2, srcAngle: -90,
|
srcX: srcX2, srcY: srcY2, srcAngle: -90,
|
||||||
|
|
@ -523,6 +556,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
destX: fanX + slot2 * MINI_STEP + MINI_W / 2, destY: fanY,
|
destX: fanX + slot2 * MINI_STEP + MINI_W / 2, destY: fanY,
|
||||||
destW: MINI_W, destH: MINI_H,
|
destW: MINI_W, destH: MINI_H,
|
||||||
});
|
});
|
||||||
|
this._suppressHandRender = false;
|
||||||
if (secondFaceUp) {
|
if (secondFaceUp) {
|
||||||
await this.animateMarketSlideUp(b.index, origFaceUpLen2 - 1);
|
await this.animateMarketSlideUp(b.index, origFaceUpLen2 - 1);
|
||||||
if (refillColor2 !== undefined) await this.animateRefillMarketSlot(origFaceUpLen2 - 1, refillColor2);
|
if (refillColor2 !== undefined) await this.animateRefillMarketSlot(origFaceUpLen2 - 1, refillColor2);
|
||||||
|
|
@ -533,17 +567,17 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
|
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
await this.delay(200);
|
await this.delay(200);
|
||||||
this.busy = false;
|
this._setBusy(false);
|
||||||
this.advance();
|
this.advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
async aiResolveTickets() {
|
async aiResolveTickets() {
|
||||||
this.busy = true;
|
this._setBusy(true);
|
||||||
await this.delay(550);
|
await this.delay(550);
|
||||||
const pend = this.gs.pendingTickets;
|
const pend = this.gs.pendingTickets;
|
||||||
const keep = AI.chooseTicketsToKeep(this.gs, pend.seat, pend.drawn, pend.minKeep);
|
const keep = AI.chooseTicketsToKeep(this.gs, pend.seat, pend.drawn, pend.minKeep);
|
||||||
this.gs = L.resolveTicketKeep(this.gs, pend.seat, keep);
|
this.gs = L.resolveTicketKeep(this.gs, pend.seat, keep);
|
||||||
this.busy = false;
|
this._setBusy(false);
|
||||||
this.advance();
|
this.advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -563,7 +597,9 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
this.trainCarObjs.forEach((o) => o.destroy());
|
this.trainCarObjs.forEach((o) => o.destroy());
|
||||||
this.trainCarObjs = [];
|
this.trainCarObjs = [];
|
||||||
const g = this.routeGfx;
|
const g = this.routeGfx;
|
||||||
|
const sg = this.trainStrokeGfx;
|
||||||
g.clear();
|
g.clear();
|
||||||
|
sg.clear();
|
||||||
ROUTES.forEach((r, id) => {
|
ROUTES.forEach((r, id) => {
|
||||||
const owner = this.gs.claimed[id];
|
const owner = this.gs.claimed[id];
|
||||||
const segs = this.segCache[id];
|
const segs = this.segCache[id];
|
||||||
|
|
@ -575,10 +611,16 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
.setRotation(seg.angle)
|
.setRotation(seg.angle)
|
||||||
.setTint(col.hex)
|
.setTint(col.hex)
|
||||||
.setDepth(D.train);
|
.setDepth(D.train);
|
||||||
|
sg.save();
|
||||||
|
sg.translateCanvas(seg.cx, seg.cy);
|
||||||
|
sg.rotateCanvas(seg.angle);
|
||||||
|
sg.lineStyle(2, 0xffffff, 1);
|
||||||
|
sg.strokeRoundedRect(-seg.w / 2, -seg.h / 2, seg.w, seg.h, 3);
|
||||||
|
sg.restore();
|
||||||
this.trainCarObjs.push(img);
|
this.trainCarObjs.push(img);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const fill = CARD_COLOR_HEX[r.color] ?? CARD_COLOR_HEX.gray;
|
const fill = TTR_ROUTE_HEX[r.color] ?? TTR_ROUTE_HEX.gray;
|
||||||
for (const seg of segs) this.drawCar(g, seg, fill, 0x2a2118, null);
|
for (const seg of segs) this.drawCar(g, seg, fill, 0x2a2118, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -639,6 +681,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHand() {
|
renderHand() {
|
||||||
|
if (this._suppressHandRender) return;
|
||||||
this.handObjs.forEach((o) => o.destroy());
|
this.handObjs.forEach((o) => o.destroy());
|
||||||
this.handObjs = [];
|
this.handObjs = [];
|
||||||
const hand = this.gs.players[0].hand;
|
const hand = this.gs.players[0].hand;
|
||||||
|
|
@ -683,7 +726,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
// Mini hand fan
|
// Mini hand fan
|
||||||
const gfx = this.oppMiniHandGfx[seat];
|
const gfx = this.oppMiniHandGfx[seat];
|
||||||
if (gfx) {
|
if (gfx && !this._suppressHandRender) {
|
||||||
const { x, y } = this.oppPos(seat);
|
const { x, y } = this.oppPos(seat);
|
||||||
const fanX = x - OPP_R;
|
const fanX = x - OPP_R;
|
||||||
const fanY = y + OPP_R + 20;
|
const fanY = y + OPP_R + 20;
|
||||||
|
|
@ -785,6 +828,10 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
closeModal() {
|
closeModal() {
|
||||||
this.modalObjs.forEach((o) => o.destroy());
|
this.modalObjs.forEach((o) => o.destroy());
|
||||||
this.modalObjs = [];
|
this.modalObjs = [];
|
||||||
|
// Block input for one tick so the click that closed the modal doesn't
|
||||||
|
// immediately fire on whatever is now exposed underneath it.
|
||||||
|
this.input.enabled = false;
|
||||||
|
this.time.delayedCall(80, () => { this.input.enabled = true; });
|
||||||
}
|
}
|
||||||
|
|
||||||
promptTicketKeep() {
|
promptTicketKeep() {
|
||||||
|
|
@ -792,7 +839,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
const pend = this.gs.pendingTickets;
|
const pend = this.gs.pendingTickets;
|
||||||
const setup = pend.context === 'setup';
|
const setup = pend.context === 'setup';
|
||||||
const selected = new Set(pend.drawn); // pre-keep everything
|
const selected = new Set(pend.drawn); // pre-keep everything
|
||||||
const PX = 700, PW = 720;
|
const PX = RB.cx, PW = RB.w - 52;
|
||||||
const rowH = 70;
|
const rowH = 70;
|
||||||
const PH = 200 + pend.drawn.length * rowH;
|
const PH = 200 + pend.drawn.length * rowH;
|
||||||
const PY = 540;
|
const PY = 540;
|
||||||
|
|
@ -802,7 +849,8 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
const panel = this.add.rectangle(PX, PY, PW, PH, 0x14202c, 0.98).setStrokeStyle(3, COLORS.accent).setDepth(D.modal + 1);
|
const panel = this.add.rectangle(PX, PY, PW, PH, 0x14202c, 0.98).setStrokeStyle(3, COLORS.accent).setDepth(D.modal + 1);
|
||||||
const title = this.add.text(PX, PY - PH / 2 + 36,
|
const title = this.add.text(PX, PY - PH / 2 + 36,
|
||||||
setup ? 'Keep at least 2 destination tickets' : 'Keep at least 1 destination ticket', {
|
setup ? 'Keep at least 2 destination tickets' : 'Keep at least 1 destination ticket', {
|
||||||
fontFamily: 'Righteous', fontSize: '26px', color: COLORS.textHex,
|
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.textHex,
|
||||||
|
wordWrap: { width: PW - 32 }, align: 'center',
|
||||||
}).setOrigin(0.5).setDepth(D.modal + 2);
|
}).setOrigin(0.5).setDepth(D.modal + 2);
|
||||||
this.modalObjs.push(overlay, panel, title);
|
this.modalObjs.push(overlay, panel, title);
|
||||||
|
|
||||||
|
|
@ -819,10 +867,10 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
const box = this.add.rectangle(PX, ry, PW - 80, rowH - 12, 0x223344, 1)
|
const box = this.add.rectangle(PX, ry, PW - 80, rowH - 12, 0x223344, 1)
|
||||||
.setStrokeStyle(3, COLORS.gold).setDepth(D.modal + 2).setInteractive({ useHandCursor: true });
|
.setStrokeStyle(3, COLORS.gold).setDepth(D.modal + 2).setInteractive({ useHandCursor: true });
|
||||||
const label = this.add.text(PX - PW / 2 + 60, ry, `${CITIES[t.a].name} → ${CITIES[t.b].name}`, {
|
const label = this.add.text(PX - PW / 2 + 60, ry, `${CITIES[t.a].name} → ${CITIES[t.b].name}`, {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.textHex,
|
fontFamily: '"Julius Sans One"', fontSize: '17px', color: COLORS.textHex,
|
||||||
}).setOrigin(0, 0.5).setDepth(D.modal + 3);
|
}).setOrigin(0, 0.5).setDepth(D.modal + 3);
|
||||||
const pts = this.add.text(PX + PW / 2 - 60, ry, `${t.points} pts`, {
|
const pts = this.add.text(PX + PW / 2 - 46, ry, `${t.points} pts`, {
|
||||||
fontFamily: 'Righteous', fontSize: '20px', color: COLORS.goldHex,
|
fontFamily: 'Righteous', fontSize: '17px', color: COLORS.goldHex,
|
||||||
}).setOrigin(1, 0.5).setDepth(D.modal + 3);
|
}).setOrigin(1, 0.5).setDepth(D.modal + 3);
|
||||||
const mark = this.add.text(PX - PW / 2 + 32, ry, '✓', {
|
const mark = this.add.text(PX - PW / 2 + 32, ry, '✓', {
|
||||||
fontFamily: 'Righteous', fontSize: '22px', color: '#5fd97a',
|
fontFamily: 'Righteous', fontSize: '22px', color: '#5fd97a',
|
||||||
|
|
@ -854,9 +902,9 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
|
|
||||||
promptClaimPayment(routeId, options) {
|
promptClaimPayment(routeId, options) {
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
this.busy = true; // block other input while choosing
|
this._setBusy(true); // block other input while choosing
|
||||||
const route = ROUTES[routeId];
|
const route = ROUTES[routeId];
|
||||||
const PX = 700, PW = 640;
|
const PX = RB.cx, PW = RB.w - 52;
|
||||||
const PH = 180 + options.length * 70;
|
const PH = 180 + options.length * 70;
|
||||||
const PY = 540;
|
const PY = 540;
|
||||||
const overlay = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.55)
|
const overlay = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.55)
|
||||||
|
|
@ -864,7 +912,8 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
const panel = this.add.rectangle(PX, PY, PW, PH, 0x14202c, 0.98).setStrokeStyle(3, COLORS.accent).setDepth(D.modal + 1);
|
const panel = this.add.rectangle(PX, PY, PW, PH, 0x14202c, 0.98).setStrokeStyle(3, COLORS.accent).setDepth(D.modal + 1);
|
||||||
const title = this.add.text(PX, PY - PH / 2 + 34,
|
const title = this.add.text(PX, PY - PH / 2 + 34,
|
||||||
`Claim ${CITIES[route.a].name} – ${CITIES[route.b].name}\nPay with:`, {
|
`Claim ${CITIES[route.a].name} – ${CITIES[route.b].name}\nPay with:`, {
|
||||||
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.textHex, align: 'center',
|
fontFamily: 'Righteous', fontSize: '20px', color: COLORS.textHex, align: 'center',
|
||||||
|
wordWrap: { width: PW - 32 },
|
||||||
}).setOrigin(0.5).setDepth(D.modal + 2);
|
}).setOrigin(0.5).setDepth(D.modal + 2);
|
||||||
this.modalObjs.push(overlay, panel, title);
|
this.modalObjs.push(overlay, panel, title);
|
||||||
|
|
||||||
|
|
@ -875,10 +924,10 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
await this.animateClaimRoute(0, routeId, payment);
|
await this.animateClaimRoute(0, routeId, payment);
|
||||||
this.gs = newGs;
|
this.gs = newGs;
|
||||||
playSound(this, SFX.CARD_PLACE);
|
playSound(this, SFX.CARD_PLACE);
|
||||||
this.busy = false;
|
this._setBusy(false);
|
||||||
this.advance();
|
this.advance();
|
||||||
} else {
|
} else {
|
||||||
this.busy = false;
|
this._setBusy(false);
|
||||||
this.advance();
|
this.advance();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -904,14 +953,14 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
for (const rid of this.gs.players[0].routes) { const r = ROUTES[rid]; parent[f(r.a)] = f(r.b); }
|
for (const rid of this.gs.players[0].routes) { const r = ROUTES[rid]; parent[f(r.a)] = f(r.b); }
|
||||||
return f;
|
return f;
|
||||||
})();
|
})();
|
||||||
const PX = 700, PW = 700, rowH = 64;
|
const PX = RB.cx, PW = RB.w - 52, rowH = 64;
|
||||||
const PH = 170 + Math.max(1, tickets.length) * rowH;
|
const PH = 170 + Math.max(1, tickets.length) * rowH;
|
||||||
const PY = 540;
|
const PY = 540;
|
||||||
const overlay = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.55)
|
const overlay = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.55)
|
||||||
.setDepth(D.modal).setInteractive();
|
.setDepth(D.modal).setInteractive();
|
||||||
const panel = this.add.rectangle(PX, PY, PW, PH, 0x14202c, 0.98).setStrokeStyle(3, COLORS.accent).setDepth(D.modal + 1);
|
const panel = this.add.rectangle(PX, PY, PW, PH, 0x14202c, 0.98).setStrokeStyle(3, COLORS.accent).setDepth(D.modal + 1);
|
||||||
const title = this.add.text(PX, PY - PH / 2 + 34, 'Your Destination Tickets', {
|
const title = this.add.text(PX, PY - PH / 2 + 34, 'Your Destination Tickets', {
|
||||||
fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex,
|
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.textHex,
|
||||||
}).setOrigin(0.5).setDepth(D.modal + 2);
|
}).setOrigin(0.5).setDepth(D.modal + 2);
|
||||||
this.modalObjs.push(overlay, panel, title);
|
this.modalObjs.push(overlay, panel, title);
|
||||||
|
|
||||||
|
|
@ -923,13 +972,13 @@ export default class TicketToRideGame extends Phaser.Scene {
|
||||||
tickets.forEach((t, i) => {
|
tickets.forEach((t, i) => {
|
||||||
const ry = PY - PH / 2 + 84 + i * rowH;
|
const ry = PY - PH / 2 + 84 + i * rowH;
|
||||||
const done = this.gs.players[0].routes.length > 0 && find(t.a) === find(t.b);
|
const done = this.gs.players[0].routes.length > 0 && find(t.a) === find(t.b);
|
||||||
this.modalObjs.push(this.add.text(PX - PW / 2 + 40, ry, `${CITIES[t.a].name} → ${CITIES[t.b].name}`, {
|
this.modalObjs.push(this.add.text(PX - PW / 2 + 28, ry, `${CITIES[t.a].name} → ${CITIES[t.b].name}`, {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '19px', color: COLORS.textHex,
|
fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.textHex,
|
||||||
}).setOrigin(0, 0.5).setDepth(D.modal + 2));
|
}).setOrigin(0, 0.5).setDepth(D.modal + 2));
|
||||||
this.modalObjs.push(this.add.text(PX + PW / 2 - 150, ry, `${t.points} pts`, {
|
this.modalObjs.push(this.add.text(PX + PW / 2 - 90, ry, `${t.points} pts`, {
|
||||||
fontFamily: 'Righteous', fontSize: '18px', color: COLORS.goldHex,
|
fontFamily: 'Righteous', fontSize: '16px', color: COLORS.goldHex,
|
||||||
}).setOrigin(1, 0.5).setDepth(D.modal + 2));
|
}).setOrigin(1, 0.5).setDepth(D.modal + 2));
|
||||||
this.modalObjs.push(this.add.text(PX + PW / 2 - 40, ry, done ? '✓ done' : 'open', {
|
this.modalObjs.push(this.add.text(PX + PW / 2 - 18, ry, done ? '✓' : '…', {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '16px', color: done ? '#5fd97a' : COLORS.mutedHex,
|
fontFamily: '"Julius Sans One"', fontSize: '16px', color: done ? '#5fd97a' : COLORS.mutedHex,
|
||||||
}).setOrigin(1, 0.5).setDepth(D.modal + 2));
|
}).setOrigin(1, 0.5).setDepth(D.modal + 2));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,12 @@ const CATEGORIES = [
|
||||||
{ key: 'word', label: 'Word' },
|
{ key: 'word', label: 'Word' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const TAB_ICON_FRAMES = { tabletop: 0, cards: 1, casino: 2, word: 3 };
|
||||||
|
const ICON_INACTIVE = 56;
|
||||||
|
const ICON_ACTIVE = 72;
|
||||||
|
const ICON_OVERSHOOT = 86;
|
||||||
|
const ICON_X_OFFSET = -125;
|
||||||
|
|
||||||
export default class GameMenuScene extends Phaser.Scene {
|
export default class GameMenuScene extends Phaser.Scene {
|
||||||
constructor() { super('GameMenu'); }
|
constructor() { super('GameMenu'); }
|
||||||
|
|
||||||
|
|
@ -63,6 +69,15 @@ export default class GameMenuScene extends Phaser.Scene {
|
||||||
this._tabs[key] = btn;
|
this._tabs[key] = btn;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._tabIcons = {};
|
||||||
|
activeCats.forEach(({ key }) => {
|
||||||
|
const btn = this._tabs[key];
|
||||||
|
const icon = this.add.image(btn.x + ICON_X_OFFSET, btn.y, 'tab-icons', TAB_ICON_FRAMES[key])
|
||||||
|
.setDisplaySize(ICON_INACTIVE, ICON_INACTIVE);
|
||||||
|
icon._glow = null;
|
||||||
|
this._tabIcons[key] = icon;
|
||||||
|
});
|
||||||
|
|
||||||
this.showCategory(activeCats[0].key);
|
this.showCategory(activeCats[0].key);
|
||||||
|
|
||||||
new Button(this, cx, GAME_HEIGHT - 100, 'Back', () => this.scene.start('Landing'), { variant: 'ghost' });
|
new Button(this, cx, GAME_HEIGHT - 100, 'Back', () => this.scene.start('Landing'), { variant: 'ghost' });
|
||||||
|
|
@ -73,6 +88,49 @@ export default class GameMenuScene extends Phaser.Scene {
|
||||||
btn.setActive(k === key);
|
btn.setActive(k === key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._tabIcons) {
|
||||||
|
for (const [k, icon] of Object.entries(this._tabIcons)) {
|
||||||
|
this.tweens.killTweensOf(icon._glow);
|
||||||
|
icon.postFX.clear();
|
||||||
|
icon._glow = null;
|
||||||
|
|
||||||
|
if (k === key) {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: icon,
|
||||||
|
displayWidth: ICON_OVERSHOOT, displayHeight: ICON_OVERSHOOT,
|
||||||
|
duration: 160,
|
||||||
|
ease: 'Quad.easeOut',
|
||||||
|
onComplete: () => {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: icon,
|
||||||
|
displayWidth: ICON_ACTIVE, displayHeight: ICON_ACTIVE,
|
||||||
|
duration: 220,
|
||||||
|
ease: 'Back.easeOut',
|
||||||
|
onComplete: () => {
|
||||||
|
icon._glow = icon.postFX.addGlow(0xd4a017, 0, 0, false, 0.1, 16);
|
||||||
|
this.tweens.add({
|
||||||
|
targets: icon._glow,
|
||||||
|
outerStrength: 6,
|
||||||
|
duration: 900,
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
ease: 'Sine.easeInOut',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: icon,
|
||||||
|
displayWidth: ICON_INACTIVE, displayHeight: ICON_INACTIVE,
|
||||||
|
duration: 180,
|
||||||
|
ease: 'Quad.easeIn',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const obj of this._gameObjects) obj.destroy();
|
for (const obj of this._gameObjects) obj.destroy();
|
||||||
this._gameObjects = [];
|
this._gameObjects = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ export default class PreloadScene extends Phaser.Scene {
|
||||||
// Optional — same procedural fallback applies when the sheet is absent.
|
// Optional — same procedural fallback applies when the sheet is absent.
|
||||||
this.load.spritesheet('dominion-prosperity', '/assets/images/dominion-prosperity.png', { frameWidth: 270, frameHeight: 390 });
|
this.load.spritesheet('dominion-prosperity', '/assets/images/dominion-prosperity.png', { frameWidth: 270, frameHeight: 390 });
|
||||||
this.load.spritesheet('ttr-cards', '/assets/images/tickettoride-cards.png', { frameWidth: 270, frameHeight: 390 });
|
this.load.spritesheet('ttr-cards', '/assets/images/tickettoride-cards.png', { frameWidth: 270, frameHeight: 390 });
|
||||||
|
this.load.spritesheet('tab-icons', '/assets/images/tab-icons.png', { frameWidth: 128, frameHeight: 128 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async create() {
|
async create() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue