Added VP coins to Dominion

This commit is contained in:
Brian Fertig 2026-05-30 17:18:37 -06:00
parent eca8013fd4
commit fcc061e43d
4 changed files with 132 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

View File

@ -110,6 +110,8 @@ export default class DominionGame extends Phaser.Scene {
this._liveDiscardPile = null;
this._liveDiscardBadge = null;
this._liveDiscardCountText = null;
this.tokenSprites = [];
this.lastVpTokens = { 0: 0, 1: 0, 2: 0, 3: 0 };
}
create() {
@ -825,6 +827,133 @@ export default class DominionGame extends Phaser.Scene {
});
}
// ── Floating token sprites (Prosperity expansion) ─────────────────────────────
// Spawns a token sprite above the player's phase dial with random position
// and rotation when VP tokens or gold are earned.
// Phase dial anchor positions (mirrors buildPhaseDials).
_dialPos(seat) {
if (seat === 0) return { x: 168, y: 748 };
const s = this.oppSlot(seat - 1);
const r = 26;
return { x: s.x + s.r + 14 + r, y: s.y };
}
spawnTokenSprite(seat, tokenType) {
// tokenType: 'vp1', 'vp5', or 'gold'
const spriteMap = { vp1: 0, vp5: 1, gold: 2 };
const frameIndex = spriteMap[tokenType];
if (frameIndex === undefined || !this.textures.exists('dominion-tokens')) return;
const dial = this._dialPos(seat);
// Random position in a ~200×250 area above the dial
const offsetX = Phaser.Math.Between(-100, 100);
const offsetY = Phaser.Math.Between(-400, -200);
const x = dial.x + offsetX;
const y = dial.y + offsetY;
// Clamp to visible area
const clampedX = Phaser.Math.Clamp(x, 75, GAME_WIDTH - 75);
const clampedY = Phaser.Math.Clamp(y, 50, GAME_HEIGHT - 50);
const angle = Phaser.Math.Between(-35, 35);
const targetScale = 80 / 150; // 80px final size from 150px source
const sprite = this.add.sprite(clampedX, clampedY, 'dominion-tokens', frameIndex)
.setDisplaySize(80, 80)
.setAngle(angle)
.setDepth(D.popup)
.setAlpha(0)
.setScale(1.5)
.setData('tokenType', tokenType);
this.tokenSprites.push(sprite);
// Animate from larger size down to 80×80 — token stays for the rest of the game
this.tweens.add({
targets: sprite,
alpha: 0.92,
scaleX: targetScale,
scaleY: targetScale,
duration: 1200,
ease: 'Back.easeOut',
});
}
// Count 1-VP tokens currently visible in the placement area (above human player's dial)
_countOneVpTokens() {
const dial = this._dialPos(0);
let count = 0;
for (const s of this.tokenSprites) {
if (s.getData('tokenType') === 'vp1' &&
s.active && s.alpha > 0.1 &&
s.y < dial.y - 200) { // in the area above the dial
count++;
}
}
return count;
}
// Remove all 1-VP tokens from the placement area with a fade-out
_removeOneVpTokens() {
for (const s of this.tokenSprites) {
if (s.getData('tokenType') === 'vp1' && s.active && s.alpha > 0.1 && s.y < this._dialPos(0).y - 200) {
this.tweens.add({
targets: s,
alpha: 0,
duration: 400,
ease: 'Sine.In',
onComplete: () => {
const idx = this.tokenSprites.indexOf(s);
if (idx > -1) this.tokenSprites.splice(idx, 1);
s.destroy();
},
});
}
}
}
_checkTokenGains(prevState, newState) {
const prevVp = this.lastVpTokens;
const newVp = {};
newState.players.forEach((p, seat) => { newVp[seat] = p.vpTokens ?? 0; });
// Only show token animations for the human player (seat 0)
const humanSeat = 0;
const diff = newVp[humanSeat] - (prevVp[humanSeat] ?? 0);
if (diff > 0) {
// Keep replacing 1-VP tokens with 5-VP tokens until we can't anymore
let remaining = diff;
while (remaining > 0) {
const currentOneVp = this._countOneVpTokens();
if (currentOneVp > 0 && currentOneVp < 5 && currentOneVp + remaining >= 5) {
// Remove all 1-VP tokens and spawn one 5-VP token
this._removeOneVpTokens();
this.spawnTokenSprite(humanSeat, 'vp5');
remaining -= (5 - currentOneVp);
} else if (remaining >= 5) {
// No 1-VP tokens on screen, but we have 5+ to spend — spawn a 5-VP token
this.spawnTokenSprite(humanSeat, 'vp5');
remaining -= 5;
} else {
// Just spawn 1-VP tokens
for (let i = 0; i < remaining; i++) {
this.spawnTokenSprite(humanSeat, 'vp1');
}
remaining = 0;
}
}
}
this.lastVpTokens = newVp;
// Detect gold gains from log events (human player only)
const newLog = newState.log.slice(prevState.log.length);
for (const evt of newLog) {
if (evt.kind === 'gain' && evt.id === 'gold' && evt.seat === humanSeat) {
this.spawnTokenSprite(humanSeat, 'gold');
}
}
}
updateControls() {
const gs = this.gs;
const humanTurn = gs.turn === 0 && !gs.pending && !this.gameOver && !this._animating;
@ -1164,6 +1293,7 @@ export default class DominionGame extends Phaser.Scene {
}
this.gs = s;
this._checkTokenGains(prev, s);
this.clearPrompt();
this.render();
this.scheduleAdvance(10);

View File

@ -73,6 +73,8 @@ export default class PreloadScene extends Phaser.Scene {
// Prosperity expansion art (frame order documented in expansions/prosperity.js).
// Optional — same procedural fallback applies when the sheet is absent.
this.load.spritesheet('dominion-prosperity', '/assets/images/dominion-prosperity.png', { frameWidth: 270, frameHeight: 390 });
// Prosperity token sprites (1 VP, 5 VP, Gold) — 150×150 each.
this.load.spritesheet('dominion-tokens', '/assets/images/dominion-tokens.png', { frameWidth: 150, frameHeight: 150 });
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 });
}