Cards being dealt animations
This commit is contained in:
parent
29cf5972b6
commit
c601771c92
|
|
@ -43,6 +43,12 @@ const DEPTH = { board: 10, town: 20, hand: 40, fx: 60, ui: 80, overlay: 90, hove
|
||||||
// Canonical card shapes (w/h) — every rendered instance of a card type keeps
|
// Canonical card shapes (w/h) — every rendered instance of a card type keeps
|
||||||
// this aspect ratio, whatever box a call site hands it (see fitCardBox).
|
// this aspect ratio, whatever box a call site hands it (see fitCardBox).
|
||||||
const CARD_ASPECT = { room: 280 / 390, spell: 260 / 360, hero: 280 / 390, boss: 300 / 430 };
|
const CARD_ASPECT = { room: 280 / 390, spell: 260 / 360, hero: 280 / 390, boss: 300 / 430 };
|
||||||
|
// Width a drawn card grows to while paused, face down then flipped, in the
|
||||||
|
// center of the screen mid-draw-animation, before shrinking into the hand.
|
||||||
|
const DRAW_FULL_W = { room: 340, spell: 300 };
|
||||||
|
// Deck-pile screen positions the draw animation flies cards out of — must
|
||||||
|
// match renderDeckPiles()'s room/spell rows.
|
||||||
|
const DECK_PILE_POS = { room: { x: 60, y: 520, w: 46, h: 64 }, spell: { x: 60, y: 600, w: 46, h: 64 } };
|
||||||
|
|
||||||
export default class DungeonBossGame extends Phaser.Scene {
|
export default class DungeonBossGame extends Phaser.Scene {
|
||||||
constructor() { super('DungeonBossGame'); }
|
constructor() { super('DungeonBossGame'); }
|
||||||
|
|
@ -64,6 +70,7 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
this._recorded = false;
|
this._recorded = false;
|
||||||
this.hoverTimer = null;
|
this.hoverTimer = null;
|
||||||
this.hoverVisible = false;
|
this.hoverVisible = false;
|
||||||
|
this._dealing = false; // true while the initial-deal animation plays
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -98,9 +105,12 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
this.buildStaticUi();
|
this.buildStaticUi();
|
||||||
this.gs = newGame(this.nPlayers, (Math.random() * 1e9) | 0);
|
this.gs = newGame(this.nPlayers, (Math.random() * 1e9) | 0);
|
||||||
takeEvents(this.gs); // swallow gameStart
|
takeEvents(this.gs); // swallow gameStart
|
||||||
|
this._dealing = true;
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
this.showBanner('Discard 2 cards, then build your first room');
|
this.animateInitialDeal(() => {
|
||||||
this.pump();
|
this.showBanner('Discard 2 cards, then build your first room');
|
||||||
|
this.pump();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── static chrome ──────────────────────────────────────────────────────────
|
// ── static chrome ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -467,6 +477,20 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
g.lineStyle(1.5, 0x4a3a5c, 0.7); g.strokeRoundedRect(-w / 2 + i * 3, -h / 2 + i * 3, w, h, 5);
|
g.lineStyle(1.5, 0x4a3a5c, 0.7); g.strokeRoundedRect(-w / 2 + i * 3, -h / 2 + i * 3, w, h, 5);
|
||||||
cont.add(g);
|
cont.add(g);
|
||||||
}
|
}
|
||||||
|
this.makeDeckBackFace(0, 0, w, h, kind, cont);
|
||||||
|
if (count <= 0) {
|
||||||
|
cont.add(this.add.text(0, 0, '✕', { fontSize: `${Math.round(h * 0.5)}px`, color: '#c0392b' }).setOrigin(0.5));
|
||||||
|
cont.setAlpha(0.45);
|
||||||
|
}
|
||||||
|
(parent || this.boardLayer).add(cont);
|
||||||
|
return cont;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The single card-back face (art if supplied, else a procedural
|
||||||
|
// plate+glyph), shared by the deck-pile display and the draw-flight
|
||||||
|
// animation so both show the same back design.
|
||||||
|
makeDeckBackFace(x, y, w, h, kind, parent) {
|
||||||
|
const cont = this.add.container(x, y);
|
||||||
const art = this.artFor('deckBack', kind);
|
const art = this.artFor('deckBack', kind);
|
||||||
if (art) {
|
if (art) {
|
||||||
cont.add(this.add.image(0, 0, art.key, art.frame).setDisplaySize(w, h));
|
cont.add(this.add.image(0, 0, art.key, art.frame).setDisplaySize(w, h));
|
||||||
|
|
@ -482,14 +506,57 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
const glyph = { room: '⌂', spell: '✦', hero: '⚔', epic: '♛' }[kind] || '✦';
|
const glyph = { room: '⌂', spell: '✦', hero: '⚔', epic: '♛' }[kind] || '✦';
|
||||||
cont.add(this.add.text(0, 0, glyph, { fontSize: `${Math.round(h * 0.34)}px`, color: '#5c4a70' }).setOrigin(0.5));
|
cont.add(this.add.text(0, 0, glyph, { fontSize: `${Math.round(h * 0.34)}px`, color: '#5c4a70' }).setOrigin(0.5));
|
||||||
}
|
}
|
||||||
if (count <= 0) {
|
|
||||||
cont.add(this.add.text(0, 0, '✕', { fontSize: `${Math.round(h * 0.5)}px`, color: '#c0392b' }).setOrigin(0.5));
|
|
||||||
cont.setAlpha(0.45);
|
|
||||||
}
|
|
||||||
(parent || this.boardLayer).add(cont);
|
(parent || this.boardLayer).add(cont);
|
||||||
return cont;
|
return cont;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flies one card from its deck pile to the center of the screen at full
|
||||||
|
// size, flips it face-up there, then shrinks it into its resting spot in
|
||||||
|
// the hand. Used both for the initial deal and for any later draw. The
|
||||||
|
// container is built once at full size and only ever scaled (never
|
||||||
|
// redrawn), so the back/front art stays crisp through every phase.
|
||||||
|
// onComplete receives the landed container — it is NOT auto-destroyed,
|
||||||
|
// since during the initial deal it needs to keep showing (as the real
|
||||||
|
// hand render is suppressed until every card has landed); callers that
|
||||||
|
// don't need that (e.g. a single mid-game draw, where the real card is
|
||||||
|
// already rendered underneath) should destroy it themselves.
|
||||||
|
flyCardFromDeck(kind, inst, targetX, targetY, targetW, onComplete) {
|
||||||
|
const pile = DECK_PILE_POS[kind];
|
||||||
|
const { w: fw, h: fh } = this.fitCardBox(DRAW_FULL_W[kind], 0, CARD_ASPECT[kind]);
|
||||||
|
const cont = this.add.container(pile.x, pile.y).setScale(pile.w / fw);
|
||||||
|
this.fxLayer.add(cont);
|
||||||
|
const back = this.makeDeckBackFace(0, 0, fw, fh, kind, cont);
|
||||||
|
this.sfx(SFX.CARD_DEAL);
|
||||||
|
this.tweens.add({
|
||||||
|
targets: cont, x: GAME_WIDTH / 2, y: GAME_HEIGHT / 2 - 60, scale: 1,
|
||||||
|
duration: 300, ease: 'Cubic.easeOut',
|
||||||
|
onComplete: () => {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: cont, scaleX: 0, duration: 110, ease: 'Sine.easeIn',
|
||||||
|
onComplete: () => {
|
||||||
|
back.destroy();
|
||||||
|
this.sfx(SFX.CARD_SHOW);
|
||||||
|
if (kind === 'room') this.makeRoomCard(0, 0, inst, fw, fh, { parent: cont, showText: true, hover: false, noHoverPreview: true });
|
||||||
|
else this.makeSpellCard(0, 0, inst, fw, fh, { parent: cont, hover: false, noHoverPreview: true });
|
||||||
|
this.tweens.add({
|
||||||
|
targets: cont, scaleX: 1, duration: 110, ease: 'Sine.easeOut',
|
||||||
|
onComplete: () => {
|
||||||
|
this.time.delayedCall(1200, () => {
|
||||||
|
const handScale = targetW / fw;
|
||||||
|
this.tweens.add({
|
||||||
|
targets: cont, x: targetX, y: targetY, scale: handScale,
|
||||||
|
duration: 280, ease: 'Cubic.easeIn',
|
||||||
|
onComplete: () => onComplete?.(cont),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Left-side "deck piles" area: small representations of the four draw
|
// Left-side "deck piles" area: small representations of the four draw
|
||||||
// decks with remaining counts, sitting in the gap between the deck-count
|
// decks with remaining counts, sitting in the gap between the deck-count
|
||||||
// text (above) and the player portrait cluster (below).
|
// text (above) and the player portrait cluster (below).
|
||||||
|
|
@ -849,7 +916,47 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
}).setOrigin(0.5));
|
}).setOrigin(0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deals the human player's starting hand (5 rooms + 2 spells, per
|
||||||
|
// newGame()) one card at a time, flying each from its deck pile to its
|
||||||
|
// real final hand slot. renderHand() is suppressed via this._dealing
|
||||||
|
// until every card has landed, so the reflow always matches where the
|
||||||
|
// flying copies land. Each landed card is kept on screen (not destroyed)
|
||||||
|
// so the hand visibly builds up card by card; once the last one lands,
|
||||||
|
// the real hand is rendered underneath and only then are the landed
|
||||||
|
// stand-ins destroyed, in the same tick, so there's no flicker.
|
||||||
|
animateInitialDeal(onDone) {
|
||||||
|
const p = this.gs.players[this.humanSeat];
|
||||||
|
const rooms = p.hand.rooms, spells = p.hand.spells;
|
||||||
|
const total = rooms.length + spells.length;
|
||||||
|
if (!total) { this._dealing = false; onDone(); return; }
|
||||||
|
const roomW = 125, spellW = 104;
|
||||||
|
const pitch = Math.min(160, 1500 / total);
|
||||||
|
const width = (total - 1) * pitch;
|
||||||
|
const baseX = GAME_WIDTH / 2 - width / 2, y = 985;
|
||||||
|
const plan = [
|
||||||
|
...rooms.map((inst, i) => ({ inst, kind: 'room', x: baseX + i * pitch, y, w: roomW })),
|
||||||
|
...spells.map((inst, i) => ({ inst, kind: 'spell', x: baseX + (rooms.length + i) * pitch, y: y - 16, w: spellW })),
|
||||||
|
];
|
||||||
|
const landed = [];
|
||||||
|
const dealNext = (i) => {
|
||||||
|
if (i >= plan.length) {
|
||||||
|
this._dealing = false;
|
||||||
|
this.renderAll();
|
||||||
|
landed.forEach((c) => c.destroy());
|
||||||
|
onDone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const step = plan[i];
|
||||||
|
this.flyCardFromDeck(step.kind, step.inst, step.x, step.y, step.w, (cont) => {
|
||||||
|
landed.push(cont);
|
||||||
|
dealNext(i + 1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
dealNext(0);
|
||||||
|
}
|
||||||
|
|
||||||
renderHand() {
|
renderHand() {
|
||||||
|
if (this._dealing) return;
|
||||||
const p = this.gs.players[this.humanSeat];
|
const p = this.gs.players[this.humanSeat];
|
||||||
if (!p.alive) return;
|
if (!p.alive) return;
|
||||||
const rooms = p.hand.rooms;
|
const rooms = p.hand.rooms;
|
||||||
|
|
@ -1340,6 +1447,13 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
playEvents(events) {
|
playEvents(events) {
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
this.setPrompt('');
|
this.setPrompt('');
|
||||||
|
// A single action can emit several same-kind 'draw' events for the human
|
||||||
|
// (e.g. draw 2 rooms); the event carries no card uid, so figure out up
|
||||||
|
// front how many of each kind are coming, and consume them in order from
|
||||||
|
// the end of the (already fully mutated) hand array as each is played.
|
||||||
|
this._humanDrawBatch = { room: 0, spell: 0 };
|
||||||
|
this._humanDrawSeen = { room: 0, spell: 0 };
|
||||||
|
for (const e of events) if (e.type === 'draw' && e.seat === this.humanSeat) this._humanDrawBatch[e.card]++;
|
||||||
const queue = events.filter((e) => this.eventDelay(e) > 0 || this.eventFx(e, true));
|
const queue = events.filter((e) => this.eventDelay(e) > 0 || this.eventFx(e, true));
|
||||||
const step = () => {
|
const step = () => {
|
||||||
const e = queue.shift();
|
const e = queue.shift();
|
||||||
|
|
@ -1376,6 +1490,7 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
case 'roomDestroyed': return 320;
|
case 'roomDestroyed': return 320;
|
||||||
case 'roomFrozen': return 300;
|
case 'roomFrozen': return 300;
|
||||||
case 'trapArmed': return 260;
|
case 'trapArmed': return 260;
|
||||||
|
case 'draw': return 260;
|
||||||
default: return 0;
|
default: return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1397,6 +1512,19 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
case 'heroRevealed':
|
case 'heroRevealed':
|
||||||
this.sfx(SFX.CARD_DEAL);
|
this.sfx(SFX.CARD_DEAL);
|
||||||
break;
|
break;
|
||||||
|
case 'draw': {
|
||||||
|
if (e.seat !== this.humanSeat) { this.sfx(SFX.CARD_DEAL); break; }
|
||||||
|
const p = gs.players[e.seat];
|
||||||
|
const list = e.card === 'room' ? p.hand.rooms : p.hand.spells;
|
||||||
|
const total = this._humanDrawBatch[e.card] || 1;
|
||||||
|
const seen = this._humanDrawSeen[e.card]++;
|
||||||
|
const inst = list[list.length - total + seen];
|
||||||
|
const sprite = inst && this._handSprites.get(inst.uid);
|
||||||
|
if (inst && sprite) {
|
||||||
|
this.flyCardFromDeck(e.card, inst, sprite.x, sprite.y, e.card === 'room' ? 125 : 104, (cont) => cont.destroy());
|
||||||
|
} else this.sfx(SFX.CARD_DEAL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'heroWalks': {
|
case 'heroWalks': {
|
||||||
const from = this._heroTokens.get(e.uid);
|
const from = this._heroTokens.get(e.uid);
|
||||||
const hero = this.findHero(e.uid) || { uid: e.uid, id: e.id, hp: 0 };
|
const hero = this.findHero(e.uid) || { uid: e.uid, id: e.id, hp: 0 };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue