305 lines
13 KiB
JavaScript
305 lines
13 KiB
JavaScript
// Advance Wars HUD + menus: top strip (funds/day/objective), CO panel with
|
|
// power meter, contextual action menu, damage preview, production menu.
|
|
// All text in the arcade pixel font.
|
|
|
|
import * as Phaser from 'phaser';
|
|
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
|
|
import * as Logic from './AdvanceWarsLogic.js';
|
|
import { UI_FRAMES, armyColorInt } from './AdvanceWarsMapView.js';
|
|
import { createOpponentPortrait } from '../../ui/Portrait.js';
|
|
|
|
export const FONT = 'm6x11, "Julius Sans One"';
|
|
const PANEL = 0x141826;
|
|
const PANEL_EDGE = 0x3a4260;
|
|
|
|
export function mkText(scene, x, y, text, size, color = '#ffffff', origin = [0, 0.5]) {
|
|
return scene.add.text(x, y, text, {
|
|
fontFamily: FONT, fontSize: `${size}px`, color,
|
|
}).setOrigin(origin[0], origin[1]);
|
|
}
|
|
|
|
export function mkButton(scene, x, y, w, h, label, onClick, { size = 24, color = 0x2a3350, textColor = '#ffffff', depth = 40 } = {}) {
|
|
const c = scene.add.container(x, y).setDepth(depth);
|
|
const bg = scene.add.rectangle(0, 0, w, h, color, 1).setStrokeStyle(2, PANEL_EDGE);
|
|
const txt = mkText(scene, 0, 0, label, size, textColor, [0.5, 0.5]);
|
|
c.add([bg, txt]);
|
|
c.setSize(w, h);
|
|
c.setInteractive({ useHandCursor: true });
|
|
c.on('pointerover', () => bg.setFillStyle(lighten(color), 1));
|
|
c.on('pointerout', () => bg.setFillStyle(color, 1));
|
|
c.on('pointerdown', (p, lx, ly, ev) => { ev?.stopPropagation(); onClick(); });
|
|
c.bg = bg; c.label = txt;
|
|
return c;
|
|
}
|
|
|
|
function lighten(color) {
|
|
const r = Math.min(255, ((color >> 16) & 0xff) + 30);
|
|
const g = Math.min(255, ((color >> 8) & 0xff) + 30);
|
|
const b = Math.min(255, (color & 0xff) + 30);
|
|
return (r << 16) | (g << 8) | b;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export class AdvanceWarsHUD {
|
|
constructor(scene, rules, { onEndTurn, onMenu, onPower }) {
|
|
this.scene = scene;
|
|
this.rules = rules;
|
|
const d = 30;
|
|
|
|
this.bar = scene.add.rectangle(GAME_WIDTH / 2, 34, GAME_WIDTH, 68, PANEL, 0.96)
|
|
.setStrokeStyle(2, PANEL_EDGE).setDepth(d);
|
|
this.dayText = mkText(scene, 28, 34, '', 30).setDepth(d);
|
|
this.fundsText = mkText(scene, 190, 34, '', 30, '#ffe14d').setDepth(d);
|
|
this.objectiveText = mkText(scene, 470, 34, '', 22, '#9db4e8').setDepth(d);
|
|
this.turnText = mkText(scene, GAME_WIDTH / 2 + 210, 34, '', 26, '#ffffff', [0.5, 0.5]).setDepth(d);
|
|
|
|
this.endTurnBtn = mkButton(scene, GAME_WIDTH - 330, 34, 170, 46, 'END TURN', onEndTurn, { size: 24 });
|
|
this.menuBtn = mkButton(scene, GAME_WIDTH - 120, 34, 130, 46, 'MENU', onMenu, { size: 24 });
|
|
|
|
// CO panel (right column)
|
|
const px = GAME_WIDTH - 148;
|
|
this.coPanel = scene.add.rectangle(px, 620, 268, 1080 - 90, PANEL, 0.92)
|
|
.setStrokeStyle(2, PANEL_EDGE).setDepth(d - 1);
|
|
this.coName = mkText(scene, px, 210, '', 28, '#ffffff', [0.5, 0.5]).setDepth(d);
|
|
this.coTag = mkText(scene, px, 244, '', 16, '#8f9dc4', [0.5, 0]).setDepth(d);
|
|
this.coTag.setWordWrapWidth(240);
|
|
this.powerLabel = mkText(scene, px, 330, '', 20, '#ffd94d', [0.5, 0.5]).setDepth(d);
|
|
this.stars = [];
|
|
this.powerBtn = mkButton(scene, px, 395, 220, 52, 'POWER!', onPower, { size: 26, color: 0x8a4a10 });
|
|
this.powerBtn.setVisible(false);
|
|
|
|
this.enemyName = mkText(scene, px, 640, '', 24, '#ff9d9d', [0.5, 0.5]).setDepth(d);
|
|
this.enemyPowerLabel = mkText(scene, px, 810, '', 18, '#c99', [0.5, 0.5]).setDepth(d);
|
|
this.enemyStars = [];
|
|
|
|
this.portraits = [];
|
|
}
|
|
|
|
attachPortraits(playerOpp, enemyOpp) {
|
|
const px = GAME_WIDTH - 148;
|
|
for (const p of this.portraits) p?.destroy?.();
|
|
this.portraits = [
|
|
createOpponentPortrait(this.scene, playerOpp, px, 140, 56, 31, { playIntro: false }),
|
|
enemyOpp ? createOpponentPortrait(this.scene, enemyOpp, px, 720, 56, 31, { playIntro: false }) : null,
|
|
];
|
|
}
|
|
|
|
buildStars(rules, state) {
|
|
for (const s of [...this.stars, ...this.enemyStars]) s.destroy();
|
|
this.stars = []; this.enemyStars = [];
|
|
const px = GAME_WIDTH - 148;
|
|
const mk = (co, y, arr) => {
|
|
const n = rules.coById[co].power?.stars ?? 0;
|
|
const total = n * 24;
|
|
for (let i = 0; i < n; i++) {
|
|
const img = this.scene.add.image(px - total / 2 + 12 + i * 24, y, this.uiKey ?? 'advancewars-ui-proc', UI_FRAMES.star)
|
|
.setDisplaySize(22, 22).setDepth(31);
|
|
arr.push(img);
|
|
}
|
|
};
|
|
mk(state.armies[0].co, 292, this.stars);
|
|
if (state.armies[1]) mk(state.armies[1].co, 775, this.enemyStars);
|
|
}
|
|
|
|
refresh(state, humanArmy = 0) {
|
|
const rules = this.rules;
|
|
const a = state.armies[humanArmy];
|
|
const co = rules.coById[a.co];
|
|
this.dayText.setText(`DAY ${state.day}`);
|
|
this.fundsText.setText(`G ${a.funds.toLocaleString()}`);
|
|
this.turnText.setText(state.turn === humanArmy ? 'YOUR TURN' : `${rules.coById[state.armies[state.turn].co].coName}'S TURN`);
|
|
this.turnText.setColor(state.turn === humanArmy ? '#7dff9a' : '#ff9d9d');
|
|
this.coName.setText(co.coName);
|
|
this.coTag.setText(co.tagline ?? '');
|
|
const powered = a.powerActive;
|
|
this.powerLabel.setText(powered ? `${co.power?.name ?? ''} ACTIVE!` : (co.power?.name ?? ''));
|
|
|
|
const setStars = (army, arr) => {
|
|
const st = state.armies[army];
|
|
const def = rules.coById[st.co];
|
|
if (!def.power) return;
|
|
const per = rules.constants.starCharge;
|
|
arr.forEach((img, i) => {
|
|
const filled = st.charge >= (i + 1) * per;
|
|
img.setAlpha(filled ? 1 : 0.25);
|
|
});
|
|
};
|
|
setStars(humanArmy, this.stars);
|
|
if (state.armies[1]) {
|
|
const eco = rules.coById[state.armies[1].co];
|
|
this.enemyName.setText(eco.coName);
|
|
this.enemyPowerLabel.setText(eco.power?.name ?? '');
|
|
setStars(1, this.enemyStars);
|
|
}
|
|
this.powerBtn.setVisible(state.turn === humanArmy && Logic.powerReady(rules, state, humanArmy));
|
|
const myTurn = state.turn === humanArmy && !state.result;
|
|
this.endTurnBtn.setAlpha(myTurn ? 1 : 0.4);
|
|
}
|
|
|
|
setObjective(text) { this.objectiveText.setText(text); }
|
|
|
|
destroy() {
|
|
for (const p of this.portraits) p?.destroy?.();
|
|
for (const s of [...this.stars, ...this.enemyStars]) s.destroy();
|
|
for (const o of [this.bar, this.dayText, this.fundsText, this.objectiveText, this.turnText,
|
|
this.endTurnBtn, this.menuBtn, this.coPanel, this.coName, this.coTag, this.powerLabel,
|
|
this.powerBtn, this.enemyName, this.enemyPowerLabel]) o?.destroy();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Contextual action menu — big touch-friendly vertical buttons.
|
|
|
|
export class ActionMenu {
|
|
constructor(scene) {
|
|
this.scene = scene;
|
|
this.items = [];
|
|
}
|
|
|
|
open(x, y, options) {
|
|
this.close();
|
|
const w = 210, h = 52, gap = 6;
|
|
const total = options.length * (h + gap);
|
|
let top = Math.min(Math.max(y - total / 2, 90), GAME_HEIGHT - total - 20);
|
|
const left = Math.min(x + 30, GAME_WIDTH - 300 - w);
|
|
options.forEach((opt, i) => {
|
|
const btn = mkButton(this.scene, left + w / 2, top + i * (h + gap) + h / 2, w, h,
|
|
opt.label, () => { this.close(); opt.cb(); },
|
|
{ size: 24, color: opt.danger ? 0x6b2a2a : 0x2a3350, depth: 45 });
|
|
if (opt.disabled) { btn.setAlpha(0.45); btn.disableInteractive(); }
|
|
this.items.push(btn);
|
|
});
|
|
}
|
|
|
|
close() {
|
|
for (const b of this.items) b.destroy();
|
|
this.items = [];
|
|
}
|
|
|
|
get isOpen() { return this.items.length > 0; }
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Damage preview panel
|
|
|
|
export class DamagePreview {
|
|
constructor(scene) {
|
|
this.scene = scene;
|
|
this.objs = [];
|
|
}
|
|
|
|
show(x, y, dealPct, counterPct, onConfirm, onCancel) {
|
|
this.hide();
|
|
const w = 300, h = 170;
|
|
const px = Math.min(Math.max(x, w / 2 + 20), GAME_WIDTH - 300 - w / 2);
|
|
const py = Math.min(Math.max(y - 140, 100), GAME_HEIGHT - h - 20);
|
|
const panel = this.scene.add.rectangle(px, py + h / 2, w, h, PANEL, 0.97)
|
|
.setStrokeStyle(2, PANEL_EDGE).setDepth(46);
|
|
const t1 = mkText(this.scene, px, py + 34, `DAMAGE ${dealPct}%`, 28, '#7dff9a', [0.5, 0.5]).setDepth(46);
|
|
const t2 = mkText(this.scene, px, py + 70, counterPct == null ? 'NO COUNTER' : `COUNTER ${counterPct}%`,
|
|
22, counterPct == null ? '#8f9dc4' : '#ff9d9d', [0.5, 0.5]).setDepth(46);
|
|
const ok = mkButton(this.scene, px - 70, py + 126, 120, 48, 'FIRE!', () => { this.hide(); onConfirm(); },
|
|
{ size: 24, color: 0x8a2a2a, depth: 46 });
|
|
const no = mkButton(this.scene, px + 70, py + 126, 120, 48, 'BACK', () => { this.hide(); onCancel(); },
|
|
{ size: 24, depth: 46 });
|
|
this.objs = [panel, t1, t2, ok, no];
|
|
}
|
|
|
|
hide() { for (const o of this.objs) o.destroy(); this.objs = []; }
|
|
get isOpen() { return this.objs.length > 0; }
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Production menu (modal)
|
|
|
|
export class ProductionMenu {
|
|
constructor(scene, rules, unitsKey) {
|
|
this.scene = scene;
|
|
this.rules = rules;
|
|
this.unitsKey = unitsKey;
|
|
this.objs = [];
|
|
}
|
|
|
|
open(options, funds, onPick, onClose) {
|
|
this.close();
|
|
const rows = options.length;
|
|
const w = 460, rowH = 58, h = rows * rowH + 110;
|
|
const cx = GAME_WIDTH / 2 - 130, cy = GAME_HEIGHT / 2;
|
|
const dim = this.scene.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.45)
|
|
.setDepth(44).setInteractive();
|
|
dim.on('pointerdown', () => { this.close(); onClose?.(); });
|
|
const panel = this.scene.add.rectangle(cx, cy, w, h, PANEL, 0.97)
|
|
.setStrokeStyle(2, PANEL_EDGE).setDepth(45);
|
|
const title = mkText(this.scene, cx, cy - h / 2 + 34, 'BUILD UNIT', 30, '#ffffff', [0.5, 0.5]).setDepth(45);
|
|
const fundsT = mkText(this.scene, cx, cy - h / 2 + 66, `Funds: G ${funds.toLocaleString()}`, 20, '#ffe14d', [0.5, 0.5]).setDepth(45);
|
|
this.objs = [dim, panel, title, fundsT];
|
|
|
|
options.forEach((opt, i) => {
|
|
const spec = this.rules.unitById[opt.type];
|
|
const ry = cy - h / 2 + 100 + i * rowH + rowH / 2;
|
|
const row = this.scene.add.rectangle(cx, ry, w - 30, rowH - 8, opt.affordable ? 0x223050 : 0x1a1f30, 1)
|
|
.setStrokeStyle(1, PANEL_EDGE).setDepth(45);
|
|
const icon = this.scene.add.image(cx - w / 2 + 50, ry + 12, this.unitsKey, spec.frame)
|
|
.setOrigin(0.5, 0.78).setScale(0.8).setDepth(45);
|
|
const name = mkText(this.scene, cx - w / 2 + 95, ry, spec.name, 24,
|
|
opt.affordable ? '#ffffff' : '#666e88').setDepth(45);
|
|
const cost = mkText(this.scene, cx + w / 2 - 40, ry, `G ${opt.cost.toLocaleString()}`, 22,
|
|
opt.affordable ? '#ffe14d' : '#666e88', [1, 0.5]).setDepth(45);
|
|
this.objs.push(row, icon, name, cost);
|
|
if (opt.affordable) {
|
|
row.setInteractive({ useHandCursor: true });
|
|
row.on('pointerover', () => row.setFillStyle(0x2e4070, 1));
|
|
row.on('pointerout', () => row.setFillStyle(0x223050, 1));
|
|
row.on('pointerdown', (p, lx, ly, ev) => { ev?.stopPropagation(); this.close(); onPick(opt.type); });
|
|
}
|
|
});
|
|
}
|
|
|
|
close() { for (const o of this.objs) o.destroy(); this.objs = []; }
|
|
get isOpen() { return this.objs.length > 0; }
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tile info chip (bottom-left): terrain name, stars, capture progress.
|
|
|
|
export class TileInfo {
|
|
constructor(scene, rules) {
|
|
this.scene = scene;
|
|
this.rules = rules;
|
|
const d = 30;
|
|
this.panel = scene.add.rectangle(150, GAME_HEIGHT - 60, 280, 96, PANEL, 0.92)
|
|
.setStrokeStyle(2, PANEL_EDGE).setDepth(d);
|
|
this.name = mkText(scene, 36, GAME_HEIGHT - 84, '', 24).setDepth(d);
|
|
this.detail = mkText(scene, 36, GAME_HEIGHT - 50, '', 18, '#9db4e8').setDepth(d);
|
|
this.unitLine = mkText(scene, 36, GAME_HEIGHT - 26, '', 18, '#ffe14d').setDepth(d);
|
|
}
|
|
|
|
show(state, x, y, spottedUnit) {
|
|
const t = Logic.terrainAt(this.rules, state, x, y);
|
|
const k = Logic.tileKey(state, x, y);
|
|
let name = t.name;
|
|
if (t.property) {
|
|
const owner = state.owner[k];
|
|
name += owner < 0 ? ' (Neutral)' : ` (${this.rules.constants.armyNames[owner] ?? 'Army'})`;
|
|
}
|
|
this.name.setText(name);
|
|
let detail = `Defense ${'★'.repeat(t.stars) || '—'}`;
|
|
if (t.property && state.captureHp[k] < this.rules.constants.captureGoal) {
|
|
detail += ` Capture ${state.captureHp[k]}/${this.rules.constants.captureGoal}`;
|
|
}
|
|
this.detail.setText(detail);
|
|
if (spottedUnit) {
|
|
const spec = this.rules.unitById[spottedUnit.type];
|
|
const bits = [`${spec.name} ${Logic.hpDisplay(spottedUnit)}/10`];
|
|
bits.push(`F${spottedUnit.fuel}`);
|
|
if (spec.ammo > 0) bits.push(`A${spottedUnit.ammo}`);
|
|
this.unitLine.setText(bits.join(' '));
|
|
} else {
|
|
this.unitLine.setText('');
|
|
}
|
|
}
|
|
|
|
destroy() { for (const o of [this.panel, this.name, this.detail, this.unitLine]) o.destroy(); }
|
|
}
|