refactor(mastervega): remove Tooltip dependency, show unexplored stars, and use tech-aware habitability

- Remove Tooltip import and onStarHover callback from MasterOfVegaGame; system view now stays closed for unexplored stars until a scout arrives.
- Make VegaStarMap own its Tooltip via attachTo, replacing the removed onStarHover handler.
- Show unexplored stars at all zoom levels (dimmed) so they remain hoverable targets and display an UNEXPLORED tooltip.
- Split out habitableForEmpire from canColonize so the star map and tooltips use the same tech-aware habitability check (planet type + current planetology tech) instead of the static colonizable flag.
- Change starting exploration: only the home system is explored; all other stars remain unexplored until a scout physically arrives.
This commit is contained in:
Brian Fertig 2026-08-02 08:10:01 -06:00
parent 7f1cfcc982
commit a16e59067b
4 changed files with 94 additions and 21 deletions

View File

@ -7,7 +7,6 @@
import * as Phaser from 'phaser'; import * as Phaser from 'phaser';
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js'; import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
import { Button } from '../../ui/Button.js'; import { Button } from '../../ui/Button.js';
import { Tooltip } from '../../ui/Tooltip.js';
import { MusicPlayer } from '../../ui/MusicPlayer.js'; import { MusicPlayer } from '../../ui/MusicPlayer.js';
import { getGameSoundtrack } from '../../services/soundtrack.js'; import { getGameSoundtrack } from '../../services/soundtrack.js';
import { playSound, SFX } from '../../ui/Sounds.js'; import { playSound, SFX } from '../../ui/Sounds.js';
@ -53,8 +52,6 @@ export default class MasterOfVegaGame extends Phaser.Scene {
console.info(`[MasterOfVega] procedural art for: ${procedural.join(', ')}`); console.info(`[MasterOfVega] procedural art for: ${procedural.join(', ')}`);
} }
this.tooltip = new Tooltip(this, { depth: 70 });
try { try {
const { tracks, volume } = getGameSoundtrack(this); const { tracks, volume } = getGameSoundtrack(this);
if (tracks?.length) this.music = new MusicPlayer(this, tracks, volume); if (tracks?.length) this.music = new MusicPlayer(this, tracks, volume);
@ -467,7 +464,6 @@ export default class MasterOfVegaGame extends Phaser.Scene {
this.map = new VegaStarMap(this, this.rules, this.state, this.art, { this.map = new VegaStarMap(this, this.rules, this.state, this.art, {
onStarClick: (idx) => this.onStarClick(idx), onStarClick: (idx) => this.onStarClick(idx),
onStarHover: (idx) => this.onStarHover(idx),
onFleetClick: (fleet) => this.onFleetClick(fleet), onFleetClick: (fleet) => this.onFleetClick(fleet),
blockWheel: () => this.modalOpen, blockWheel: () => this.modalOpen,
}); });
@ -580,6 +576,11 @@ export default class MasterOfVegaGame extends Phaser.Scene {
return; return;
} }
// Unexplored systems only get the map's hover tooltip (VegaStarMap owns
// it) — the system view stays closed until a scout has actually arrived.
const emp = this.state.empires[this.state.humanIndex];
if (emp && !emp.explored[idx]) return;
this.openModal((done) => openSystemView(this, this.rules, this.state, idx, this.art, { this.openModal((done) => openSystemView(this, this.rules, this.state, idx, this.art, {
viewerIdx: this.state.humanIndex, viewerIdx: this.state.humanIndex,
onChanged: () => this.refreshAll(), onChanged: () => this.refreshAll(),
@ -587,13 +588,6 @@ export default class MasterOfVegaGame extends Phaser.Scene {
})); }));
} }
onStarHover(idx) {
if (idx < 0) return;
const star = this.state.galaxy.stars[idx];
const emp = this.state.empires[this.state.humanIndex];
if (!star || (emp && !emp.explored[idx])) return;
}
onFleetClick(fleet) { onFleetClick(fleet) {
if (this.modalOpen || this.busy) return; if (this.modalOpen || this.busy) return;
if (fleet.empireIdx !== this.state.humanIndex) return; if (fleet.empireIdx !== this.state.humanIndex) return;

View File

@ -197,11 +197,21 @@ export function colonyGroundDefense(rules, state, colony) {
// Can this empire settle this planet at all? // Can this empire settle this planet at all?
export function canColonize(rules, state, e, starIdx, orbit) { export function canColonize(rules, state, e, starIdx, orbit) {
if (colonyAt(state, starIdx) && coloniesAt(state, starIdx).some((c) => c.orbit === orbit)) return false;
return habitableForEmpire(rules, state, e, starIdx, orbit);
}
// Whether this planet is within an empire's current colonisation reach —
// type + hostility vs. current planetology tech — regardless of whether it is
// already settled. Split out of canColonize so the star map and tooltip's "N
// habitable" readout can match what the colony screen actually offers instead
// of just the planet type's static colonizable flag (which ignores tech and
// made "1 habitable" show even for worlds too hostile to settle yet).
export function habitableForEmpire(rules, state, e, starIdx, orbit) {
const planet = state.galaxy.stars[starIdx]?.planets?.[orbit]; const planet = state.galaxy.stars[starIdx]?.planets?.[orbit];
if (!planet) return false; if (!planet) return false;
const type = rules.planetTypes[planet.typeId]; const type = rules.planetTypes[planet.typeId];
if (!type.colonizable) return false; if (!type.colonizable) return false;
if (colonyAt(state, starIdx) && coloniesAt(state, starIdx).some((c) => c.orbit === orbit)) return false;
const emp = state.empires[e]; const emp = state.empires[e];
const spec = rules.species[emp.speciesId]; const spec = rules.species[emp.speciesId];
if (spec.traits.colonizeAnything) return true; if (spec.traits.colonizeAnything) return true;
@ -383,12 +393,8 @@ export function createGame(rules, opts) {
colony.factories = rules.economy.startingFactories; colony.factories = rules.economy.startingFactories;
colony.capital = true; colony.capital = true;
state.empires[e].explored[starIdx] = true; state.empires[e].explored[starIdx] = true;
// Neighbours are charted from the start — nobody begins truly blind. // Everything else starts unexplored — a scout has to physically arrive
for (let i = 0; i < state.galaxy.stars.length; i += 1) { // (see moveFleets' `explored[f.starIdx] = true`) before its details show.
if (parsecs(state.galaxy, starIdx, i) <= (rules.economy.baseFuelRange ?? 4)) {
state.empires[e].explored[i] = true;
}
}
// Starting fleet: a scout and a colony ship, plus difficulty handicap ships. // Starting fleet: a scout and a colony ship, plus difficulty handicap ships.
const bonus = e === humanIndex ? 0 : diff.aiStartBonus; const bonus = e === humanIndex ? 0 : diff.aiStartBonus;
addFleet(rules, state, e, starIdx, [ addFleet(rules, state, e, starIdx, [

View File

@ -13,11 +13,15 @@
import * as Phaser from 'phaser'; import * as Phaser from 'phaser';
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js'; import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
import { Tooltip } from '../../ui/Tooltip.js';
import { makeNebula } from './VegaNebula.js'; import { makeNebula } from './VegaNebula.js';
import { PARSEC_PX, parsecs, mulberry32 } from './VegaGalaxyGen.js'; import { PARSEC_PX, parsecs, mulberry32 } from './VegaGalaxyGen.js';
import { reachableStars, coloniesAt, empireColonies, fleetEta } from './VegaLogic.js'; import {
reachableStars, coloniesAt, empireColonies, fleetEta, habitableForEmpire,
} from './VegaLogic.js';
import { starFrame } from './VegaArt.js'; import { starFrame } from './VegaArt.js';
import { buildZoomLadder, DEFAULT_ZOOM_INDEX } from './VegaZoom.js'; import { buildZoomLadder, DEFAULT_ZOOM_INDEX } from './VegaZoom.js';
import { describeStarTooltip } from './VegaTooltips.js';
const FONT = '"Julius Sans One"'; const FONT = '"Julius Sans One"';
@ -65,6 +69,8 @@ export default class VegaStarMap {
ensureSoftDisc(scene, 'vega-soft-disc', 256); ensureSoftDisc(scene, 'vega-soft-disc', 256);
this.tooltip = new Tooltip(scene, { depth: 70 });
// --- screen-space backdrop // --- screen-space backdrop
this.bgLayer = scene.add.container(0, 0).setDepth(0); this.bgLayer = scene.add.container(0, 0).setDepth(0);
this.nebula = makeNebula(scene, this.bgLayer, { this.nebula = makeNebula(scene, this.bgLayer, {
@ -178,6 +184,10 @@ export default class VegaStarMap {
hit.on('pointerover', () => { this.hoverStar = star.idx; this.cb.onStarHover?.(star.idx); }); hit.on('pointerover', () => { this.hoverStar = star.idx; this.cb.onStarHover?.(star.idx); });
hit.on('pointerout', () => { if (this.hoverStar === star.idx) this.hoverStar = -1; this.cb.onStarHover?.(-1); }); hit.on('pointerout', () => { if (this.hoverStar === star.idx) this.hoverStar = -1; this.cb.onStarHover?.(-1); });
hit.on('pointerup', (p) => { if (!this.dragged) this.cb.onStarClick?.(star.idx, p); }); hit.on('pointerup', (p) => { if (!this.dragged) this.cb.onStarClick?.(star.idx, p); });
this.tooltip.attachTo(hit, () => {
const viewer = this.viewerIdx >= 0 ? this.state.empires[this.viewerIdx] : null;
return describeStarTooltip(this.rules, this.state, viewer, star.idx);
});
container.add(hit); container.add(hit);
this.starLayer.add(container); this.starLayer.add(container);
@ -274,7 +284,10 @@ export default class VegaStarMap {
const viewer = this.viewerIdx >= 0 ? state.empires[this.viewerIdx] : null; const viewer = this.viewerIdx >= 0 ? state.empires[this.viewerIdx] : null;
for (const s of this.starSprites) { for (const s of this.starSprites) {
const explored = !viewer || viewer.explored[s.star.idx]; const explored = !viewer || viewer.explored[s.star.idx];
s.container.setVisible(explored || this.isFarZoom); // Unexplored stars stay visible (dimmed) at every zoom level so there is
// always something to hover/click for the UNEXPLORED tooltip and a scout
// target — only their name, ring and system contents are hidden.
s.container.setVisible(true);
s.body.setAlpha(explored ? 1 : 0.25); s.body.setAlpha(explored ? 1 : 0.25);
const cols = coloniesAt(state, s.star.idx); const cols = coloniesAt(state, s.star.idx);
@ -394,7 +407,13 @@ export default class VegaStarMap {
// Closest zoom adds the system's contents. // Closest zoom adds the system's contents.
if (this.isNearZoom) { if (this.isNearZoom) {
const habitable = s.star.planets.filter((p) => this.rules.planetTypes[p.typeId].colonizable).length; // "Habitable" here means settleable at the viewer's CURRENT
// planetology, not just the planet type's static ceiling — a Radiated
// world is colonizable in principle but still shows as 0/1 until tech
// catches up, matching what the system view's colonize button says.
const habitable = viewer
? s.star.planets.filter((p, orbit) => habitableForEmpire(this.rules, state, viewer.idx, s.star.idx, orbit)).length
: s.star.planets.filter((p) => this.rules.planetTypes[p.typeId].colonizable).length;
if (s.star.planets.length) { if (s.star.planets.length) {
const sub = this.scene.add.text( const sub = this.scene.add.text(
s.star.x, s.star.y + s.cls.radius * 2.2 + 32, s.star.x, s.star.y + s.cls.radius * 2.2 + 32,
@ -533,6 +552,7 @@ export default class VegaStarMap {
setViewer(idx) { this.viewerIdx = idx; this.rangeDirty = true; this.refresh(); } setViewer(idx) { this.viewerIdx = idx; this.rangeDirty = true; this.refresh(); }
destroy() { destroy() {
this.tooltip.destroy();
this.nebula?.destroy(); this.nebula?.destroy();
this.bgLayer.destroy(); this.bgLayer.destroy();
this.starfield.destroy(); this.starfield.destroy();

View File

@ -0,0 +1,53 @@
// Master of Vega — hover-tooltip content, brought over from the civilization
// game's tooltip system (src/games/civilization/CivilizationTooltips.js) and
// adapted for the star map. Rendering itself is the shared src/ui/Tooltip.js
// component; these functions only build the {title, lines} content it wants.
import { COLORS } from '../../config.js';
import { coloniesAt, habitableForEmpire } from './VegaLogic.js';
// Hover tooltip for a star on the map. `viewerEmp` is the human empire (or
// null in observer mode, which sees everything). An unexplored star only
// ever reveals its class — no name, no colonies, no planets — since that is
// the one thing visible from a distance without a scout in the system.
export function describeStarTooltip(rules, state, viewerEmp, starIdx) {
const star = state.galaxy.stars[starIdx];
const cls = rules.starClasses[star.classId];
const explored = !viewerEmp || viewerEmp.explored[starIdx];
if (!explored) {
return {
title: 'UNEXPLORED',
titleColor: COLORS.dangerHex,
lines: [
{ text: `${cls.name} star`, color: COLORS.goldHex },
{ text: cls.desc },
{ text: 'Send a scout ship to chart this system.', color: COLORS.mutedHex },
],
};
}
const lines = [{ text: `${cls.name} star`, color: COLORS.goldHex }];
const cols = coloniesAt(state, starIdx);
for (const c of cols) {
const owner = state.empires[c.empireIdx];
lines.push({
text: `${owner.name} colony — pop ${c.pop.toFixed(1)}${c.capital ? ' (capital)' : ''}`,
color: owner.color,
});
}
if (star.planets.length) {
// Matches the system view's colonize button: habitable at the viewer's
// CURRENT planetology, not just the planet type's static ceiling.
const habitable = viewerEmp
? star.planets.filter((p, orbit) => habitableForEmpire(rules, state, viewerEmp.idx, starIdx, orbit)).length
: star.planets.filter((p) => rules.planetTypes[p.typeId].colonizable).length;
lines.push({ text: `${star.planets.length} world${star.planets.length === 1 ? '' : 's'} · ${habitable} habitable` });
} else {
lines.push({ text: 'No planets in this system.', color: COLORS.mutedHex });
}
return { title: star.name, lines };
}