From 4c3b17610ac3f7c491c4bd74605cbb00a09765a3 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Mon, 10 Aug 2026 21:30:15 -0600 Subject: [PATCH] refactor(mastervega): consolidate HUD buttons into Empire dropdown and fix ghost button styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/games/mastervega/MasterOfVegaGame.js | 104 +++++++++++++++++++---- src/games/mastervega/VegaButton.js | 26 ++++-- 2 files changed, 107 insertions(+), 23 deletions(-) diff --git a/src/games/mastervega/MasterOfVegaGame.js b/src/games/mastervega/MasterOfVegaGame.js index 37257d6..0d1fa17 100644 --- a/src/games/mastervega/MasterOfVegaGame.js +++ b/src/games/mastervega/MasterOfVegaGame.js @@ -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. diff --git a/src/games/mastervega/VegaButton.js b/src/games/mastervega/VegaButton.js index 8f288c8..b119553 100644 --- a/src/games/mastervega/VegaButton.js +++ b/src/games/mastervega/VegaButton.js @@ -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;