feat: add fly-in animation for town hero and epic reveals
Extend the card draw animation system to support town hero and epic reveals. Add hero and epic configurations to DRAW_FULL_W and DECK_PILE_POS. Update flyCardFromDeck to handle hero/epic cards and accept a configurable pause duration. Introduce _pendingHeroRevealUids to suppress town rendering until fly-in animations complete. Enhance heroRevealed event handling to trigger the full fly/flip/shrink sequence. Adjust event timing delays to accommodate the longer animation.
This commit is contained in:
parent
da483541a4
commit
8c8bce814f
|
|
@ -50,11 +50,15 @@ const TOWN_HERO_W = 106;
|
||||||
const OPP_ENTRANCE_W = 46, OPP_ENTRANCE_PITCH = 30;
|
const OPP_ENTRANCE_W = 46, OPP_ENTRANCE_PITCH = 30;
|
||||||
const HUMAN_ENTRANCE_W = 64, HUMAN_ENTRANCE_PITCH = 38;
|
const HUMAN_ENTRANCE_W = 64, HUMAN_ENTRANCE_PITCH = 38;
|
||||||
// Width a drawn card grows to while paused, face down then flipped, in the
|
// 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.
|
// center of the screen mid-draw-animation, before shrinking into the hand
|
||||||
const DRAW_FULL_W = { room: 340, spell: 300 };
|
// (room/spell) or town row (hero/epic).
|
||||||
|
const DRAW_FULL_W = { room: 340, spell: 300, hero: 300, epic: 300 };
|
||||||
// Deck-pile screen positions the draw animation flies cards out of — must
|
// Deck-pile screen positions the draw animation flies cards out of — must
|
||||||
// match renderDeckPiles()'s room/spell rows.
|
// match renderDeckPiles()'s room/spell/hero/epic rows.
|
||||||
const DECK_PILE_POS = { room: { x: 60, y: 520, w: 46, h: 64 }, spell: { x: 60, y: 600, w: 46, h: 64 } };
|
const DECK_PILE_POS = {
|
||||||
|
room: { x: 60, y: 520, w: 46, h: 64 }, spell: { x: 60, y: 600, w: 46, h: 64 },
|
||||||
|
hero: { x: 60, y: 680, w: 46, h: 64 }, epic: { x: 60, y: 760, w: 46, h: 64 },
|
||||||
|
};
|
||||||
|
|
||||||
// ── adventure battle modal: layout constants ────────────────────────────────
|
// ── adventure battle modal: layout constants ────────────────────────────────
|
||||||
// Event types the modal owns end-to-end once a walk starts (see
|
// Event types the modal owns end-to-end once a walk starts (see
|
||||||
|
|
@ -292,6 +296,10 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
// animation hasn't landed yet — see renderHand's placeholder and
|
// animation hasn't landed yet — see renderHand's placeholder and
|
||||||
// eventFx's 'draw' case / renderHandOnly.
|
// eventFx's 'draw' case / renderHandOnly.
|
||||||
this._pendingDrawUids = new Set();
|
this._pendingDrawUids = new Set();
|
||||||
|
// Town reveal overlay: uids of freshly-drawn town heroes whose
|
||||||
|
// fly-in-from-deck animation hasn't landed yet — see renderTown's
|
||||||
|
// placeholder and eventFx's 'heroRevealed' case.
|
||||||
|
this._pendingHeroRevealUids = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -931,7 +939,8 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
|
|
||||||
// Flies one card from its deck pile to the center of the screen at full
|
// 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
|
// 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
|
// the hand (room/spell) or town row (hero/epic). Used for the initial hand
|
||||||
|
// deal, any later room/spell draw, and hero/epic town reveals. The
|
||||||
// container is built once at full size and only ever scaled (never
|
// container is built once at full size and only ever scaled (never
|
||||||
// redrawn), so the back/front art stays crisp through every phase.
|
// redrawn), so the back/front art stays crisp through every phase.
|
||||||
// onComplete receives the landed container — it is NOT auto-destroyed,
|
// onComplete receives the landed container — it is NOT auto-destroyed,
|
||||||
|
|
@ -939,7 +948,7 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
// hand render is suppressed until every card has landed); callers that
|
// 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
|
// don't need that (e.g. a single mid-game draw, where the real card is
|
||||||
// already rendered underneath) should destroy it themselves.
|
// already rendered underneath) should destroy it themselves.
|
||||||
flyCardFromDeck(kind, inst, targetX, targetY, targetW, onComplete) {
|
flyCardFromDeck(kind, inst, targetX, targetY, targetW, onComplete, { pauseMs = 1200 } = {}) {
|
||||||
const pile = DECK_PILE_POS[kind];
|
const pile = DECK_PILE_POS[kind];
|
||||||
const { w: fw, h: fh } = this.fitCardBox(DRAW_FULL_W[kind], 0, CARD_ASPECT[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);
|
const cont = this.add.container(pile.x, pile.y).setScale(pile.w / fw);
|
||||||
|
|
@ -956,11 +965,12 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
back.destroy();
|
back.destroy();
|
||||||
this.sfx(SFX.CARD_SHOW);
|
this.sfx(SFX.CARD_SHOW);
|
||||||
if (kind === 'room') this.makeRoomCard(0, 0, inst, fw, fh, { parent: cont, showText: true, hover: false, noHoverPreview: true });
|
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 });
|
else if (kind === 'spell') this.makeSpellCard(0, 0, inst, fw, fh, { parent: cont, hover: false, noHoverPreview: true });
|
||||||
|
else this.makeHeroCard(0, 0, inst, fw, fh, { parent: cont, hover: false, noHoverPreview: true });
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: cont, scaleX: 1, duration: 110, ease: 'Sine.easeOut',
|
targets: cont, scaleX: 1, duration: 110, ease: 'Sine.easeOut',
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
this.time.delayedCall(1200, () => {
|
this.time.delayedCall(pauseMs, () => {
|
||||||
const handScale = targetW / fw;
|
const handScale = targetW / fw;
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: cont, x: targetX, y: targetY, scale: handScale,
|
targets: cont, x: targetX, y: targetY, scale: handScale,
|
||||||
|
|
@ -1341,6 +1351,10 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
roster.forEach((hero, i) => {
|
roster.forEach((hero, i) => {
|
||||||
const { x, y } = this.townPos(i, n);
|
const { x, y } = this.townPos(i, n);
|
||||||
this._heroTokens.set(hero.uid, { x, y });
|
this._heroTokens.set(hero.uid, { x, y });
|
||||||
|
// Still mid fly-in-from-deck reveal (see eventFx's 'heroRevealed' case)
|
||||||
|
// — leave this slot empty until it lands; _heroTokens above still
|
||||||
|
// records where it'll land.
|
||||||
|
if (this._pendingHeroRevealUids.has(hero.uid)) return;
|
||||||
this.makeHeroCard(x, y, hero, TOWN_HERO_W, TOWN_HERO_W / CARD_ASPECT.hero, {
|
this.makeHeroCard(x, y, hero, TOWN_HERO_W, TOWN_HERO_W / CARD_ASPECT.hero, {
|
||||||
parent: this.townLayer,
|
parent: this.townLayer,
|
||||||
highlight: this.isHeroTarget(hero.uid),
|
highlight: this.isHeroTarget(hero.uid),
|
||||||
|
|
@ -3463,6 +3477,10 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
const list = humanHand.hand.spells;
|
const list = humanHand.hand.spells;
|
||||||
for (let i = list.length - this._humanDrawBatch.spell; i < list.length; i++) this._pendingDrawUids.add(list[i].uid);
|
for (let i = list.length - this._humanDrawBatch.spell; i < list.length; i++) this._pendingDrawUids.add(list[i].uid);
|
||||||
}
|
}
|
||||||
|
// Freshly-drawn town heroes (already pushed into gs.town) stay hidden
|
||||||
|
// from the real town render until their own fly-in-from-deck animation
|
||||||
|
// lands — see renderTown's placeholder and eventFx's 'heroRevealed' case.
|
||||||
|
this._pendingHeroRevealUids = new Set(events.filter((e) => e.type === 'heroRevealed').map((e) => e.uid));
|
||||||
// Heroes baited to walk to an entrance this batch: keep each one's town
|
// Heroes baited to walk to an entrance this batch: keep each one's town
|
||||||
// card in place and hide it from the (already-mutated) real entrance
|
// card in place and hide it from the (already-mutated) real entrance
|
||||||
// render until its own walk step actually fires — see renderTown's
|
// render until its own walk step actually fires — see renderTown's
|
||||||
|
|
@ -3501,7 +3519,10 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case 'flip': return 260;
|
case 'flip': return 260;
|
||||||
case 'roomBuilt': return 240;
|
case 'roomBuilt': return 240;
|
||||||
case 'heroRevealed': return 140;
|
// Full fly/flip/pause/shrink reveal (see eventFx) — long enough that if
|
||||||
|
// several heroes are revealed in the same round-start batch, each one's
|
||||||
|
// reveal finishes before the next one's flight begins.
|
||||||
|
case 'heroRevealed': return 1850;
|
||||||
case 'heroWalks': return 1500;
|
case 'heroWalks': return 1500;
|
||||||
case 'heroEnters': return 200;
|
case 'heroEnters': return 200;
|
||||||
case 'roomHits': return 340;
|
case 'roomHits': return 340;
|
||||||
|
|
@ -3542,9 +3563,18 @@ export default class DungeonBossGame extends Phaser.Scene {
|
||||||
if (r) this.popText(r.x, r.y, ROOMS[e.id]?.name || '', '#f2ead8');
|
if (r) this.popText(r.x, r.y, ROOMS[e.id]?.name || '', '#f2ead8');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'heroRevealed':
|
case 'heroRevealed': {
|
||||||
this.sfx(SFX.CARD_DEAL);
|
const idx = gs.town.findIndex((h) => h.uid === e.uid);
|
||||||
|
const hero = gs.town[idx];
|
||||||
|
if (idx < 0 || !hero) { this.sfx(SFX.CARD_DEAL); break; }
|
||||||
|
const { x, y } = this.townPos(idx, gs.town.length);
|
||||||
|
this.flyCardFromDeck(e.epic ? 'epic' : 'hero', hero, x, y, TOWN_HERO_W, (cont) => {
|
||||||
|
this._pendingHeroRevealUids.delete(e.uid);
|
||||||
|
this.renderTownOnly();
|
||||||
|
cont.destroy();
|
||||||
|
}, { pauseMs: 1000 });
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'draw': {
|
case 'draw': {
|
||||||
if (e.seat !== this.humanSeat) { this.sfx(SFX.CARD_DEAL); break; }
|
if (e.seat !== this.humanSeat) { this.sfx(SFX.CARD_DEAL); break; }
|
||||||
const p = gs.players[e.seat];
|
const p = gs.players[e.seat];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue