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
This commit is contained in:
Brian Fertig 2026-07-19 19:04:14 -06:00
parent c0ad354458
commit f96f2d491c
5 changed files with 52 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": { "uiSheet": {
"key": "advancewars-ui", "key": "advancewars-ui",
"path": null, "path": "assets/images/advancewars/advancewars-ui.png",
"frameWidth": 48, "frameWidth": 48,
"frameHeight": 48 "frameHeight": 48
}, },

View File

@ -10,6 +10,7 @@ import { playSound } from '../../ui/Sounds.js';
const W = 760; const W = 760;
const H = 380; const H = 380;
const HP_TEXT_PADDING = 20;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Battle backgrounds: one backdrop image per terrain/building a fighter can // 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, const bar = scene.add.rectangle(cx + side * W / 4 - 108, cy - H / 2 + 58, 216, 12,
0x7dff9a, 1).setOrigin(0, 0.5).setDepth(52); 0x7dff9a, 1).setOrigin(0, 0.5).setDepth(52);
objs.push(barBg, bar); 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) // 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.width = Math.max(0, 216 * (hp / 100));
f.bar.setFillStyle(hp > 50 ? 0x7dff9a : hp > 25 ? 0xffe14d : 0xff5a5a, 1); 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(left, leftHp);
setBar(right, rightHp); setBar(right, rightHp);
left.hpText.setText(String(hpDisplay(leftHp)));
right.hpText.setText(String(hpDisplay(rightHp)));
const flip = scene.time.addEvent({ const flip = scene.time.addEvent({
delay: 220, loop: true, callback: () => { delay: 220, loop: true, callback: () => {
@ -290,8 +308,17 @@ export function playBattleAnim(scene, rules, state, texKey, battles, onDone) {
objs.push(flash); objs.push(flash);
scene.tweens.add({ targets: flash, alpha: 0, duration: 450, onComplete: () => flash.destroy() }); scene.tweens.add({ targets: flash, alpha: 0, duration: 450, onComplete: () => flash.destroy() });
scene.cameras.main.shake(120, 0.004); scene.cameras.main.shake(120, 0.004);
if (fromLeft) { rightHp = b.defender.hp; setBar(right, rightHp); } if (fromLeft) {
else { leftHp = b.defender.hp; setBar(left, leftHp); } 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) { if (b.killed) {
scene.tweens.add({ targets: victim.img, alpha: 0, y: victim.img.y + 16, duration: 550 }); scene.tweens.add({ targets: victim.img, alpha: 0, y: victim.img.y + 16, duration: 550 });
} }

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); }; 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 = { const PROC_PAINTERS = {
terrain(scene, key, spec) { terrain(scene, key, spec) {
const { at, finish } = mkCanvasSheet(scene, key, spec, spec.frames); const { at, finish } = mkCanvasSheet(scene, key, spec, spec.frames);
@ -180,12 +199,7 @@ const PROC_PAINTERS = {
ui(scene, key, spec) { ui(scene, key, spec) {
const { at, finish } = mkCanvasSheet(scene, key, spec, spec.frames); const { at, finish } = mkCanvasSheet(scene, key, spec, spec.frames);
at(UI_FRAMES.cursor, (c) => { at(UI_FRAMES.cursor, drawCursor);
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 boom = (r) => (c) => { const boom = (r) => (c) => {
c.fillStyle = '#ffb02f'; c.fillStyle = '#ffb02f';
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
@ -244,7 +258,7 @@ export class AdvanceWarsMapView {
this.atkG = scene.add.graphics().setDepth(this.depths.atkOv); this.atkG = scene.add.graphics().setDepth(this.depths.atkOv);
this.fogG = scene.add.graphics().setDepth(this.depths.fog); this.fogG = scene.add.graphics().setDepth(this.depths.fog);
this.pathG = scene.add.graphics().setDepth(this.depths.path); 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) .setDisplaySize(this.tile, this.tile)
.setDepth(this.depths.cursor).setVisible(false); .setDepth(this.depths.cursor).setVisible(false);
this.syncUnits(); this.syncUnits();