feat(mastervega): add 10-slot save/load system, game menu, and sound effects
- Implement 10 manual save slots with metadata (turn, year, empire, species, date) - Add ☰ game menu button with Save, Load, Return to Main Menu, and Quit to Arcade - Replace generic sounds with 7 custom Vega sound effects (select, build, close, newturn, unit, view, warp) - Introduce uiClick() helper to standardize click sound across all modal screens - Update research video paths for umbrix and rrashaa species - Refactor button handlers in all Vega screens to use uiClick() pattern - Save/load uses scene restart via pendingSavedState for clean HUD/map teardown
This commit is contained in:
parent
223c1fd191
commit
8cb6988ded
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 MiB After Width: | Height: | Size: 4.3 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -214,10 +214,10 @@
|
|||
"human": { "key": "vega-research-human", "path": "assets/videos/vega/research-human.mp4" },
|
||||
"kestrelli": { "key": "vega-research-kestrelli", "path": "assets/videos/vega/research-kestrelli.mp4" },
|
||||
"ursaal": { "key": "vega-research-ursaal", "path": "assets/videos/vega/research-ursaal.mp4" },
|
||||
"umbrix": { "key": "vega-research-umbrix", "path": null },
|
||||
"umbrix": { "key": "vega-research-umbrix", "path": "assets/videos/vega/research-umbrix.mp4" },
|
||||
"kkrix": { "key": "vega-research-kkrix", "path": "assets/videos/vega/research-kkrix.mp4" },
|
||||
"mekhan": { "key": "vega-research-mekhan", "path": "assets/videos/vega/research-mekhan.mp4" },
|
||||
"rrashaa": { "key": "vega-research-rrashaa", "path": null },
|
||||
"rrashaa": { "key": "vega-research-rrashaa", "path": "assets/videos/vega/research-rrashaa.mp4" },
|
||||
"cerebrai": { "key": "vega-research-cerebrai", "path": "assets/videos/vega/research-cerebrai.mp4" },
|
||||
"ssakar": { "key": "vega-research-ssakar", "path": "assets/videos/vega/research-ssakar.mp4" },
|
||||
"lithox": { "key": "vega-research-lithox", "path": "assets/videos/vega/research-lithox.mp4" }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import * as Phaser from 'phaser';
|
|||
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { TextInput } from '../../ui/TextInput.js';
|
||||
import { Tooltip } from '../../ui/Tooltip.js';
|
||||
import { VegaMusic } from './VegaMusic.js';
|
||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
import { enqueue as enqueueSpeech, resetQueue as resetSpeechQueue } from '../../ui/SpeechQueue.js';
|
||||
|
|
@ -26,7 +27,7 @@ import { openSystemView } from './VegaSystemView.js';
|
|||
import { openCombatView } from './VegaCombatView.js';
|
||||
import {
|
||||
FONT, D, openDiplomacyScreen, openCouncilScreen, openLeaderScreen,
|
||||
showVictoryOverlay,
|
||||
openSaveScreen, openLoadScreen, showVictoryOverlay,
|
||||
} from './VegaScreens.js';
|
||||
import { openResearchScreen } from './VegaResearchScreen.js';
|
||||
import { openResearchChoiceScreen } from './VegaResearchChoiceScreen.js';
|
||||
|
|
@ -36,6 +37,13 @@ import { openAudienceScreen } from './VegaAudience.js';
|
|||
import { claimAudienceContacts, claimFleetComplaints, canNegotiate } from './VegaDiplomacy.js';
|
||||
|
||||
const SAVE_KEY = 'mastervega-save';
|
||||
// 10 manual slots, independent of the single SAVE_KEY auto-save above (which
|
||||
// exists purely to power the landing screen's "Resume Game" button). Each
|
||||
// slot is one localStorage entry: { meta: {...for display...}, raw: the
|
||||
// engine's own serialize() string }, so loading a slot is just a
|
||||
// deserialize() call away from the exact same state shape "Resume Game" uses.
|
||||
const SAVE_SLOT_COUNT = 10;
|
||||
const saveSlotKey = (i) => `mastervega-save-slot-${i}`;
|
||||
|
||||
export default class MasterOfVegaGame extends Phaser.Scene {
|
||||
constructor() { super('MasterOfVegaGame'); }
|
||||
|
|
@ -43,6 +51,12 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
init(data) {
|
||||
this.roomData = data ?? {};
|
||||
this.gameDef = data?.game ?? { slug: 'mastervega', name: 'Master of Vega' };
|
||||
// Set by the game menu's Load screen: a state deserialized from a save
|
||||
// slot, handed across a full scene restart (see openLoadScreen below)
|
||||
// rather than swapped into a still-running scene, so the map/HUD/fx from
|
||||
// the game being left behind get torn down the same proven way Quit to
|
||||
// Arcade already relies on instead of a bespoke in-place teardown.
|
||||
this.pendingSavedState = data?.savedState ?? null;
|
||||
this.modalOpen = false;
|
||||
this.busy = false;
|
||||
// Empire indices with a freshly-claimed contact waiting for their
|
||||
|
|
@ -68,7 +82,8 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
|
||||
this.events.once('shutdown', () => this.teardown());
|
||||
|
||||
this.showLanding();
|
||||
if (this.pendingSavedState) this.beginGame(null, this.pendingSavedState);
|
||||
else this.showLanding();
|
||||
}
|
||||
|
||||
// Wrap a menu handler so it clicks. Every button and card on the front-end
|
||||
|
|
@ -76,7 +91,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
// is actually clickable.
|
||||
uiClick(fn) {
|
||||
return (...args) => {
|
||||
playSound(this, SFX.EIGHTBIT_ACTIVATE);
|
||||
playSound(this, SFX.VEGA_SELECT);
|
||||
fn?.(...args);
|
||||
};
|
||||
}
|
||||
|
|
@ -476,6 +491,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
});
|
||||
|
||||
// --- option rows
|
||||
const optTooltip = new Tooltip(this, { depth: D.detail });
|
||||
const optY = 600;
|
||||
const mkRow = (label, y, options, key, format = (o) => o.name) => {
|
||||
layer.add(this.add.text(GAME_WIDTH / 2 - 620, y, label, {
|
||||
|
|
@ -489,6 +505,9 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
b.setActive(true);
|
||||
}), { width: 195, height: 46, fontSize: 19 });
|
||||
if ((opt.id ?? opt) === choice[key]) b.setActive(true);
|
||||
if (opt.desc) {
|
||||
optTooltip.attachTo(b, () => ({ title: opt.name, lines: [{ text: opt.desc }] }));
|
||||
}
|
||||
buttons.push(b);
|
||||
layer.add(b);
|
||||
});
|
||||
|
|
@ -531,6 +550,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
if (!choice.speciesId || !choice.homeColonyName.trim()) { refreshStart(); return; }
|
||||
choice.homeColonyName = choice.homeColonyName.trim();
|
||||
nameInput.destroy();
|
||||
optTooltip.destroy();
|
||||
layer.destroy();
|
||||
this.beginGame(choice);
|
||||
}), { width: 260, height: 64, fontSize: 30 });
|
||||
|
|
@ -540,6 +560,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
// Resuming lives on the landing screen now, so this only steps back to it.
|
||||
const back = new Button(this, 120, 60, '← Back', this.uiClick(() => {
|
||||
nameInput.destroy();
|
||||
optTooltip.destroy();
|
||||
layer.destroy();
|
||||
this.showLanding();
|
||||
}), { width: 170, height: 48, fontSize: 20, variant: 'ghost' });
|
||||
|
|
@ -626,7 +647,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
hud.add(this.hudText);
|
||||
|
||||
const mk = (label, x, fn) => {
|
||||
const b = new Button(this, x, 31, label, fn, { width: 150, height: 42, fontSize: 18 });
|
||||
const b = new Button(this, x, 31, label, this.uiClick(fn), { width: 150, height: 42, fontSize: 18 });
|
||||
hud.add(b);
|
||||
return b;
|
||||
};
|
||||
|
|
@ -641,7 +662,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
openLeaderScreen(this, this.rules, this.state, this.state.humanIndex, this.art, done,
|
||||
() => this.refreshHud())));
|
||||
|
||||
this.endTurnBtn = new Button(this, GAME_WIDTH - 130, 31, 'End turn', () => this.onEndTurn(),
|
||||
this.endTurnBtn = new Button(this, GAME_WIDTH - 130, 31, 'End turn', this.uiClick(() => this.onEndTurn()),
|
||||
{ width: 190, height: 46, fontSize: 20 });
|
||||
hud.add(this.endTurnBtn);
|
||||
|
||||
|
|
@ -661,11 +682,103 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
this.offerCardLayer = this.add.container(0, 0);
|
||||
hud.add(this.offerCardLayer);
|
||||
|
||||
const back = new Button(this, 96, GAME_HEIGHT - 40, '← Menu', () => {
|
||||
this.writeSave();
|
||||
this.scene.start('GameMenu');
|
||||
}, { width: 150, height: 40, fontSize: 17, variant: 'ghost' });
|
||||
hud.add(back);
|
||||
const menuBtn = new Button(this, 56, GAME_HEIGHT - 40, '☰', this.uiClick(() => this.toggleGameMenu()),
|
||||
{ width: 56, height: 44, fontSize: 24, variant: 'ghost' });
|
||||
hud.add(menuBtn);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- game menu
|
||||
|
||||
/** ☰ toggles a small popover directly above it: Return to Main Menu, Save,
|
||||
* Load (lit only once a slot exists), Quit to Arcade. Gated by modalOpen
|
||||
* like every other HUD button so it can't stack on top of Research/
|
||||
* Diplomacy/Council/Leaders. */
|
||||
toggleGameMenu() {
|
||||
if (this.modalOpen) return;
|
||||
if (this.gameMenuLayer) { this.closeGameMenu(); return; }
|
||||
this.openGameMenu();
|
||||
}
|
||||
|
||||
openGameMenu() {
|
||||
const layer = this.add.container(0, 0).setDepth(D.modal);
|
||||
this.gameMenuLayer = layer;
|
||||
|
||||
// Click-anywhere-else-to-dismiss, same mechanism VegaScreens.js's list
|
||||
// pickers use for their own veil.
|
||||
const catcher = this.add.rectangle(0, 0, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.001)
|
||||
.setOrigin(0, 0).setInteractive();
|
||||
catcher.on('pointerup', () => this.closeGameMenu());
|
||||
layer.add(catcher);
|
||||
|
||||
const items = [
|
||||
['Return to Main Menu', () => this.returnToMainMenu()],
|
||||
['Save', () => this.openSaveMenu()],
|
||||
['Load', () => this.openLoadMenu(), !this.hasAnySaveSlot()],
|
||||
['Quit to Arcade', () => this.quitToArcade()],
|
||||
];
|
||||
|
||||
const BTN_W = 260;
|
||||
const BTN_H = 46;
|
||||
const GAP = 10;
|
||||
const PAD = 16;
|
||||
const panelH = PAD * 2 + items.length * BTN_H + (items.length - 1) * GAP;
|
||||
const panelCx = 56 + BTN_W / 2 - 12;
|
||||
const panelBottomY = (GAME_HEIGHT - 40) - 22 - 12; // just above the ☰ button
|
||||
const panelCy = panelBottomY - 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, disabled] of items) {
|
||||
const btn = new Button(this, panelCx, by, label, this.uiClick(() => { this.closeGameMenu(); fn(); }),
|
||||
{ width: BTN_W, height: BTN_H, fontSize: 16, variant: 'ghost' });
|
||||
if (disabled) btn.setEnabled(false);
|
||||
layer.add(btn);
|
||||
by += BTN_H + GAP;
|
||||
}
|
||||
}
|
||||
|
||||
closeGameMenu() {
|
||||
this.gameMenuLayer?.destroy();
|
||||
this.gameMenuLayer = 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.
|
||||
returnToMainMenu() {
|
||||
this.writeSave();
|
||||
this.scene.start('MasterOfVegaGame', { game: this.gameDef });
|
||||
}
|
||||
|
||||
quitToArcade() {
|
||||
this.writeSave();
|
||||
this.scene.start('GameMenu');
|
||||
}
|
||||
|
||||
openSaveMenu() {
|
||||
this.openModal((done) => openSaveScreen(this, this.rules, this.state, {
|
||||
getSlots: () => this.allSaveSlotMeta(),
|
||||
onSave: (i) => this.writeSaveSlot(i),
|
||||
}, done));
|
||||
}
|
||||
|
||||
openLoadMenu() {
|
||||
this.openModal((done) => openLoadScreen(this, this.rules, this.state, {
|
||||
getSlots: () => this.allSaveSlotMeta(),
|
||||
// A successful load restarts the whole scene (see init()'s
|
||||
// pendingSavedState) rather than swapping this.state under the
|
||||
// running map/HUD — the same proven teardown path Quit to Arcade
|
||||
// already relies on, so `done` (the modal-close callback) is
|
||||
// intentionally left uncalled: the scene it would resume is gone.
|
||||
onLoad: (i) => {
|
||||
const loaded = this.loadSaveSlot(i);
|
||||
if (!loaded) return;
|
||||
this.scene.start('MasterOfVegaGame', { game: this.gameDef, savedState: loaded });
|
||||
},
|
||||
onDelete: (i) => this.deleteSaveSlot(i),
|
||||
}, done));
|
||||
}
|
||||
|
||||
refreshHud() {
|
||||
|
|
@ -721,12 +834,12 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
}).setOrigin(0, 0.5));
|
||||
|
||||
const canSeek = canNegotiate(this.rules, this.state, this.state.humanIndex, otherIdx);
|
||||
const btn = new Button(this, W / 2 - 78, 0, 'Seek Audience', () => {
|
||||
const btn = new Button(this, W / 2 - 78, 0, 'Seek Audience', this.uiClick(() => {
|
||||
this.openModal((done) => openAudienceScreen(
|
||||
this, this.rules, this.state, this.state.humanIndex, otherIdx, this.art, done,
|
||||
() => this.refreshAll(),
|
||||
));
|
||||
}, { width: 140, height: 40, fontSize: 14 });
|
||||
}), { width: 140, height: 40, fontSize: 14 });
|
||||
if (!canSeek) btn.setEnabled(false);
|
||||
card.add(btn);
|
||||
|
||||
|
|
@ -799,6 +912,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
// instead of ringing the destination star.
|
||||
if (this.selectedFleet && this.state.fleets.includes(this.selectedFleet)
|
||||
&& this.selectedFleet.starIdx >= 0 && this.selectedFleet.starIdx !== idx) {
|
||||
playSound(this, SFX.VEGA_VIEW);
|
||||
this.map.setRoutePreview(this.selectedFleet.starIdx, idx);
|
||||
this.panel.showOrder(idx);
|
||||
return;
|
||||
|
|
@ -818,6 +932,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
if (fleet.starIdx >= 0) this.onStarClick(fleet.starIdx);
|
||||
return;
|
||||
}
|
||||
playSound(this, SFX.VEGA_UNIT);
|
||||
this.selectedFleet = fleet;
|
||||
this.map.setSelectedFleet(fleet);
|
||||
this.panel.showFleet(fleet);
|
||||
|
|
@ -865,7 +980,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
}
|
||||
const eta = Logic.fleetEta(this.rules, this.state, sent);
|
||||
this.log(`Fleet away — ${dest.name} in ${eta} turn${eta === 1 ? '' : 's'}.`);
|
||||
playSound(this, 'ta-rocket-1');
|
||||
playSound(this, SFX.VEGA_WARP);
|
||||
this.selectedFleet = null;
|
||||
this.panel.hide();
|
||||
this.map.setSelectedStar(toStar);
|
||||
|
|
@ -1089,4 +1204,50 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
clearSave() {
|
||||
try { window.localStorage.removeItem(SAVE_KEY); } catch (err) { /* ignore */ }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- manual save slots
|
||||
|
||||
writeSaveSlot(i) {
|
||||
try {
|
||||
const emp = this.state.empires[this.state.humanIndex];
|
||||
const meta = {
|
||||
savedAt: Date.now(),
|
||||
turn: this.state.turn,
|
||||
year: turnToYear(this.state.turn),
|
||||
empireName: emp?.name ?? 'Unknown',
|
||||
speciesName: this.rules.species[emp?.speciesId]?.name ?? '?',
|
||||
};
|
||||
window.localStorage.setItem(saveSlotKey(i), JSON.stringify({ meta, raw: Logic.serialize(this.state) }));
|
||||
return true;
|
||||
} catch (err) { return false; }
|
||||
}
|
||||
|
||||
readSaveSlotMeta(i) {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(saveSlotKey(i));
|
||||
return raw ? (JSON.parse(raw).meta ?? null) : null;
|
||||
} catch (err) { return null; }
|
||||
}
|
||||
|
||||
loadSaveSlot(i) {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(saveSlotKey(i));
|
||||
if (!raw) return null;
|
||||
return Logic.deserialize(JSON.parse(raw).raw);
|
||||
} catch (err) { return null; }
|
||||
}
|
||||
|
||||
deleteSaveSlot(i) {
|
||||
try { window.localStorage.removeItem(saveSlotKey(i)); } catch (err) { /* ignore */ }
|
||||
}
|
||||
|
||||
allSaveSlotMeta() {
|
||||
const out = [];
|
||||
for (let i = 0; i < SAVE_SLOT_COUNT; i += 1) out.push(this.readSaveSlotMeta(i));
|
||||
return out;
|
||||
}
|
||||
|
||||
hasAnySaveSlot() {
|
||||
return this.allSaveSlotMeta().some(Boolean);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ import * as Phaser from 'phaser';
|
|||
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { enqueue as enqueueSpeech } from '../../ui/SpeechQueue.js';
|
||||
import { FONT, D } from './VegaScreens.js';
|
||||
import { FONT, D, uiClick } from './VegaScreens.js';
|
||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
import {
|
||||
sourceWidth, makeSpeciesPortrait, speciesSpeechClip, audienceVideoKey, hasAudienceVideo,
|
||||
} from './VegaArt.js';
|
||||
|
|
@ -343,7 +344,10 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
|||
fontFamily: FONT, fontSize: '18px', color: '#9fb6cc',
|
||||
});
|
||||
root.add(statusTxt);
|
||||
const closeBtn = new Button(scene, GAME_WIDTH - 80, 60, '✕', () => close(), { width: 56, height: 48, variant: 'ghost' });
|
||||
const closeBtn = new Button(scene, GAME_WIDTH - 80, 60, '✕', () => {
|
||||
playSound(scene, SFX.VEGA_CLOSE);
|
||||
close();
|
||||
}, { width: 56, height: 48, variant: 'ghost' });
|
||||
root.add(closeBtn);
|
||||
|
||||
function statusLine() {
|
||||
|
|
@ -682,7 +686,7 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
|||
t.on('pointerdown', () => { box.destroy(true); onPick(id); });
|
||||
box.add(t);
|
||||
});
|
||||
const cancel = new Button(scene, bx, by + h / 2 - 34, 'Cancel', () => box.destroy(true),
|
||||
const cancel = new Button(scene, bx, by + h / 2 - 34, 'Cancel', uiClick(scene, () => box.destroy(true)),
|
||||
{ width: 160, height: 40, fontSize: 16, variant: 'ghost' });
|
||||
box.add(cancel);
|
||||
}
|
||||
|
|
@ -717,7 +721,7 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
|||
t.on('pointerdown', () => { box.destroy(true); doGift(tier.id); });
|
||||
box.add(t);
|
||||
});
|
||||
const cancel = new Button(scene, bx, by + h / 2 - 34, 'Cancel', () => box.destroy(true),
|
||||
const cancel = new Button(scene, bx, by + h / 2 - 34, 'Cancel', uiClick(scene, () => box.destroy(true)),
|
||||
{ width: 160, height: 40, fontSize: 16, variant: 'ghost' });
|
||||
box.add(cancel);
|
||||
}
|
||||
|
|
@ -730,7 +734,7 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
|||
const row = Math.floor(i / perRow);
|
||||
const x = CHAT_X + col * (BTN_W + BTN_GAP) + BTN_W / 2;
|
||||
const yy = y + row * (BTN_H + BTN_GAP) + BTN_H / 2;
|
||||
buttonRow.add(new Button(scene, x, yy, label, fn, { width: BTN_W, height: BTN_H, fontSize: 16, ...extraOpts }));
|
||||
buttonRow.add(new Button(scene, x, yy, label, uiClick(scene, fn), { width: BTN_W, height: BTN_H, fontSize: 16, ...extraOpts }));
|
||||
});
|
||||
return Math.ceil(list.length / perRow) || 0;
|
||||
}
|
||||
|
|
@ -786,7 +790,7 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
|||
[['off', 'Off'], ['steal', 'Steal Tech'], ['sabotage', 'Sabotage']].forEach(([id, label], i) => {
|
||||
const active = current === id;
|
||||
const x = CHAT_X + i * (BTN_W + BTN_GAP) + BTN_W / 2;
|
||||
buttonRow.add(new Button(scene, x, rowY, label, () => doSetMission(id), {
|
||||
buttonRow.add(new Button(scene, x, rowY, label, uiClick(scene, () => doSetMission(id)), {
|
||||
width: BTN_W, height: BTN_H, fontSize: 16,
|
||||
variant: active ? 'solid' : 'ghost',
|
||||
bg: active ? ACCENT : undefined,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import * as Phaser from 'phaser';
|
|||
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { TextInput } from '../../ui/TextInput.js';
|
||||
import { FONT, D, ORBIT } from './VegaScreens.js';
|
||||
import { FONT, D, ORBIT, uiClick } from './VegaScreens.js';
|
||||
import { turnToYear } from './VegaRules.js';
|
||||
import {
|
||||
colonyVideoKey, hasColonyVideo, worldBackground, planetFrame, sourceWidth,
|
||||
|
|
@ -349,10 +349,10 @@ export function promptColonyName(scene, { worldName, defaultName }, onConfirm, o
|
|||
onCancel?.();
|
||||
};
|
||||
|
||||
const found = new Button(scene, cx - 110, y + NAME_PANEL_H - 46, 'Found', confirm,
|
||||
const found = new Button(scene, cx - 110, y + NAME_PANEL_H - 46, 'Found', uiClick(scene, confirm),
|
||||
{ width: 200, height: 52 });
|
||||
root.add(found);
|
||||
root.add(new Button(scene, cx + 110, y + NAME_PANEL_H - 46, 'Cancel', cancel,
|
||||
root.add(new Button(scene, cx + 110, y + NAME_PANEL_H - 46, 'Cancel', uiClick(scene, cancel),
|
||||
{ width: 200, height: 52, variant: 'ghost' }));
|
||||
|
||||
refresh();
|
||||
|
|
@ -708,7 +708,7 @@ export function openColonyIntro(scene, rules, state, colony, art, opts = {}) {
|
|||
resumeMusic();
|
||||
};
|
||||
|
||||
lower.add(new Button(scene, 1620, PANEL_Y + PANEL_H / 2, 'Continue', close,
|
||||
lower.add(new Button(scene, 1620, PANEL_Y + PANEL_H / 2, 'Continue', uiClick(scene, close),
|
||||
{ width: 320, height: 70, fontSize: 28 }));
|
||||
|
||||
scene.tweens.add({ targets: root, alpha: 1, duration: 340, ease: 'Cubic.easeOut' });
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ import * as Phaser from 'phaser';
|
|||
|
||||
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { FONT, D, ORBIT, slider } from './VegaScreens.js';
|
||||
import { FONT, D, ORBIT, slider, uiClick } from './VegaScreens.js';
|
||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
import { worldBackground, buildingFrame } from './VegaArt.js';
|
||||
import { createShipMediaPool, makeShipIcon } from './VegaShipMedia.js';
|
||||
import { openShipDetail } from './VegaShipDetail.js';
|
||||
|
|
@ -290,8 +291,10 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
add(scene.add.text(x, y, 'COLONY', {
|
||||
fontFamily: FONT, fontSize: '26px', color: '#cfe8ff',
|
||||
}));
|
||||
add(new Button(scene, PANEL_X + PANEL_W - 38, y + 16, '✕', close,
|
||||
{ width: 40, height: 36, fontSize: 18, variant: 'ghost' }));
|
||||
add(new Button(scene, PANEL_X + PANEL_W - 38, y + 16, '✕', () => {
|
||||
playSound(scene, SFX.VEGA_CLOSE);
|
||||
close();
|
||||
}, { width: 40, height: 36, fontSize: 18, variant: 'ghost' }));
|
||||
y += 48;
|
||||
|
||||
// --- summary
|
||||
|
|
@ -422,13 +425,13 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
|
||||
y = buttonY;
|
||||
const sendBtn = add(new Button(scene, PANEL_X + PANEL_W / 2, y + 22,
|
||||
'Send Population', openTransportPicker, { width: w, height: 44, fontSize: 18 }));
|
||||
'Send Population', uiClick(scene, openTransportPicker), { width: w, height: 44, fontSize: 18 }));
|
||||
if (maxSendablePopulation(colony) <= 0 || empireColonies(state, colony.empireIdx).length < 2) {
|
||||
sendBtn.setEnabled(false);
|
||||
}
|
||||
y += 56;
|
||||
add(new Button(scene, PANEL_X + PANEL_W / 2, y + 24,
|
||||
flyOpen ? 'Close Build Queue' : 'Build Queue', toggleFlyout,
|
||||
flyOpen ? 'Close Build Queue' : 'Build Queue', uiClick(scene, toggleFlyout),
|
||||
{ width: w, height: 48, fontSize: 20 }));
|
||||
}
|
||||
|
||||
|
|
@ -573,7 +576,7 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
bg.on('pointerout', () => bg.setFillStyle(0x0f1a2c, 0.0));
|
||||
// Scrolled out of sight is still hit-testable — a mask is a rendering
|
||||
// concern only, so the visible band has to be checked explicitly.
|
||||
bg.on('pointerup', (p) => { if (scroller.contains(p)) onAdd(); });
|
||||
bg.on('pointerup', (p) => { if (scroller.contains(p)) { playSound(scene, SFX.VEGA_BUILD); onAdd(); } });
|
||||
scroller.content.add(bg);
|
||||
// A ship gets its commanding officer beside the hull; a building keeps
|
||||
// the single icon it always had. Both sheets are optional and procedural
|
||||
|
|
@ -614,7 +617,7 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
.setStrokeStyle(1, ACCENT, 0.55).setInteractive({ useHandCursor: true });
|
||||
r.on('pointerover', () => r.setFillStyle(0x22405f));
|
||||
r.on('pointerout', () => r.setFillStyle(0x16253c));
|
||||
r.on('pointerup', (p) => { if (scroller.contains(p)) onAddFive(); });
|
||||
r.on('pointerup', (p) => { if (scroller.contains(p)) { playSound(scene, SFX.VEGA_BUILD); onAddFive(); } });
|
||||
scroller.content.add(r);
|
||||
scroller.content.add(scene.add.text(w - 40, cy + 25, '+5', {
|
||||
fontFamily: FONT, fontSize: '15px', color: '#cfe8ff',
|
||||
|
|
@ -727,8 +730,10 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
transportLayer.add(scene.add.text(x, y, 'SEND POPULATION', {
|
||||
fontFamily: FONT, fontSize: '24px', color: '#cfe8ff',
|
||||
}));
|
||||
transportLayer.add(new Button(scene, TX + TW - 38, y + 14, '✕', closeTransportPicker,
|
||||
{ width: 40, height: 36, fontSize: 18, variant: 'ghost' }));
|
||||
transportLayer.add(new Button(scene, TX + TW - 38, y + 14, '✕', () => {
|
||||
playSound(scene, SFX.VEGA_CLOSE);
|
||||
closeTransportPicker();
|
||||
}, { width: 40, height: 36, fontSize: 18, variant: 'ghost' }));
|
||||
y += 46;
|
||||
|
||||
transportLayer.add(scene.add.text(x, y,
|
||||
|
|
@ -793,7 +798,7 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
pusher(transportLayer, x + 235, y + 15, '+', () => setAmount(amount + 1), amount < cap);
|
||||
pusher(transportLayer, x + 271, y + 15, '+5', () => setAmount(amount + 5), amount < cap,
|
||||
null, { w: 34, fontSize: 13 });
|
||||
transportLayer.add(new Button(scene, x + 331, y + 15, 'MAX', () => setAmount(cap),
|
||||
transportLayer.add(new Button(scene, x + 331, y + 15, 'MAX', uiClick(scene, () => setAmount(cap)),
|
||||
{ width: 66, height: 32, fontSize: 14, variant: 'ghost' }));
|
||||
transportLayer.add(scene.add.text(x, y + 40, `${cap.toFixed(1)} available to send`, {
|
||||
fontFamily: FONT, fontSize: '13px', color: '#6f8aa3',
|
||||
|
|
@ -801,14 +806,14 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
y += 76;
|
||||
|
||||
const canSend = !!dest && amount > 0;
|
||||
const sendBtn = new Button(scene, TX + TW / 2, y + 22, 'Send', () => {
|
||||
const sendBtn = new Button(scene, TX + TW / 2, y + 22, 'Send', uiClick(scene, () => {
|
||||
if (!dest) return;
|
||||
if (sendPopulation(rules, state, colony.empireIdx, colony.id, dest.id, amount)) {
|
||||
closeTransportPicker();
|
||||
buildPanel();
|
||||
onChanged?.();
|
||||
}
|
||||
}, { width: w, height: 44, fontSize: 18 });
|
||||
}), { width: w, height: 44, fontSize: 18 });
|
||||
if (!canSend) sendBtn.setEnabled(false);
|
||||
transportLayer.add(sendBtn);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import * as Phaser from 'phaser';
|
||||
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { FONT, D } from './VegaScreens.js';
|
||||
import { FONT, D, uiClick } from './VegaScreens.js';
|
||||
import VegaFx from './VegaFx.js';
|
||||
import { stepRound, runBattle, battleResult } from './VegaCombat.js';
|
||||
import { shipFrame } from './VegaArt.js';
|
||||
|
|
@ -138,27 +138,27 @@ export function openCombatView(scene, rules, battle, art, opts = {}) {
|
|||
}
|
||||
|
||||
// --- controls
|
||||
const next = new Button(scene, GAME_WIDTH / 2 - 230, GAME_HEIGHT - 70, 'Next round', () => {
|
||||
const next = new Button(scene, GAME_WIDTH / 2 - 230, GAME_HEIGHT - 70, 'Next round', uiClick(scene, () => {
|
||||
if (battle.done) return;
|
||||
animate(stepRound(battle, {}));
|
||||
if (battle.done) scene.time.delayedCall(700, finish);
|
||||
}, { width: 220, height: 52 });
|
||||
}), { width: 220, height: 52 });
|
||||
layer.add(next);
|
||||
|
||||
const auto = new Button(scene, GAME_WIDTH / 2, GAME_HEIGHT - 70, 'Auto-resolve', () => {
|
||||
const auto = new Button(scene, GAME_WIDTH / 2, GAME_HEIGHT - 70, 'Auto-resolve', uiClick(scene, () => {
|
||||
runBattle(battle);
|
||||
syncMarkers();
|
||||
finish();
|
||||
}, { width: 220, height: 52 });
|
||||
}), { width: 220, height: 52 });
|
||||
layer.add(auto);
|
||||
|
||||
const retreat = new Button(scene, GAME_WIDTH / 2 + 230, GAME_HEIGHT - 70, 'Withdraw', () => {
|
||||
const retreat = new Button(scene, GAME_WIDTH / 2 + 230, GAME_HEIGHT - 70, 'Withdraw', uiClick(scene, () => {
|
||||
if (battle.done || !playerSide) return;
|
||||
const orders = {};
|
||||
for (const s of battle.stacks) if (s.side === playerSide) orders[s.uid] = 'retreat';
|
||||
animate(stepRound(battle, orders));
|
||||
if (battle.done) scene.time.delayedCall(700, finish);
|
||||
}, { width: 220, height: 52, variant: playerSide ? 'solid' : 'ghost' });
|
||||
}), { width: 220, height: 52, variant: playerSide ? 'solid' : 'ghost' });
|
||||
layer.add(retreat);
|
||||
|
||||
buildMarkers();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import * as Phaser from 'phaser';
|
||||
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
import {
|
||||
empireColonies, empireFleets, hireLeader, assignLeader, unassignLeader, leaderHireCost,
|
||||
} from './VegaLogic.js';
|
||||
|
|
@ -39,6 +40,16 @@ export const ORBIT = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII'];
|
|||
const ACCENT = 0x6fc4ff;
|
||||
const PANEL = 0x0b1220;
|
||||
|
||||
// Wrap a Button handler so every ordinary click across the game's screens
|
||||
// shares one cue. Anything that closes a window via the ✕ glyph or has its
|
||||
// own dedicated sound (build queueing, ship detail, new-turn) bypasses this.
|
||||
export function uiClick(scene, fn) {
|
||||
return (...args) => {
|
||||
playSound(scene, SFX.VEGA_SELECT);
|
||||
fn?.(...args);
|
||||
};
|
||||
}
|
||||
|
||||
/** The holographic frame every modal is built inside. */
|
||||
export function modalShell(scene, title, onClose, { width = 1220, height = 800, closable = true } = {}) {
|
||||
const layer = scene.add.container(0, 0).setDepth(D.modal);
|
||||
|
|
@ -78,6 +89,7 @@ export function modalShell(scene, title, onClose, { width = 1220, height = 800,
|
|||
// closable defaults true so every existing caller is unaffected.
|
||||
if (closable) {
|
||||
const close = new Button(scene, x + width - 70, y + 40, '✕', () => {
|
||||
playSound(scene, SFX.VEGA_CLOSE);
|
||||
layer.destroy();
|
||||
onClose?.();
|
||||
}, { width: 46, height: 40, variant: 'ghost' });
|
||||
|
|
@ -187,10 +199,10 @@ export function openDiplomacyScreen(scene, rules, state, e, art, onClose, onChan
|
|||
|
||||
const bx = shell.body.x + shell.body.w - 150;
|
||||
const canSeek = canNegotiate(rules, state, e, other.idx);
|
||||
const b = new Button(scene, bx, y + rowH / 2 - 20, 'Seek Audience', () => {
|
||||
const b = new Button(scene, bx, y + rowH / 2 - 20, 'Seek Audience', uiClick(scene, () => {
|
||||
shell.destroy();
|
||||
openAudienceScreen(scene, rules, state, e, other.idx, art, onClose, onChanged);
|
||||
}, { width: 200, height: 40 });
|
||||
}), { width: 200, height: 40 });
|
||||
if (!canSeek) b.setEnabled(false);
|
||||
shell.add(b);
|
||||
});
|
||||
|
|
@ -290,7 +302,7 @@ function openListPicker(scene, shell, title, items) {
|
|||
t.on('pointerdown', () => { box.destroy(true); fn(); });
|
||||
box.add(t);
|
||||
});
|
||||
const cancel = new Button(scene, bx, by + h / 2 - 34, 'Cancel', () => box.destroy(true),
|
||||
const cancel = new Button(scene, bx, by + h / 2 - 34, 'Cancel', uiClick(scene, () => box.destroy(true)),
|
||||
{ width: 160, height: 40, fontSize: 16, variant: 'ghost' });
|
||||
box.add(cancel);
|
||||
}
|
||||
|
|
@ -319,13 +331,13 @@ export function openLeaderScreen(scene, rules, state, e, art, onClose, onChanged
|
|||
fontFamily: FONT, fontSize: '13px', color: '#7fd8a0',
|
||||
}));
|
||||
const b = new Button(scene, shell.body.x + shell.body.w - 130, y + 30,
|
||||
`Hire ${cost} BC`, () => {
|
||||
`Hire ${cost} BC`, uiClick(scene, () => {
|
||||
if (hireLeader(rules, state, e, offer.id)) {
|
||||
shell.destroy();
|
||||
openLeaderScreen(scene, rules, state, e, art, onClose, onChanged);
|
||||
onChanged?.();
|
||||
}
|
||||
}, { width: 190, height: 38, variant: affordable ? 'solid' : 'ghost' });
|
||||
}), { width: 190, height: 38, variant: affordable ? 'solid' : 'ghost' });
|
||||
shell.add(b);
|
||||
y += 84;
|
||||
}
|
||||
|
|
@ -379,12 +391,12 @@ export function openLeaderScreen(scene, rules, state, e, art, onClose, onChanged
|
|||
openListPicker(scene, shell, def.kind === 'admin' ? 'POST TO WHICH COLONY?' : 'POST TO WHICH FLEET?', targets);
|
||||
};
|
||||
shell.add(new Button(scene, shell.body.x + shell.body.w - 110, y + 20,
|
||||
l.assignKind ? 'Reassign' : 'Assign', openPicker, { width: 118, height: 32, fontSize: 14 }));
|
||||
l.assignKind ? 'Reassign' : 'Assign', uiClick(scene, openPicker), { width: 118, height: 32, fontSize: 14 }));
|
||||
if (l.assignKind) {
|
||||
shell.add(new Button(scene, shell.body.x + shell.body.w - 250, y + 20, 'Unassign', () => {
|
||||
shell.add(new Button(scene, shell.body.x + shell.body.w - 250, y + 20, 'Unassign', uiClick(scene, () => {
|
||||
unassignLeader(rules, state, e, l.leaderId);
|
||||
refresh();
|
||||
}, { width: 118, height: 32, fontSize: 14, variant: 'ghost' }));
|
||||
}), { width: 118, height: 32, fontSize: 14, variant: 'ghost' }));
|
||||
}
|
||||
y += 44;
|
||||
}
|
||||
|
|
@ -393,6 +405,109 @@ export function openLeaderScreen(scene, rules, state, e, art, onClose, onChanged
|
|||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/** "Slot 3 — Ursaal Empire (Ursaal) — Year 2431, Turn 131 — 8/8/2026 3:45 PM". */
|
||||
function saveSlotLabel(i, meta) {
|
||||
if (!meta) return `Slot ${i + 1} — empty`;
|
||||
const d = new Date(meta.savedAt);
|
||||
const when = `${d.toLocaleDateString()} ${d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
||||
return `Slot ${i + 1} — ${meta.empireName} (${meta.speciesName}) — Year ${meta.year}, Turn ${meta.turn} — ${when}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the current game to one of 10 slots. `getSlots()` returns a
|
||||
* length-10 array of meta objects (or null for an empty slot), re-read on
|
||||
* every (re)render so a just-written slot shows up immediately. Overwriting
|
||||
* an occupied slot needs a confirm click first — `confirmIdx` carries that
|
||||
* transient state across the destroy+rebuild refresh, same pattern
|
||||
* openLeaderScreen uses for its own picker flows.
|
||||
*/
|
||||
export function openSaveScreen(scene, rules, state, { getSlots, onSave }, onClose, confirmIdx = -1) {
|
||||
const shell = modalShell(scene, 'Save Game', onClose, { width: 1000, height: 800 });
|
||||
shell.add(scene.add.text(shell.body.x, shell.body.y,
|
||||
'Choose a slot to save your current game to.', {
|
||||
fontFamily: FONT, fontSize: '15px', color: '#7f97b3',
|
||||
}));
|
||||
|
||||
const refresh = (nextConfirmIdx = -1) => {
|
||||
shell.destroy();
|
||||
openSaveScreen(scene, rules, state, { getSlots, onSave }, onClose, nextConfirmIdx);
|
||||
};
|
||||
|
||||
const ROW_H = 66;
|
||||
// Load's row (Load + Delete buttons) leaves less room than Save's (one
|
||||
// button) — sized to the tighter of the two so a long empire/species name
|
||||
// never runs under either screen's buttons.
|
||||
const LABEL_W = shell.body.w - 340;
|
||||
let y = shell.body.y + 34;
|
||||
getSlots().forEach((meta, i) => {
|
||||
shell.add(scene.add.text(shell.body.x, y + ROW_H / 2, saveSlotLabel(i, meta), {
|
||||
fontFamily: FONT, fontSize: '15px', color: meta ? '#c8dcf0' : '#5f7890',
|
||||
wordWrap: { width: LABEL_W }, lineSpacing: 2,
|
||||
}).setOrigin(0, 0.5));
|
||||
|
||||
if (confirmIdx === i) {
|
||||
shell.add(new Button(scene, shell.body.x + shell.body.w - 320, y + ROW_H / 2, 'Confirm Overwrite',
|
||||
uiClick(scene, () => { onSave(i); refresh(); }), { width: 200, height: 40, fontSize: 14 }));
|
||||
shell.add(new Button(scene, shell.body.x + shell.body.w - 100, y + ROW_H / 2, 'Cancel',
|
||||
uiClick(scene, () => refresh()), { width: 180, height: 40, fontSize: 14, variant: 'ghost' }));
|
||||
} else {
|
||||
shell.add(new Button(scene, shell.body.x + shell.body.w - 100, y + ROW_H / 2,
|
||||
meta ? 'Overwrite' : 'Save',
|
||||
uiClick(scene, () => { if (meta) refresh(i); else { onSave(i); refresh(); } }),
|
||||
{ width: 180, height: 40, fontSize: 15, variant: meta ? 'ghost' : 'solid' }));
|
||||
}
|
||||
y += ROW_H;
|
||||
});
|
||||
return shell;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load or delete one of the 10 slots. Deleting needs the same confirm-click
|
||||
* as an overwrite in openSaveScreen; loading doesn't (it's not destructive
|
||||
* to anything but the current in-progress session, and the caller is about
|
||||
* to tear that down regardless — see MasterOfVegaGame.openLoadMenu).
|
||||
*/
|
||||
export function openLoadScreen(scene, rules, state, { getSlots, onLoad, onDelete }, onClose, confirmDeleteIdx = -1) {
|
||||
const shell = modalShell(scene, 'Load Game', onClose, { width: 1000, height: 800 });
|
||||
shell.add(scene.add.text(shell.body.x, shell.body.y,
|
||||
'Choose a saved game to load or delete.', {
|
||||
fontFamily: FONT, fontSize: '15px', color: '#7f97b3',
|
||||
}));
|
||||
|
||||
const refresh = (nextConfirmDeleteIdx = -1) => {
|
||||
shell.destroy();
|
||||
openLoadScreen(scene, rules, state, { getSlots, onLoad, onDelete }, onClose, nextConfirmDeleteIdx);
|
||||
};
|
||||
|
||||
const ROW_H = 66;
|
||||
const LABEL_W = shell.body.w - 340;
|
||||
let y = shell.body.y + 34;
|
||||
getSlots().forEach((meta, i) => {
|
||||
shell.add(scene.add.text(shell.body.x, y + ROW_H / 2, saveSlotLabel(i, meta), {
|
||||
fontFamily: FONT, fontSize: '15px', color: meta ? '#c8dcf0' : '#5f7890',
|
||||
wordWrap: { width: LABEL_W }, lineSpacing: 2,
|
||||
}).setOrigin(0, 0.5));
|
||||
|
||||
if (!meta) { y += ROW_H; return; }
|
||||
|
||||
if (confirmDeleteIdx === i) {
|
||||
shell.add(new Button(scene, shell.body.x + shell.body.w - 320, y + ROW_H / 2, 'Confirm Delete',
|
||||
uiClick(scene, () => { onDelete(i); refresh(); }), { width: 200, height: 40, fontSize: 14 }));
|
||||
shell.add(new Button(scene, shell.body.x + shell.body.w - 100, y + ROW_H / 2, 'Cancel',
|
||||
uiClick(scene, () => refresh()), { width: 180, height: 40, fontSize: 14, variant: 'ghost' }));
|
||||
} else {
|
||||
shell.add(new Button(scene, shell.body.x + shell.body.w - 320, y + ROW_H / 2, 'Load',
|
||||
uiClick(scene, () => onLoad(i)), { width: 180, height: 40, fontSize: 15 }));
|
||||
shell.add(new Button(scene, shell.body.x + shell.body.w - 100, y + ROW_H / 2, 'Delete',
|
||||
uiClick(scene, () => refresh(i)), { width: 180, height: 40, fontSize: 15, variant: 'ghost' }));
|
||||
}
|
||||
y += ROW_H;
|
||||
});
|
||||
return shell;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
export function showVictoryOverlay(scene, rules, state, onClose) {
|
||||
const layer = scene.add.container(0, 0).setDepth(D.toast);
|
||||
layer.add(scene.add.rectangle(0, 0, GAME_WIDTH, GAME_HEIGHT, 0x00060e, 0.9)
|
||||
|
|
@ -424,10 +539,10 @@ export function showVictoryOverlay(scene, rules, state, onClose) {
|
|||
}).setOrigin(0.5));
|
||||
|
||||
// Buttons in this repo draw centred, so they are positioned by their centre.
|
||||
const b = new Button(scene, GAME_WIDTH / 2, 560, 'Return to menu', () => {
|
||||
const b = new Button(scene, GAME_WIDTH / 2, 560, 'Return to menu', uiClick(scene, () => {
|
||||
layer.destroy();
|
||||
onClose?.();
|
||||
}, { width: 220, height: 52 });
|
||||
}), { width: 220, height: 52 });
|
||||
layer.add(b);
|
||||
return layer;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ import { markNumeral } from './VegaRules.js';
|
|||
import { empireDesign } from './VegaLogic.js';
|
||||
import { refitCost } from './VegaShips.js';
|
||||
import { makeCommanderPortrait, makeShipIcon } from './VegaShipMedia.js';
|
||||
import { FONT, D } from './VegaScreens.js';
|
||||
import { FONT, D, uiClick } from './VegaScreens.js';
|
||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
|
||||
const ACCENT = 0x6fc4ff;
|
||||
const PANEL = 0x0b1220;
|
||||
|
|
@ -44,6 +45,7 @@ const ROLE_NAMES = {
|
|||
*/
|
||||
export function openShipDetail(scene, rules, state, art, opts, onClose) {
|
||||
const { empireIdx, hullId, mark = null } = opts;
|
||||
playSound(scene, SFX.VEGA_UNIT);
|
||||
const emp = state.empires[empireIdx];
|
||||
const species = rules.species[emp.speciesId];
|
||||
const design = empireDesign(rules, state, empireIdx, hullId);
|
||||
|
|
@ -223,7 +225,7 @@ export function openShipDetail(scene, rules, state, art, opts, onClose) {
|
|||
});
|
||||
};
|
||||
|
||||
pop.add(new Button(scene, halfW - 110, halfH - 52, 'Close', close,
|
||||
pop.add(new Button(scene, halfW - 110, halfH - 52, 'Close', uiClick(scene, close),
|
||||
{ width: 180, height: 52, fontSize: 22, variant: 'ghost' }));
|
||||
veil.on('pointerup', close);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ import {
|
|||
colonyMaxPop, colonyProduction, colonyFactoryCap, effectiveFactories,
|
||||
colonyDefenseCap, habitableForEmpire, reachableStars, atWar, queueItemEta,
|
||||
} from './VegaLogic.js';
|
||||
import { FONT, D, ORBIT } from './VegaScreens.js';
|
||||
import { FONT, D, ORBIT, uiClick } from './VegaScreens.js';
|
||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
import { etaText } from './VegaColonyView.js';
|
||||
import { createShipMediaPool, makeShipIcon } from './VegaShipMedia.js';
|
||||
import { openShipDetail } from './VegaShipDetail.js';
|
||||
|
|
@ -92,6 +93,7 @@ export default class VegaSidePanel {
|
|||
this.root.add(scene.add.rectangle(PAD, 72, COL, 1, ACCENT, 0.4).setOrigin(0, 0));
|
||||
|
||||
this.root.add(new Button(scene, W - 32, 30, '✕', () => {
|
||||
playSound(scene, SFX.VEGA_CLOSE);
|
||||
this.cb.onClose?.();
|
||||
}, { width: 38, height: 34, fontSize: 18, variant: 'ghost' }));
|
||||
|
||||
|
|
@ -273,7 +275,7 @@ export default class VegaSidePanel {
|
|||
/** Full-width action button parked at the bottom of the column. */
|
||||
action(label, fn, opts = {}) {
|
||||
const { enabled = true, width = COL, x = PAD + COL / 2, variant = 'solid' } = opts;
|
||||
const b = new Button(this.scene, x, this.y + 24, label, enabled ? fn : null,
|
||||
const b = new Button(this.scene, x, this.y + 24, label, enabled ? uiClick(this.scene, fn) : null,
|
||||
{ width, height: 48, fontSize: 20, variant });
|
||||
if (!enabled) b.setEnabled?.(false);
|
||||
this.body.add(b);
|
||||
|
|
@ -752,13 +754,16 @@ export default class VegaSidePanel {
|
|||
|
||||
const half = (COL - 12) / 2;
|
||||
const btnY = this.y;
|
||||
// Not uiClick — accepting an order plays its own vega-warp cue once the
|
||||
// fleet is actually sent (MasterOfVegaGame.confirmOrder), not a generic
|
||||
// click, and a fuel-range refusal should stay silent either way.
|
||||
const accept = new Button(this.scene, PAD + half / 2, btnY + 24, 'Accept',
|
||||
refusal ? null : () => this.cb.onAcceptOrder?.(this.fleet, this.orderStar, this.selectedShips()),
|
||||
{ width: half, height: 48, fontSize: 20 });
|
||||
if (refusal) accept.setEnabled?.(false);
|
||||
this.body.add(accept);
|
||||
this.body.add(new Button(this.scene, PAD + half * 1.5 + 12, btnY + 24, 'Cancel',
|
||||
() => this.cb.onCancelOrder?.(), { width: half, height: 48, fontSize: 20, variant: 'ghost' }));
|
||||
uiClick(this.scene, () => this.cb.onCancelOrder?.()), { width: half, height: 48, fontSize: 20, variant: 'ghost' }));
|
||||
this.y = btnY + 60;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
import * as Phaser from 'phaser';
|
||||
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { modalShell, FONT, ORBIT } from './VegaScreens.js';
|
||||
import { modalShell, FONT, ORBIT, uiClick } from './VegaScreens.js';
|
||||
import { planetFrame, starFrame } from './VegaArt.js';
|
||||
import { openColonyView, CHANNEL_COLOUR, itemName, etaText } from './VegaColonyView.js';
|
||||
import { openColonyIntro, ensureColonyVideo, promptColonyName } from './VegaColonyIntro.js';
|
||||
|
|
@ -167,7 +167,7 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
// repeat: every rebuild of this panel asks again, and ensureColonyVideo
|
||||
// returns immediately once the clip is cached or already in flight.
|
||||
ensureColonyVideo(scene, planet.typeId);
|
||||
console_.add(new Button(scene, panelX + 130, y + 20, 'Found colony', () => {
|
||||
console_.add(new Button(scene, panelX + 130, y + 20, 'Found colony', uiClick(scene, () => {
|
||||
const orbit = selected;
|
||||
// Naming happens before the colony exists — canColonize/colonize
|
||||
// read no state this pauses, so the orrery is frozen for the whole
|
||||
|
|
@ -188,7 +188,7 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
onClose: () => { tick.paused = false; rebuild(); },
|
||||
});
|
||||
}, () => { tick.paused = false; });
|
||||
}, { width: 260, height: 44 }));
|
||||
}), { width: 260, height: 44 }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -223,13 +223,13 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
wordWrap: { width: panelW },
|
||||
}));
|
||||
y += 44;
|
||||
console_.add(new Button(scene, panelX + 120, y + 20, 'Bombard', () => {
|
||||
console_.add(new Button(scene, panelX + 120, y + 20, 'Bombard', uiClick(scene, () => {
|
||||
bombard(rules, state, viewerIdx, starIdx); onChanged?.(); rebuild();
|
||||
}, { width: 220, height: 42, bg: 0x6b2230 }));
|
||||
}), { width: 220, height: 42, bg: 0x6b2230 }));
|
||||
if (forecast && forecast.troops > 0) {
|
||||
console_.add(new Button(scene, panelX + 370, y + 20, 'Invade', () => {
|
||||
console_.add(new Button(scene, panelX + 370, y + 20, 'Invade', uiClick(scene, () => {
|
||||
invade(rules, state, viewerIdx, starIdx); onChanged?.(); rebuild();
|
||||
}, { width: 220, height: 42 }));
|
||||
}), { width: 220, height: 42 }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
|
@ -303,7 +303,7 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
// second modal while one is up — and this one has to stay up, because it is
|
||||
// what is keeping the star map behind it inert.
|
||||
y = Math.max(y + 20, shell.y + shell.height - 96);
|
||||
console_.add(new Button(scene, panelX + panelW / 2, y + 24, 'View Colony', () => {
|
||||
console_.add(new Button(scene, panelX + panelW / 2, y + 24, 'View Colony', uiClick(scene, () => {
|
||||
shell.layer.setVisible(false);
|
||||
tick.paused = true;
|
||||
openColonyView(scene, rules, state, colony, art, {
|
||||
|
|
@ -314,7 +314,7 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
rebuild();
|
||||
},
|
||||
});
|
||||
}, { width: panelW, height: 50, fontSize: 21 }));
|
||||
}), { width: panelW, height: 50, fontSize: 21 }));
|
||||
}
|
||||
|
||||
function select(i) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
// would make the two files import each other. Same one-way-dependency shape
|
||||
// as VegaSystemView.js, which already imports from both.
|
||||
|
||||
import { modalShell, FONT } from './VegaScreens.js';
|
||||
import { modalShell, FONT, uiClick } from './VegaScreens.js';
|
||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
import { scrollColumn } from './VegaColonyView.js';
|
||||
import { openSystemView } from './VegaSystemView.js';
|
||||
import { openResearchScreen } from './VegaResearchScreen.js';
|
||||
|
|
@ -45,6 +46,7 @@ const BUILDING_MEDIA_W = 90;
|
|||
const VIEW_SYSTEM_TYPES = new Set(['discovered', 'buildingDone', 'shipDone']);
|
||||
|
||||
export function openTurnReportScreen(scene, rules, state, events, onClose) {
|
||||
playSound(scene, SFX.VEGA_NEWTURN);
|
||||
const shell = modalShell(scene, `New Turn: ${turnToYear(state.turn)}`, onClose, { width: 1180, height: 760 });
|
||||
|
||||
const ordered = events
|
||||
|
|
@ -142,11 +144,13 @@ export function openTurnReportScreen(scene, rules, state, events, onClose) {
|
|||
};
|
||||
|
||||
const allBtn = new Button(scene, 16 + 84, cy + 14, 'All Colonies', () => {
|
||||
playSound(scene, SFX.VEGA_BUILD);
|
||||
queueAt(empireColonies(state, ev.empire));
|
||||
}, { width: 168, height: 28, fontSize: 13 });
|
||||
col.content.add(allBtn);
|
||||
|
||||
const bcBtn = new Button(scene, 16 + 168 + 16 + 105, cy + 14, 'Colonies with positive BC', () => {
|
||||
playSound(scene, SFX.VEGA_BUILD);
|
||||
queueAt(empireColonies(state, ev.empire).filter((c) => colonyNetIncome(rules, state, c) > 0));
|
||||
}, { width: 210, height: 28, fontSize: 13 });
|
||||
col.content.add(bcBtn);
|
||||
|
|
@ -224,7 +228,7 @@ export function openTurnReportScreen(scene, rules, state, events, onClose) {
|
|||
// the bottom-right of the expanded synopsis, not the collapsed
|
||||
// header, so it only appears once the player has read the row.
|
||||
if (VIEW_SYSTEM_TYPES.has(ev.type)) {
|
||||
const viewBtn = new Button(scene, w - 108, cy + 14, 'View Star System', () => {
|
||||
const viewBtn = new Button(scene, w - 108, cy + 14, 'View Star System', uiClick(scene, () => {
|
||||
col.destroy();
|
||||
shell.destroy();
|
||||
openSystemView(scene, rules, state, ev.starIdx, scene.art, {
|
||||
|
|
@ -232,7 +236,7 @@ export function openTurnReportScreen(scene, rules, state, events, onClose) {
|
|||
onChanged: () => scene.refreshAll?.(),
|
||||
onClose,
|
||||
});
|
||||
}, { width: 168, height: 28, fontSize: 13 });
|
||||
}), { width: 168, height: 28, fontSize: 13 });
|
||||
col.content.add(viewBtn);
|
||||
cy += 36;
|
||||
}
|
||||
|
|
@ -244,13 +248,13 @@ export function openTurnReportScreen(scene, rules, state, events, onClose) {
|
|||
// through the same onClose so the turn-transition still completes
|
||||
// exactly once, whichever button the player used to leave the report.
|
||||
if (ev.type === 'techDone') {
|
||||
const viewBtn = new Button(scene, w - 108, cy + 14, 'View Research', () => {
|
||||
const viewBtn = new Button(scene, w - 108, cy + 14, 'View Research', uiClick(scene, () => {
|
||||
col.destroy();
|
||||
shell.destroy();
|
||||
openResearchScreen(scene, rules, state, ev.empire, scene.art, onClose, {
|
||||
initialField: rules.techs[ev.techId].field,
|
||||
});
|
||||
}, { width: 168, height: 28, fontSize: 13 });
|
||||
}), { width: 168, height: 28, fontSize: 13 });
|
||||
col.content.add(viewBtn);
|
||||
cy += 36;
|
||||
}
|
||||
|
|
@ -277,11 +281,11 @@ export function openTurnReportScreen(scene, rules, state, events, onClose) {
|
|||
};
|
||||
render();
|
||||
|
||||
const btn = new Button(scene, shell.x + shell.width / 2, shell.y + shell.height - 34, 'Continue', () => {
|
||||
const btn = new Button(scene, shell.x + shell.width / 2, shell.y + shell.height - 34, 'Continue', uiClick(scene, () => {
|
||||
col.destroy();
|
||||
shell.destroy();
|
||||
onClose?.();
|
||||
}, { width: 220, height: 44 });
|
||||
}), { width: 220, height: 44 });
|
||||
shell.add(btn);
|
||||
|
||||
return shell;
|
||||
|
|
|
|||
|
|
@ -182,6 +182,14 @@ export default class PreloadScene extends Phaser.Scene {
|
|||
this.load.audio('sfx-monopoly-expense', 'assets/fx/monopoly-expense.mp3');
|
||||
this.load.audio('sfx-monopoly-pay', 'assets/fx/monopoly-pay.mp3');
|
||||
this.load.audio('sfx-monopoly-paid', 'assets/fx/monopoly-paid.mp3');
|
||||
|
||||
this.load.audio('sfx-vega-select', 'assets/fx/vega-select.mp3');
|
||||
this.load.audio('sfx-vega-build', 'assets/fx/vega-build.mp3');
|
||||
this.load.audio('sfx-vega-close', 'assets/fx/vega-close.mp3');
|
||||
this.load.audio('sfx-vega-newturn', 'assets/fx/vega-newturn.mp3');
|
||||
this.load.audio('sfx-vega-unit', 'assets/fx/vega-unit.mp3');
|
||||
this.load.audio('sfx-vega-view', 'assets/fx/vega-view.mp3');
|
||||
this.load.audio('sfx-vega-warp', 'assets/fx/vega-warp.mp3');
|
||||
}
|
||||
|
||||
async create() {
|
||||
|
|
|
|||
|
|
@ -99,6 +99,13 @@ export const SFX = {
|
|||
KART_SHELL: 'sfx-kart-shell',
|
||||
KART_STAR: 'sfx-kart-star',
|
||||
KART_SPINNER: 'sfx-kart-spinner',
|
||||
VEGA_SELECT: 'sfx-vega-select',
|
||||
VEGA_BUILD: 'sfx-vega-build',
|
||||
VEGA_CLOSE: 'sfx-vega-close',
|
||||
VEGA_NEWTURN: 'sfx-vega-newturn',
|
||||
VEGA_UNIT: 'sfx-vega-unit',
|
||||
VEGA_VIEW: 'sfx-vega-view',
|
||||
VEGA_WARP: 'sfx-vega-warp',
|
||||
};
|
||||
|
||||
export function playSound(scene, key) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue