refactor(mastervega): consolidate HUD buttons into Empire dropdown and fix ghost button styling

- Replace four separate HUD buttons (Research, Diplomacy, Council, Leaders)
  with a single "Empire" dropdown button, freeing HUD space for future
  buttons like "Colonies"
- Add toggleEmpireMenu/openEmpireMenu/closeEmpireMenu with a popover panel
  containing the four options, click-outside-to-dismiss, and ▸/▾ indicator
  on the button label
- Fix ghost button glow washout by introducing GHOST_ALPHA (0.85) constant
  and using it instead of hardcoded 0.3 alpha for ghost button backgrounds
- Change ghost button hover text color from glow color to textDark for
  consistent readability
- Remove unused glowHex from SCHEMES config
This commit is contained in:
Brian Fertig 2026-08-10 21:30:15 -06:00
parent e1b5ae7277
commit 4c3b17610a
2 changed files with 107 additions and 23 deletions

View File

@ -47,6 +47,17 @@ const SAVE_KEY = 'mastervega-save';
const SAVE_SLOT_COUNT = 10;
const saveSlotKey = (i) => `mastervega-save-slot-${i}`;
// The Empire button's own leading open/closed indicator — swapped by
// open/closeEmpireMenu, same ▸/▾ convention VegaTurnReportScreen.js uses
// per-row, applied here to a button label instead. The button's label is
// one centered Phaser Text (VegaButton.js has no separate icon slot), so
// the only way to nudge the triangle further from "Empire" without a new
// component is padding the string itself — the extra spaces push the
// triangle left and "Empire" right by equal amounts as the whole centered
// block widens.
const EMPIRE_CLOSED_LABEL = '▸ Empire';
const EMPIRE_OPEN_LABEL = '▾ Empire';
export default class MasterOfVegaGame extends Phaser.Scene {
constructor() { super('MasterOfVegaGame'); }
@ -648,21 +659,18 @@ export default class MasterOfVegaGame extends Phaser.Scene {
this.hudText = this.add.text(24, 18, '', { fontFamily: FONT, fontSize: '20px', color: '#cfe8ff' });
hud.add(this.hudText);
const mk = (label, x, fn) => {
const b = new Button(this, x, 31, label, this.uiClick(fn), { width: 150, height: 42, fontSize: 18 });
hud.add(b);
return b;
};
mk('Research', GAME_WIDTH - 830, () => this.openModal((done) =>
openResearchScreen(this, this.rules, this.state, this.state.humanIndex, this.art, done)));
mk('Diplomacy', GAME_WIDTH - 670, () => this.openModal((done) =>
openDiplomacyScreen(this, this.rules, this.state, this.state.humanIndex, this.art, done,
() => this.refreshAll())));
mk('Council', GAME_WIDTH - 510, () => this.openModal((done) =>
openCouncilScreen(this, this.rules, this.state, done)));
mk('Leaders', GAME_WIDTH - 350, () => this.openModal((done) =>
openLeaderScreen(this, this.rules, this.state, this.state.humanIndex, this.art, done,
() => this.refreshHud())));
// Research/Diplomacy/Leaders/Council used to be four separate HUD
// buttons; consolidated into one "Empire" dropdown (openEmpireMenu,
// near the game menu below) to leave room in the bar for more empire-
// management buttons later (Brian's plan: "Colonies" next) without the
// row running out of width. Parked immediately left of End Turn, same
// width, so the two read as a pair — the leading ▸/▾ (EMPIRE_CLOSED_LABEL/
// EMPIRE_OPEN_LABEL, swapped by open/closeEmpireMenu) is this button's
// own open/closed indicator, not the same glyph VegaTurnReportScreen.js
// uses per-row, just the same visual convention.
this.empireBtn = new Button(this, GAME_WIDTH - 340, 31, EMPIRE_CLOSED_LABEL,
this.uiClick(() => this.toggleEmpireMenu()), { width: 190, height: 42, fontSize: 18 });
hud.add(this.empireBtn);
// Not this.uiClick — onEndTurn plays its own vega-endturn cue (shared with
// combat's Next round button) rather than the generic click, and a no-op
@ -749,6 +757,72 @@ export default class MasterOfVegaGame extends Phaser.Scene {
this.gameMenuLayer = null;
}
// -------------------------------------------------------------- empire menu
/** Empire toggles a small popover dropping down from directly below it:
* Research, Diplomacy, Leaders, Council, in that order (Brian's ask).
* Same shape as the game menu above click-anywhere-to-dismiss veil,
* ghost-variant item buttons just anchored under the HUD bar instead of
* above the button. Gated by modalOpen like every other HUD button. */
toggleEmpireMenu() {
if (this.modalOpen) return;
if (this.empireMenuLayer) { this.closeEmpireMenu(); return; }
this.openEmpireMenu();
}
openEmpireMenu() {
this.empireBtn.setLabel(EMPIRE_OPEN_LABEL);
const layer = this.add.container(0, 0).setDepth(D.modal);
this.empireMenuLayer = layer;
const catcher = this.add.rectangle(0, 0, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.001)
.setOrigin(0, 0).setInteractive();
catcher.on('pointerup', () => this.closeEmpireMenu());
layer.add(catcher);
const items = [
['Research', () => this.openModal((done) =>
openResearchScreen(this, this.rules, this.state, this.state.humanIndex, this.art, done))],
['Diplomacy', () => this.openModal((done) =>
openDiplomacyScreen(this, this.rules, this.state, this.state.humanIndex, this.art, done,
() => this.refreshAll()))],
['Leaders', () => this.openModal((done) =>
openLeaderScreen(this, this.rules, this.state, this.state.humanIndex, this.art, done,
() => this.refreshHud()))],
['Council', () => this.openModal((done) =>
openCouncilScreen(this, this.rules, this.state, done))],
];
const BTN_W = 200;
const BTN_H = 42;
const GAP = 8;
const PAD = 14;
const panelH = PAD * 2 + items.length * BTN_H + (items.length - 1) * GAP;
// Left-aligned under the (190-wide) Empire button, regardless of the
// dropdown panel's own width — reads empireBtn's actual width rather
// than a second hardcoded constant that could drift if either changes.
const panelCx = this.empireBtn.x - this.empireBtn.options.width / 2 + BTN_W / 2;
const panelTopY = 31 + 21 + 12; // just below the Empire button (centred at y=31, height 42)
const panelCy = panelTopY + panelH / 2;
layer.add(this.add.rectangle(panelCx, panelCy, BTN_W + PAD * 2, panelH, 0x0b1220, 0.97)
.setStrokeStyle(1.5, 0x6fc4ff, 0.55));
let by = panelCy - panelH / 2 + PAD + BTN_H / 2;
for (const [label, fn] of items) {
const btn = new Button(this, panelCx, by, label, this.uiClick(() => { this.closeEmpireMenu(); fn(); }),
{ width: BTN_W, height: BTN_H, fontSize: 16, variant: 'ghost' });
layer.add(btn);
by += BTN_H + GAP;
}
}
closeEmpireMenu() {
this.empireBtn.setLabel(EMPIRE_CLOSED_LABEL);
this.empireMenuLayer?.destroy();
this.empireMenuLayer = null;
}
// Both destinations auto-save to the single "Resume Game" slot on the way
// out, same safety net the old ← Menu button always had — independent of,
// and in addition to, the 10 manual slots below.

View File

@ -16,14 +16,24 @@ import * as Phaser from 'phaser';
const CHAMFER = 14;
const SCHEMES = {
cyan: { glow: 0x2fe3ff, glowHex: '#2fe3ff', textDark: '#04121e' },
magenta: { glow: 0xff2fd0, glowHex: '#ff2fd0', textDark: '#1a0414' },
red: { glow: 0xff3355, glowHex: '#ff3355', textDark: '#1a0406' },
green: { glow: 0x2bff9e, glowHex: '#2bff9e', textDark: '#04160e' },
cyan: { glow: 0x2fe3ff, textDark: '#04121e' },
magenta: { glow: 0xff2fd0, textDark: '#1a0414' },
red: { glow: 0xff3355, textDark: '#1a0406' },
green: { glow: 0x2bff9e, textDark: '#04160e' },
};
const PANEL = 0x0b1220;
const TEXT = '#e8f4ff';
// Every button carries an always-on postFX glow on its border (see
// bgRect.postFX.addGlow below), meant to read as "dark button, glowing
// edge." Solid buttons (fill alpha 1) stay dark under that glow because
// the opaque fill visually dominates it. Ghost buttons don't have that
// luxury — at a low fill alpha there isn't enough opaque dark color to
// resist the glow, so the whole button washes out to a flat bright cyan
// instead of reading as translucent-with-a-glowing-edge. 0.85 gives ghost
// buttons enough opacity to hold their own against the glow while staying
// visibly lighter/less final than a solid button.
const GHOST_ALPHA = 0.85;
export class Button extends Phaser.GameObjects.Container {
constructor(scene, x, y, label, onClick, options = {}) {
@ -54,7 +64,7 @@ export class Button extends Phaser.GameObjects.Container {
this.scanline.setMask(new Phaser.Display.Masks.GeometryMask(scene, this.fxMaskShape));
this.brackets = scene.add.graphics().setAlpha(0);
this._drawBg(bg, isGhost ? 0.3 : 1);
this._drawBg(bg, isGhost ? GHOST_ALPHA : 1);
this.text = scene.add.text(0, 0, label, {
fontFamily: '"Julius Sans One"',
@ -88,7 +98,7 @@ export class Button extends Phaser.GameObjects.Container {
const { scheme: s, variant: v } = this.options;
if (v === 'ghost') {
this._drawBg(s.glow, 0.22);
this.text.setColor(s.glowHex);
this.text.setColor(s.textDark);
} else {
this._drawBg(s.glow, 1);
this.text.setColor(s.textDark);
@ -99,7 +109,7 @@ export class Button extends Phaser.GameObjects.Container {
const onOut = () => {
if (this._active) return;
const { bg: b, textColor: tc, variant: v } = this.options;
this._drawBg(b, v === 'ghost' ? 0.3 : 1);
this._drawBg(b, v === 'ghost' ? GHOST_ALPHA : 1);
this.text.setColor(tc);
this._stopScanline();
this._drawBrackets(false);
@ -211,7 +221,7 @@ export class Button extends Phaser.GameObjects.Container {
this._drawBg(scheme.glow, 1);
this.text.setColor(scheme.textDark);
} else {
this._drawBg(bg, variant === 'ghost' ? 0.3 : 1);
this._drawBg(bg, variant === 'ghost' ? GHOST_ALPHA : 1);
this.text.setColor(textColor);
}
return this;