Compare commits

...

2 Commits

Author SHA1 Message Date
Brian Fertig 8e59c3b1bf 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.
2026-07-19 19:05:07 -06:00
Brian Fertig f96f2d491c feat(advancewars): add HP text display to battle animations and procedural cursor
- Add HP text readout to battle animations showing 1-10 scale HP
  (matching Advance Wars' classic HP display), with smooth tweening
- Extract cursor drawing into procedural texture, keeping it independent
  of the UI sheet
- Add advancewars-ui.png sprite sheet and update artwork config
2026-07-19 19:04:14 -06:00
7 changed files with 142 additions and 11 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

View File

@ -20,7 +20,7 @@
},
"uiSheet": {
"key": "advancewars-ui",
"path": null,
"path": "assets/images/advancewars/advancewars-ui.png",
"frameWidth": 48,
"frameHeight": 48
},

View File

@ -10,6 +10,7 @@ import { playSound } from '../../ui/Sounds.js';
const W = 760;
const H = 380;
const HP_TEXT_PADDING = 20;
// ---------------------------------------------------------------------------
// Battle backgrounds: one backdrop image per terrain/building a fighter can
@ -232,7 +233,14 @@ export function playBattleAnim(scene, rules, state, texKey, battles, onDone) {
const bar = scene.add.rectangle(cx + side * W / 4 - 108, cy - H / 2 + 58, 216, 12,
0x7dff9a, 1).setOrigin(0, 0.5).setDepth(52);
objs.push(barBg, bar);
return { img, bar, spec };
// remaining-HP readout in the outer top corner of this fighter's half —
// same font/size as the capture cut-in's points counter. 1-10 scale
// (AW's classic HP display), zero being the floor.
const hpText = scene.add.text(cx + side * W / 2 - side * HP_TEXT_PADDING, cy - H / 2 + HP_TEXT_PADDING, '',
{ fontFamily: FONT, fontSize: '64px', color: '#ffe14d', stroke: '#000000', strokeThickness: 6 })
.setOrigin(side > 0 ? 1 : 0, 0).setDepth(51);
objs.push(hpText);
return { img, bar, spec, hpText };
};
// starting HP = hp before the volleys (event carries post-hit hp; rebuild)
@ -249,8 +257,18 @@ export function playBattleAnim(scene, rules, state, texKey, battles, onDone) {
f.bar.width = Math.max(0, 216 * (hp / 100));
f.bar.setFillStyle(hp > 50 ? 0x7dff9a : hp > 25 ? 0xffe14d : 0xff5a5a, 1);
};
const hpDisplay = (hp) => Math.max(0, Math.ceil(hp / 10));
const animateHp = (f, fromHp, toHp) => {
const proxy = { hp: fromHp };
scene.tweens.add({
targets: proxy, hp: toHp, duration: 500, ease: 'Sine.easeOut',
onUpdate: () => f.hpText.setText(String(hpDisplay(proxy.hp))),
});
};
setBar(left, leftHp);
setBar(right, rightHp);
left.hpText.setText(String(hpDisplay(leftHp)));
right.hpText.setText(String(hpDisplay(rightHp)));
const flip = scene.time.addEvent({
delay: 220, loop: true, callback: () => {
@ -290,8 +308,17 @@ export function playBattleAnim(scene, rules, state, texKey, battles, onDone) {
objs.push(flash);
scene.tweens.add({ targets: flash, alpha: 0, duration: 450, onComplete: () => flash.destroy() });
scene.cameras.main.shake(120, 0.004);
if (fromLeft) { rightHp = b.defender.hp; setBar(right, rightHp); }
else { leftHp = b.defender.hp; setBar(left, leftHp); }
if (fromLeft) {
const prevHp = rightHp;
rightHp = b.defender.hp;
setBar(right, rightHp);
animateHp(right, prevHp, rightHp);
} else {
const prevHp = leftHp;
leftHp = b.defender.hp;
setBar(left, leftHp);
animateHp(left, prevHp, leftHp);
}
if (b.killed) {
scene.tweens.add({ targets: victim.img, alpha: 0, y: victim.img.y + 16, duration: 550 });
}

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; }

View File

@ -101,6 +101,25 @@ function mkCanvasSheet(scene, key, spec, count) {
const ground = (c, color) => { c.fillStyle = color; c.fillRect(0, 0, CELL, CELL); };
// Tile cursor is always the built-in procedural corner-bracket art — kept
// independent of the UI sheet even once it's painted, per explicit request.
function drawCursor(c) {
c.strokeStyle = '#ffffff'; c.lineWidth = 4;
for (const [sx, sy, dx, dy] of [[3, 3, 1, 1], [45, 3, -1, 1], [3, 45, 1, -1], [45, 45, -1, -1]]) {
c.beginPath(); c.moveTo(sx + dx * 12, sy); c.lineTo(sx, sy); c.lineTo(sx, sy + dy * 12); c.stroke();
}
}
const CURSOR_TEX_KEY = 'advancewars-cursor-proc';
function ensureCursorTexture(scene) {
if (scene.textures.exists(CURSOR_TEX_KEY)) return CURSOR_TEX_KEY;
const tex = scene.textures.createCanvas(CURSOR_TEX_KEY, CELL, CELL);
drawCursor(tex.getContext());
tex.refresh();
return CURSOR_TEX_KEY;
}
const PROC_PAINTERS = {
terrain(scene, key, spec) {
const { at, finish } = mkCanvasSheet(scene, key, spec, spec.frames);
@ -180,12 +199,7 @@ const PROC_PAINTERS = {
ui(scene, key, spec) {
const { at, finish } = mkCanvasSheet(scene, key, spec, spec.frames);
at(UI_FRAMES.cursor, (c) => {
c.strokeStyle = '#ffffff'; c.lineWidth = 4;
for (const [sx, sy, dx, dy] of [[3, 3, 1, 1], [45, 3, -1, 1], [3, 45, 1, -1], [45, 45, -1, -1]]) {
c.beginPath(); c.moveTo(sx + dx * 12, sy); c.lineTo(sx, sy); c.lineTo(sx, sy + dy * 12); c.stroke();
}
});
at(UI_FRAMES.cursor, drawCursor);
const boom = (r) => (c) => {
c.fillStyle = '#ffb02f';
for (let i = 0; i < 8; i++) {
@ -244,7 +258,7 @@ export class AdvanceWarsMapView {
this.atkG = scene.add.graphics().setDepth(this.depths.atkOv);
this.fogG = scene.add.graphics().setDepth(this.depths.fog);
this.pathG = scene.add.graphics().setDepth(this.depths.path);
this.cursor = scene.add.image(0, 0, this.tex.ui, UI_FRAMES.cursor)
this.cursor = scene.add.image(0, 0, ensureCursorTexture(scene))
.setDisplaySize(this.tile, this.tile)
.setDepth(this.depths.cursor).setVisible(false);
this.syncUnits();