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:
parent
7f1cfcc982
commit
a16e59067b
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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, [
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue