feat: add day transition banner animation between turns

Import and play day banner animation when transitioning between turns
in Advance Wars game. The banner displays the current day number and
plays before allowing the next turn to begin, providing visual feedback
for day progression. Added safety check to cancel animation if scene
changes during playback.
This commit is contained in:
Brian Fertig 2026-07-19 19:05:07 -06:00
parent f96f2d491c
commit 8e59c3b1bf
2 changed files with 90 additions and 0 deletions

View File

@ -0,0 +1,85 @@
// Advance Wars "Day N" banner — shown once per full day cycle, right as
// control returns to the player after the last enemy army's turn. Each
// letter flies in on its own (alternating in from above/below with a
// dramatic overshoot), the full word holds for 1.5s, then every letter
// flies back out the way it came. Tap to skip.
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
import { FONT } from './AdvanceWarsUI.js';
const SIZE = 150;
const HOLD_MS = 1500;
const STAGGER_MS = 70;
const FLY_DIST = 420;
const IN_DURATION = 420;
const OUT_DURATION = 380;
export function playDayBanner(scene, day, onDone) {
const text = `DAY ${day}`;
const cy = GAME_HEIGHT / 2;
const objs = [];
let finished = false;
const dim = scene.add.rectangle(GAME_WIDTH / 2, cy, GAME_WIDTH, 260, 0x000000, 0.55)
.setDepth(60).setInteractive();
objs.push(dim);
const style = {
fontFamily: FONT, fontSize: `${SIZE}px`, color: '#ffffff',
stroke: '#000000', strokeThickness: 10,
};
const spaceWidth = SIZE * 0.45;
const gap = 6;
// lay every non-space letter out at its final centered position first
// (measuring natural glyph width as each is created), then animate each
// one in from off-screen to that spot.
const letters = [];
let cursor = 0;
for (const ch of text) {
if (ch === ' ') { cursor += spaceWidth + gap; continue; }
const t = scene.add.text(0, cy, ch, style).setOrigin(0, 0.5).setDepth(61).setAlpha(0);
letters.push({ t, x: cursor });
cursor += t.width + gap;
objs.push(t);
}
const totalWidth = cursor - gap;
const startX = GAME_WIDTH / 2 - totalWidth / 2;
for (const l of letters) l.x += startX;
const cleanup = () => {
if (finished) return;
finished = true;
for (const o of objs) o.destroy();
onDone?.();
};
dim.on('pointerdown', cleanup);
letters.forEach(({ t, x }, i) => {
const fromAbove = i % 2 === 0;
t.setPosition(x, cy + (fromAbove ? -FLY_DIST : FLY_DIST));
t.setScale(2.2);
t.setRotation(fromAbove ? -0.6 : 0.6);
scene.tweens.add({
targets: t, x, y: cy, scale: 1, rotation: 0, alpha: 1,
delay: i * STAGGER_MS, duration: IN_DURATION, ease: 'Back.easeOut',
});
});
const inTotal = (letters.length - 1) * STAGGER_MS + IN_DURATION;
scene.time.delayedCall(inTotal + HOLD_MS, () => {
if (finished) return;
letters.forEach(({ t }, i) => {
const toAbove = i % 2 !== 0;
scene.tweens.add({
targets: t, y: cy + (toAbove ? -FLY_DIST : FLY_DIST), scale: 2.2,
rotation: toAbove ? 0.6 : -0.6, alpha: 0,
delay: i * STAGGER_MS, duration: OUT_DURATION, ease: 'Back.easeIn',
});
});
const outTotal = (letters.length - 1) * STAGGER_MS + OUT_DURATION;
scene.time.delayedCall(outTotal + 80, cleanup);
});
return { skip: cleanup };
}

View File

@ -17,6 +17,7 @@ import { AdvanceWarsHUD, ActionMenu, DamagePreview, ProductionMenu, TileInfo, mk
import * as Screens from './AdvanceWarsScreens.js';
import { playBattleAnim } from './AdvanceWarsBattleAnim.js';
import { playCaptureAnim } from './AdvanceWarsCaptureAnim.js';
import { playDayBanner } from './AdvanceWarsDayBanner.js';
const SAVE_KEY = 'advancewars-save';
const ANIM_KEY = 'advancewars-battle-anims';
@ -507,6 +508,10 @@ export default class AdvanceWarsGame extends Phaser.Scene {
await this.replayEvents(log, { enemy: true });
}
if (this.run !== run) return;
if (!run.state.result) {
await new Promise((resolve) => playDayBanner(this, run.state.day, resolve));
}
if (this.run !== run) return;
run.busy = false;
this.refreshAll();
if (run.state.result) { this.onMissionOver(); return; }