feat: enhance card animations and improve icon row fitting
- Add `_animPurchaseShowcase` for a dramatic card reveal with a dark overlay and animated comet border effect - Refactor purchase and gain flows to use the new showcase animation before flying the card to its destination - Update `addIconRow` to accept a width constraint, dynamically scale text/tokens, and fallback to colored bars when space is too limited
This commit is contained in:
parent
ca38548700
commit
33bbeff79b
|
|
@ -857,7 +857,7 @@ export default class DominionGame extends Phaser.Scene {
|
||||||
}).setOrigin(0.5));
|
}).setOrigin(0.5));
|
||||||
|
|
||||||
// Icon row (between title and type line)
|
// Icon row (between title and type line)
|
||||||
this.addIconRow(c, def, 0, bandTop + (h / 2 - bandTop) * 0.55, h);
|
this.addIconRow(c, def, 0, bandTop + (h / 2 - bandTop) * 0.55, h, w);
|
||||||
|
|
||||||
// Cost coin (top-left)
|
// Cost coin (top-left)
|
||||||
this.addCoinBadge(c, -w / 2 + 14, -h / 2 + 14, def.cost, Math.round(h * 0.075));
|
this.addCoinBadge(c, -w / 2 + 14, -h / 2 + 14, def.cost, Math.round(h * 0.075));
|
||||||
|
|
@ -872,7 +872,7 @@ export default class DominionGame extends Phaser.Scene {
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
addIconRow(container, def, cx, cy, h) {
|
addIconRow(container, def, cx, cy, h, w = Infinity) {
|
||||||
const tokens = [];
|
const tokens = [];
|
||||||
if (def.plus.cards) tokens.push(['cards', def.plus.cards, 0x2f6fb0]);
|
if (def.plus.cards) tokens.push(['cards', def.plus.cards, 0x2f6fb0]);
|
||||||
if (def.plus.actions) tokens.push(['action', def.plus.actions, 0x3f9b54]);
|
if (def.plus.actions) tokens.push(['action', def.plus.actions, 0x3f9b54]);
|
||||||
|
|
@ -881,9 +881,31 @@ export default class DominionGame extends Phaser.Scene {
|
||||||
if (def.coin !== undefined) tokens.push(['$', def.coin, 0xd4a017]); // treasure value
|
if (def.coin !== undefined) tokens.push(['$', def.coin, 0xd4a017]); // treasure value
|
||||||
if (tokens.length === 0) return;
|
if (tokens.length === 0) return;
|
||||||
|
|
||||||
const fs = Phaser.Math.Clamp(Math.round(h * 0.06), 8, 13);
|
const fs0 = Phaser.Math.Clamp(Math.round(h * 0.06), 8, 13);
|
||||||
const tw = fs * 4.2;
|
let tw = fs0 * 4.2;
|
||||||
|
let fs = fs0;
|
||||||
const gap = 4;
|
const gap = 4;
|
||||||
|
const maxW = w - 8;
|
||||||
|
|
||||||
|
const nominalTotalW = tokens.length * tw + (tokens.length - 1) * gap;
|
||||||
|
if (nominalTotalW > maxW) {
|
||||||
|
tw = (maxW - (tokens.length - 1) * gap) / tokens.length;
|
||||||
|
fs = Math.floor(tw / 4.2);
|
||||||
|
if (fs < 7) {
|
||||||
|
const barGap = 2;
|
||||||
|
const barW = (maxW - (tokens.length - 1) * barGap) / tokens.length;
|
||||||
|
const barH = Math.max(3, Math.round(h * 0.025));
|
||||||
|
let bx = cx - maxW / 2 + barW / 2;
|
||||||
|
for (const [, , color] of tokens) {
|
||||||
|
const bar = this.add.rectangle(bx, cy, barW, barH, color, 0.9);
|
||||||
|
bar.setStrokeStyle(0.5, 0x000000, 0.3);
|
||||||
|
container.add(bar);
|
||||||
|
bx += barW + barGap;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const totalW = tokens.length * tw + (tokens.length - 1) * gap;
|
const totalW = tokens.length * tw + (tokens.length - 1) * gap;
|
||||||
let x = cx - totalW / 2 + tw / 2;
|
let x = cx - totalW / 2 + tw / 2;
|
||||||
for (const [kind, val, color] of tokens) {
|
for (const [kind, val, color] of tokens) {
|
||||||
|
|
@ -1716,13 +1738,14 @@ export default class DominionGame extends Phaser.Scene {
|
||||||
this.render();
|
this.render();
|
||||||
const slot = this.oppSlot(seat - 1);
|
const slot = this.oppSlot(seat - 1);
|
||||||
|
|
||||||
|
this._animPurchaseShowcase(cardId, srcX, srcY, slot.x, slot.y, () => {
|
||||||
const cardName = getCard(cardId).name;
|
const cardName = getCard(cardId).name;
|
||||||
const label = this.add.text(slot.x - slot.r - 74, slot.y, `Purchased:\n${cardName}`, {
|
const label = this.add.text(slot.x - slot.r - 74, slot.y, `Purchased:\n${cardName}`, {
|
||||||
fontFamily: 'Righteous', fontSize: '32px', color: '#FFD700',
|
fontFamily: 'Righteous', fontSize: '32px', color: '#FFD700',
|
||||||
align: 'right', stroke: '#000000', strokeThickness: 4,
|
align: 'right', stroke: '#000000', strokeThickness: 4,
|
||||||
}).setOrigin(1, 0.5).setDepth(D.hud + 5).setAlpha(1);
|
}).setOrigin(1, 0.5).setDepth(D.hud + 5).setAlpha(1);
|
||||||
|
|
||||||
const src = { iid: -1, id: cardId, x: srcX, y: srcY };
|
const src = { iid: -1, id: cardId, x: slot.x, y: slot.y };
|
||||||
this._animDiscardCard(src, slot.x, slot.y, () => {
|
this._animDiscardCard(src, slot.x, slot.y, () => {
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: label, alpha: 0, duration: 600, delay: 400, ease: 'Sine.easeIn',
|
targets: label, alpha: 0, duration: 600, delay: 400, ease: 'Sine.easeIn',
|
||||||
|
|
@ -1731,7 +1754,8 @@ export default class DominionGame extends Phaser.Scene {
|
||||||
this._animating = false;
|
this._animating = false;
|
||||||
if (this._pendingAnimState) { const s = this._pendingAnimState; this._pendingAnimState = null; this.setState(s); }
|
if (this._pendingAnimState) { const s = this._pendingAnimState; this._pendingAnimState = null; this.setState(s); }
|
||||||
else this.scheduleAdvance(10);
|
else this.scheduleAdvance(10);
|
||||||
}, { flyDuration: 1000, foldDuration: 500 });
|
}, { flyDuration: 1, foldDuration: 500 });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_chainHumanGain(gainEvt, newState) {
|
_chainHumanGain(gainEvt, newState) {
|
||||||
|
|
@ -1847,6 +1871,73 @@ export default class DominionGame extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_animPurchaseShowcase(cardId, srcX, srcY, tx, ty, onComplete) {
|
||||||
|
const cy = GAME_HEIGHT / 2;
|
||||||
|
const SHOW_W = 270, SHOW_H = 390;
|
||||||
|
const def = getCard(cardId);
|
||||||
|
|
||||||
|
const overlay = this.add.rectangle(CX, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0)
|
||||||
|
.setDepth(D.modal);
|
||||||
|
this.tweens.add({ targets: overlay, alpha: 0.65, duration: 250 });
|
||||||
|
|
||||||
|
const card = this.buildCardFace(SHOW_W, SHOW_H, def);
|
||||||
|
card.setPosition(srcX, srcY).setScale(SUPPLY_W / SHOW_W).setDepth(D.modal + 1);
|
||||||
|
|
||||||
|
this.tweens.add({
|
||||||
|
targets: card, x: CX, y: cy, scaleX: 1, scaleY: 1,
|
||||||
|
duration: 350, ease: 'Back.easeOut',
|
||||||
|
onComplete: () => {
|
||||||
|
// Comet border effect around the full-size card
|
||||||
|
const gfx = this.add.graphics().setDepth(D.modal + 2);
|
||||||
|
const perim = 2 * (SHOW_W + SHOW_H);
|
||||||
|
const TAIL_PX = 130;
|
||||||
|
const SEGMENTS = 24;
|
||||||
|
const COMETS = 3;
|
||||||
|
const COMET_COLORS = [0xffd700, 0xffffff, 0xff9900];
|
||||||
|
const left = CX - SHOW_W / 2, top = cy - SHOW_H / 2;
|
||||||
|
const tracker = { t: 0 };
|
||||||
|
const borderTween = this.tweens.add({
|
||||||
|
targets: tracker, t: { from: 0, to: 1 },
|
||||||
|
duration: 1400, repeat: -1, ease: 'Linear',
|
||||||
|
onUpdate: () => {
|
||||||
|
gfx.clear();
|
||||||
|
for (let c = 0; c < COMETS; c++) {
|
||||||
|
const headDist = ((tracker.t + c / COMETS) % 1) * perim;
|
||||||
|
const cometColor = COMET_COLORS[c];
|
||||||
|
for (let seg = 0; seg < SEGMENTS; seg++) {
|
||||||
|
const frac = seg / SEGMENTS;
|
||||||
|
const d1 = ((headDist - (1 - frac) * TAIL_PX + perim * 100) % perim);
|
||||||
|
const d2 = ((headDist - (1 - (seg + 1) / SEGMENTS) * TAIL_PX + perim * 100) % perim);
|
||||||
|
const [x1, y1] = perimPoint(d1, SHOW_W, SHOW_H, left, top);
|
||||||
|
const [x2, y2] = perimPoint(d2, SHOW_W, SHOW_H, left, top);
|
||||||
|
const alpha = 0.1 + frac * 0.9;
|
||||||
|
const lineColor = frac > 0.85 ? 0xffffff : cometColor;
|
||||||
|
const lineWidth = frac > 0.85 ? 5 : 2.5;
|
||||||
|
gfx.lineStyle(lineWidth, lineColor, alpha);
|
||||||
|
gfx.beginPath();
|
||||||
|
gfx.moveTo(x1, y1);
|
||||||
|
gfx.lineTo(x2, y2);
|
||||||
|
gfx.strokePath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.time.delayedCall(1500, () => {
|
||||||
|
borderTween.stop();
|
||||||
|
gfx.destroy();
|
||||||
|
this.tweens.add({ targets: overlay, alpha: 0, duration: 300 });
|
||||||
|
this.tweens.add({
|
||||||
|
targets: card,
|
||||||
|
x: tx, y: ty, scaleX: HAND_W / SHOW_W, scaleY: HAND_H / SHOW_H,
|
||||||
|
duration: 400, ease: 'Cubic.easeIn',
|
||||||
|
onComplete: () => { card.destroy(); overlay.destroy(); onComplete(); },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_animGainCard(gainedCard, srcX, srcY, dest, newState) {
|
_animGainCard(gainedCard, srcX, srcY, dest, newState) {
|
||||||
this._animating = true;
|
this._animating = true;
|
||||||
this.gs = newState;
|
this.gs = newState;
|
||||||
|
|
@ -1855,8 +1946,9 @@ export default class DominionGame extends Phaser.Scene {
|
||||||
|
|
||||||
const tx = dest === 'deck' ? DECK_PILE_X : DISCARD_PILE_X;
|
const tx = dest === 'deck' ? DECK_PILE_X : DISCARD_PILE_X;
|
||||||
const ty = dest === 'deck' ? DECK_PILE_Y : DISCARD_PILE_Y;
|
const ty = dest === 'deck' ? DECK_PILE_Y : DISCARD_PILE_Y;
|
||||||
const src = { iid: gainedCard.iid, id: gainedCard.id, x: srcX, y: srcY };
|
|
||||||
|
|
||||||
|
this._animPurchaseShowcase(gainedCard.id, srcX, srcY, tx, ty, () => {
|
||||||
|
const src = { iid: gainedCard.iid, id: gainedCard.id, x: tx, y: ty };
|
||||||
this._animDiscardCard(src, tx, ty, () => {
|
this._animDiscardCard(src, tx, ty, () => {
|
||||||
this._animating = false;
|
this._animating = false;
|
||||||
if (this._pendingAnimState) {
|
if (this._pendingAnimState) {
|
||||||
|
|
@ -1864,6 +1956,7 @@ export default class DominionGame extends Phaser.Scene {
|
||||||
} else {
|
} else {
|
||||||
this.scheduleAdvance(10);
|
this.scheduleAdvance(10);
|
||||||
}
|
}
|
||||||
|
}, { flyDuration: 1, foldDuration: 250 });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue