From a16e59067bf1cfb1df2abe14f16ccff940a8b835 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sun, 2 Aug 2026 08:10:01 -0600 Subject: [PATCH] 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. --- src/games/mastervega/MasterOfVegaGame.js | 16 +++---- src/games/mastervega/VegaLogic.js | 20 +++++---- src/games/mastervega/VegaStarMap.js | 26 ++++++++++-- src/games/mastervega/VegaTooltips.js | 53 ++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 src/games/mastervega/VegaTooltips.js diff --git a/src/games/mastervega/MasterOfVegaGame.js b/src/games/mastervega/MasterOfVegaGame.js index b070f40..d15bede 100644 --- a/src/games/mastervega/MasterOfVegaGame.js +++ b/src/games/mastervega/MasterOfVegaGame.js @@ -7,7 +7,6 @@ import * as Phaser from 'phaser'; import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js'; import { Button } from '../../ui/Button.js'; -import { Tooltip } from '../../ui/Tooltip.js'; import { MusicPlayer } from '../../ui/MusicPlayer.js'; import { getGameSoundtrack } from '../../services/soundtrack.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(', ')}`); } - this.tooltip = new Tooltip(this, { depth: 70 }); - try { const { tracks, volume } = getGameSoundtrack(this); 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, { onStarClick: (idx) => this.onStarClick(idx), - onStarHover: (idx) => this.onStarHover(idx), onFleetClick: (fleet) => this.onFleetClick(fleet), blockWheel: () => this.modalOpen, }); @@ -580,6 +576,11 @@ export default class MasterOfVegaGame extends Phaser.Scene { 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, { viewerIdx: this.state.humanIndex, 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) { if (this.modalOpen || this.busy) return; if (fleet.empireIdx !== this.state.humanIndex) return; diff --git a/src/games/mastervega/VegaLogic.js b/src/games/mastervega/VegaLogic.js index 9075c62..b3e0081 100644 --- a/src/games/mastervega/VegaLogic.js +++ b/src/games/mastervega/VegaLogic.js @@ -197,11 +197,21 @@ export function colonyGroundDefense(rules, state, colony) { // Can this empire settle this planet at all? 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]; if (!planet) return false; const type = rules.planetTypes[planet.typeId]; 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 spec = rules.species[emp.speciesId]; if (spec.traits.colonizeAnything) return true; @@ -383,12 +393,8 @@ export function createGame(rules, opts) { colony.factories = rules.economy.startingFactories; colony.capital = true; state.empires[e].explored[starIdx] = true; - // Neighbours are charted from the start — nobody begins truly blind. - for (let i = 0; i < state.galaxy.stars.length; i += 1) { - if (parsecs(state.galaxy, starIdx, i) <= (rules.economy.baseFuelRange ?? 4)) { - state.empires[e].explored[i] = true; - } - } + // Everything else starts unexplored — a scout has to physically arrive + // (see moveFleets' `explored[f.starIdx] = true`) before its details show. // Starting fleet: a scout and a colony ship, plus difficulty handicap ships. const bonus = e === humanIndex ? 0 : diff.aiStartBonus; addFleet(rules, state, e, starIdx, [ diff --git a/src/games/mastervega/VegaStarMap.js b/src/games/mastervega/VegaStarMap.js index 0beb4e1..0d9997e 100644 --- a/src/games/mastervega/VegaStarMap.js +++ b/src/games/mastervega/VegaStarMap.js @@ -13,11 +13,15 @@ import * as Phaser from 'phaser'; import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js'; +import { Tooltip } from '../../ui/Tooltip.js'; import { makeNebula } from './VegaNebula.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 { buildZoomLadder, DEFAULT_ZOOM_INDEX } from './VegaZoom.js'; +import { describeStarTooltip } from './VegaTooltips.js'; const FONT = '"Julius Sans One"'; @@ -65,6 +69,8 @@ export default class VegaStarMap { ensureSoftDisc(scene, 'vega-soft-disc', 256); + this.tooltip = new Tooltip(scene, { depth: 70 }); + // --- screen-space backdrop this.bgLayer = scene.add.container(0, 0).setDepth(0); 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('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); }); + 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); this.starLayer.add(container); @@ -274,7 +284,10 @@ export default class VegaStarMap { const viewer = this.viewerIdx >= 0 ? state.empires[this.viewerIdx] : null; for (const s of this.starSprites) { 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); const cols = coloniesAt(state, s.star.idx); @@ -394,7 +407,13 @@ export default class VegaStarMap { // Closest zoom adds the system's contents. 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) { const sub = this.scene.add.text( 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(); } destroy() { + this.tooltip.destroy(); this.nebula?.destroy(); this.bgLayer.destroy(); this.starfield.destroy(); diff --git a/src/games/mastervega/VegaTooltips.js b/src/games/mastervega/VegaTooltips.js new file mode 100644 index 0000000..8f56d56 --- /dev/null +++ b/src/games/mastervega/VegaTooltips.js @@ -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 }; +}