feat(mastervega): refine star map colony info, tech effects display, and turn report layout

- Add colony info layer showing per-empire colony rosters (name, orbit, pop)
  stacked up-left of colonized stars with colored bars and connector lines
- Change "Range as light" to "Explored as light": reveal only actually
  explored stars (not fuel-reachable) with smaller 1.1pc radius
- Extract VegaTechEffects.js with human-readable descriptions for all
  tech effects, replacing raw key/value dumps in turn report
- Expand building unlock rows to include building descriptions
- Reorganize ship completion rows: hull icon and commander portrait now
  display below the full-width synopsis instead of beside it
- Bump zuma level 8 config to level 21 "Sun Cove" with new path and params
This commit is contained in:
Brian Fertig 2026-08-06 18:33:04 -06:00
parent a7c3ef20e4
commit 0adb689c19
5 changed files with 329 additions and 227 deletions

View File

@ -1,165 +1,106 @@
{ {
"level": 8, "level": 21,
"name": "Sun Court", "name": "Sun Cove",
"frog": [ "frog": [
960, 974,
580 548
], ],
"colors": 5, "colors": 4,
"background": "zuma-background-2", "seed": 12345,
"seed": 64352, "starScores": [
"starScores": [ 640,
1010, 960,
1520, 1280
2020 ],
], "paths": [
"paths": [ {
{ "shape": "custom",
"shape": "horseshoe", "points": [
"points": [ [
[ -34,
-80, 173
130 ],
], [
[ 609,
60, 138
130 ],
], [
[ 1474,
960, 143
130 ],
], [
[ 1755,
1191, 292
154 ],
], [
[ 1786,
1398, 592
210 ],
], [
[ 1390,
1565, 811
294 ],
], [
[ 982,
1680, 815
399 ],
], [
[ 501,
1734, 585
514 ]
], ],
[ "tunnels": [],
1727, "quota": 24,
632 "introBalls": 8,
], "pushSpeed": 30,
[ "powerUpRate": 0.06
1659, },
742 {
], "shape": "custom",
[ "points": [
1538, [
836 -143,
], 948
[ ],
1375, [
908 1072,
], 963
[ ],
1185, [
951 1634,
], 911
[ ],
983, [
964 1595,
], 397
[ ],
785, [
946 1343,
], 241
[ ],
607, [
900 945,
], 270
[ ],
462, [
831 455,
], 403
[ ],
361, [
743 289,
], 571
[ ]
311, ],
646 "tunnels": [
], [
[ 1793,
313, 2411
547 ]
], ],
[ "quota": 24,
366, "introBalls": 8,
454 "pushSpeed": 30,
], "powerUpRate": 0.06
[ }
464, ]
374
],
[
598,
313
],
[
755,
275
],
[
923,
263
],
[
1087,
276
],
[
1235,
312
],
[
1356,
368
],
[
1441,
438
],
[
1485,
517
],
[
1485,
598
],
[
1445,
673
],
[
1368,
739
]
],
"tunnels": [
[
1857,
2403
]
],
"quota": 30,
"introBalls": 9,
"pushSpeed": 96,
"powerUpRate": 0.055
}
]
} }

View File

@ -17,14 +17,27 @@ 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 { import {
reachableStars, coloniesAt, empireColonies, fleetEta, habitableForEmpire, coloniesAt, empireColonies, fleetEta, habitableForEmpire,
} from './VegaLogic.js'; } 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'; import { describeStarTooltip } from './VegaTooltips.js';
import { ORBIT } from './VegaScreens.js';
const FONT = '"Julius Sans One"'; const FONT = '"Julius Sans One"';
// Per-empire colony roster block drawn up-left of a colonized star. Sizing
// is tuned to clear both the star's own ownership ring and the in-transit
// fleet marker/comet trail, which already occupies the same upper-left
// quadrant at (star.x - 26, star.y - 22).
const COLONY_INFO_GAP_X = 112;
const COLONY_INFO_GAP_Y = 36;
const COLONY_INFO_BAR_H = 12;
const COLONY_INFO_BAR_TEXT_GAP = 8;
const COLONY_INFO_GROUP_GAP = 20;
const COLONY_INFO_MIN_BAR_W = 80;
const COLONY_INFO_TEXT_SIZE = 26;
// The range and territory fields are painted into low-resolution // The range and territory fields are painted into low-resolution
// RenderTextures and scaled up. A huge galaxy is 5400px wide — past the safe // RenderTextures and scaled up. A huge galaxy is 5400px wide — past the safe
// single-texture size — and the upscale blur is exactly the soft edge both // single-texture size — and the upscale blur is exactly the soft edge both
@ -101,6 +114,11 @@ export default class VegaStarMap {
.setOrigin(0, 0).setScale(FIELD_DIV); .setOrigin(0, 0).setScale(FIELD_DIV);
this.root.add(this.rangeRT); this.root.add(this.rangeRT);
// Sits behind starLayer (insertion order) so every star sprite paints
// over the connecting lines this layer draws to its own center.
this.colonyInfoLayer = scene.add.container(0, 0);
this.root.add(this.colonyInfoLayer);
this.starLayer = scene.add.container(0, 0); this.starLayer = scene.add.container(0, 0);
this.root.add(this.starLayer); this.root.add(this.starLayer);
// The selection reticle sits above the stars and below the fleet markers, // The selection reticle sits above the stars and below the fleet markers,
@ -218,9 +236,11 @@ export default class VegaStarMap {
// ------------------------------------------------------------- the fields // ------------------------------------------------------------- the fields
// "Range as light": the galaxy is covered in darkness, and everything inside // "Explored as light": the galaxy is covered in darkness, and each star the
// fuel range is erased back out of it. Every propulsion tech literally lights // player has actually explored (colonised, or had a fleet visit) gets a soft
// up more of the map, which is the clearest progression signal a 4X can give. // patch erased around it. Deliberately NOT fuel range — reachability is left
// for the player to work out by trial and error rather than telegraphed on
// the map.
// A scratch image reused for every stamp. RenderTexture.erase()/draw() honour // A scratch image reused for every stamp. RenderTexture.erase()/draw() honour
// a game object's scale and tint, but NOT a bare texture key's — a key is // a game object's scale and tint, but NOT a bare texture key's — a key is
// always stamped at its native 256px. Everything here needs a radius that // always stamped at its native 256px. Everything here needs a radius that
@ -244,14 +264,14 @@ export default class VegaStarMap {
const emp = this.state.empires[this.viewerIdx]; const emp = this.state.empires[this.viewerIdx];
if (!emp) return; if (!emp) return;
const reach = reachableStars(this.rules, this.state, this.viewerIdx); // One soft disc per explored star — sized to clear just that star's own
// One soft disc per reachable star, wide enough that neighbouring discs // neighbourhood, not to bridge gaps into a "sphere of influence" the way
// overlap — otherwise the lit region reads as a string of beads instead of // the old fuel-range version did (that would re-imply territory the
// one continuous sphere of influence. // player hasn't actually scouted).
const radius = PARSEC_PX * 2.6; const radius = PARSEC_PX * 1.1;
const img = this.stamp((radius * 2) / 256 / FIELD_DIV); const img = this.stamp((radius * 2) / 256 / FIELD_DIV);
for (const key of Object.keys(reach)) { for (const star of this.state.galaxy.stars) {
const star = this.state.galaxy.stars[Number(key)]; if (!emp.explored[star.idx]) continue;
rt.erase(img, star.x / FIELD_DIV, star.y / FIELD_DIV); rt.erase(img, star.x / FIELD_DIV, star.y / FIELD_DIV);
} }
} }
@ -283,6 +303,7 @@ export default class VegaStarMap {
this.redrawRange(); this.redrawRange();
this.redrawTerritory(); this.redrawTerritory();
this.refreshStars(); this.refreshStars();
this.refreshColonyInfo();
this.refreshFleets(); this.refreshFleets();
this.refreshLabels(); this.refreshLabels();
} }
@ -312,6 +333,64 @@ export default class VegaStarMap {
} }
} }
/**
* Per-empire colony roster stacked up-left of a colonized star: a thick
* owner-colored bar over a "name (pop. N)" list, one group per empire that
* holds a colony at the star, each with its own line running to the star's
* center. Only shown at the closest zoom tier same density tier as the
* "N worlds · M habitable" subtext to keep the mid/far zoom map legible.
*/
refreshColonyInfo() {
this.colonyInfoLayer.removeAll(true);
if (this.isFarZoom || !this.isNearZoom) return;
const { state } = this;
const viewer = this.viewerIdx >= 0 ? state.empires[this.viewerIdx] : null;
const textSize = Math.round(COLONY_INFO_TEXT_SIZE / Math.max(0.7, this.zoom));
for (const s of this.starSprites) {
if (viewer && !viewer.explored[s.star.idx]) continue;
const cols = coloniesAt(state, s.star.idx);
if (!cols.length) continue;
const groups = [];
for (const emp of state.empires) {
const own = cols.filter((c) => c.empireIdx === emp.idx).sort((a, b) => a.orbit - b.orbit);
if (!own.length) continue;
const text = this.scene.add.text(0, 0, own.map((c) => (
`${s.star.name} ${ORBIT[c.orbit] ?? c.orbit + 1} (pop. ${c.pop.toFixed(1)})`
)).join('\n'), {
fontFamily: FONT, fontSize: `${textSize}px`, color: '#f0f4fa', lineSpacing: 2,
}).setOrigin(1, 0);
groups.push({ emp, text });
}
if (!groups.length) continue;
const barWidth = Math.max(COLONY_INFO_MIN_BAR_W, ...groups.map((g) => g.text.width));
const blockRightX = s.star.x - COLONY_INFO_GAP_X;
const totalHeight = groups.reduce((h, g) => (
h + COLONY_INFO_BAR_H + COLONY_INFO_BAR_TEXT_GAP + g.text.height
), 0) + COLONY_INFO_GROUP_GAP * (groups.length - 1);
let y = s.star.y - COLONY_INFO_GAP_Y - totalHeight;
const lines = this.scene.add.graphics();
this.colonyInfoLayer.add(lines);
for (const g of groups) {
const colour = Phaser.Display.Color.HexStringToColor(g.emp.color).color;
const bar = this.scene.add.rectangle(
blockRightX - barWidth / 2, y + COLONY_INFO_BAR_H / 2, barWidth, COLONY_INFO_BAR_H, colour,
).setStrokeStyle(2, 0x000000, 0.85);
this.colonyInfoLayer.add(bar);
lines.lineStyle(3, colour, 0.7);
lines.lineBetween(blockRightX, y + COLONY_INFO_BAR_H / 2, s.star.x, s.star.y);
g.text.setPosition(blockRightX, y + COLONY_INFO_BAR_H + COLONY_INFO_BAR_TEXT_GAP);
this.colonyInfoLayer.add(g.text);
y += COLONY_INFO_BAR_H + COLONY_INFO_BAR_TEXT_GAP + g.text.height + COLONY_INFO_GROUP_GAP;
}
}
}
refreshFleets() { refreshFleets() {
this.fleetLayer.removeAll(true); this.fleetLayer.removeAll(true);
const { state, rules } = this; const { state, rules } = this;
@ -664,6 +743,7 @@ export default class VegaStarMap {
this.clampPan(); this.clampPan();
this.refreshLabels(); this.refreshLabels();
this.refreshStars(); this.refreshStars();
this.refreshColonyInfo();
this.cb.onZoom?.(next); this.cb.onZoom?.(next);
} }

View File

@ -0,0 +1,63 @@
// Master of Vega — human-readable descriptions for `tech.effects` values.
// Headless, no Phaser: kept separate from VegaTurnReport.js so any other
// screen (a future tech-tree view, say) can reuse the same wording without
// pulling in turn-report-specific code.
//
// Every formatter is hand-matched to how the effect is actually consumed
// elsewhere in the engine (VegaCombat.js/VegaLogic.js/VegaShips.js) — not a
// generic key/value dump. A few effects (cloaked, scanRange, singularity)
// are currently inert stats with no downstream mechanic wired up yet; their
// wording stays modest/flavor rather than promising a numeric benefit that
// doesn't exist in the sim.
const pct = (mult) => `${Math.round(Math.abs(mult - 1) * 100)}%`;
function describeWeapon(w) {
const perRound = w.shots > 1 ? `, ${w.shots} shots per round` : '';
const pierce = w.shieldPierce
? `, pierces ${w.shieldPierce} point${w.shieldPierce === 1 ? '' : 's'} of enemy shields` : '';
const cracker = w.planetCracker ? ' — powerful enough to crack a planet apart' : '';
return `New ${w.kind} weapon available: ${w.name}, ${w.min}-${w.max} damage per shot${perRound}${pierce}${cracker}.`;
}
function hostilityWorlds(rules, tier) {
const names = rules.planetTypes
.filter((t) => t.colonizable && t.hostility === tier)
.map((t) => t.name);
return names.length ? `${names.join('/')}-class` : `hostility ${tier}`;
}
const FORMATTERS = {
armor: (rules, v) => `Hull armor upgraded to ${v.name} plating — hull hit points ×${v.hpMult} (+${pct(v.hpMult)}).`,
cloaked: () => 'Ships are fitted with a stealth field, cloaking them from enemy sensors.',
colonizeHostility: (rules, v) => `Colonists can now settle ${hostilityWorlds(rules, v)} worlds.`,
counterEspionage: (rules, v) => `+${v} counter-espionage — makes it harder for enemy agents to steal your technology.`,
engine: (rules, v) => `Engines upgraded to ${v.name} Drive (speed ${v.speed}).`,
espionage: (rules, v) => `+${v} espionage — improves your odds of stealing enemy technology.`,
factoryCostMult: (rules, v) => `Factories cost ${pct(v)} less to build.`,
fuelRange: (rules, v) => `Fuel range extended to ${v} parsecs — fleets can be ordered further from friendly colonies and starbases.`,
groundAttack: (rules, v) => `+${v} ground attack — improves the odds of a successful invasion.`,
groundDefense: (rules, v) => `+${v} ground defense — colonies field more defenders when invaded.`,
initiative: (rules, v) => `+${v} initiative — fleets choose their engagement range before slower opponents each combat round.`,
maxPopBonus: (rules, v) => `+${v} maximum population per colony.`,
planetaryShield: (rules, v) => (
`+${v} planetary shield — cuts population lost to bombardment by about ${Math.round((1 - 1 / (1 + v * 0.15)) * 100)}%.`
),
redirectInFlight: () => 'Fleets already in transit can be redirected to a new destination.',
refitCostMult: (rules, v) => `Auto-refit costs reduced by ${pct(v)}.`,
repairPerRound: (rules, v) => `Ships self-repair ${Math.round(v * 100)}% of max hull every combat round.`,
researchMult: (rules, v) => `Research output increased by ${pct(v)} empire-wide.`,
scanRange: (rules, v) => `+${v} scanner range for detecting distant fleets and systems.`,
shield: (rules, v) => `Deflector shields absorb ${v} point${v === 1 ? '' : 's'} of damage from every incoming hit, on ships and planetary defenses alike.`,
singularity: () => "A breakthrough in exotic physics — the foundation for the galaxy's ultimate weapon.",
targeting: (rules, v) => `Combat accuracy increased by ${Math.round(v * 6)}% (better targeting computers).`,
wasteMult: (rules, v) => `Industrial waste reduced by ${pct(v)}.`,
weapon: (rules, v) => describeWeapon(v),
};
/** Turns a tech's `effects` object into an array of full sentences, one per effect. */
export function describeTechEffects(rules, effects) {
return Object.entries(effects ?? {})
.map(([key, value]) => FORMATTERS[key]?.(rules, value))
.filter(Boolean);
}

View File

@ -5,6 +5,7 @@
// MasterOfVegaGame.js/VegaScreens.js keeps those files from ballooning. // MasterOfVegaGame.js/VegaScreens.js keeps those files from ballooning.
import { habitableForEmpire } from './VegaLogic.js'; import { habitableForEmpire } from './VegaLogic.js';
import { describeTechEffects } from './VegaTechEffects.js';
// Events worth interrupting the player for. Everything else (refit, // Events worth interrupting the player for. Everything else (refit,
// spyCaught, techStolen, invasionFailed, leaderHired, victory — which // spyCaught, techStolen, invasionFailed, leaderHired, victory — which
@ -99,9 +100,8 @@ function describeTechDone(rules, state, ev) {
const emp = state.empires[ev.empire]; const emp = state.empires[ev.empire];
const gate = rules.techGates[ev.techId] ?? { buildings: [], prereqOf: [] }; const gate = rules.techGates[ev.techId] ?? { buildings: [], prereqOf: [] };
const lines = [line(tech.desc, '#9fb6cc')]; const lines = [line(tech.desc, '#9fb6cc')];
const effects = Object.entries(tech.effects ?? {}); for (const effectLine of describeTechEffects(rules, tech.effects)) {
if (effects.length) { lines.push(line(effectLine, '#7fd8a0'));
lines.push(line(`Effects: ${effects.map(([k, v]) => `${k} ${v > 0 ? '+' : ''}${v}`).join(', ')}`, '#7fd8a0'));
} }
const newBuildingIds = gate.buildings.filter((id) => rules.buildings[id]); const newBuildingIds = gate.buildings.filter((id) => rules.buildings[id]);
const newTechs = gate.prereqOf const newTechs = gate.prereqOf
@ -115,7 +115,9 @@ function describeTechDone(rules, state, ev) {
return { return {
headline: `Research completed: ${tech.name}.`, headline: `Research completed: ${tech.name}.`,
lines, lines,
buildingUnlocks: newBuildingIds.map((id) => ({ id, name: rules.buildings[id].name })), buildingUnlocks: newBuildingIds.map((id) => ({
id, name: rules.buildings[id].name, desc: rules.buildings[id].desc,
})),
}; };
} }

View File

@ -37,7 +37,6 @@ const ROW_BG = 0x142238;
// fallback ladder), a building just its one icon. // fallback ladder), a building just its one icon.
const MEDIA_SIZE = 72; const MEDIA_SIZE = 72;
const HULL_ICON_SIZE = 52; const HULL_ICON_SIZE = 52;
const SHIP_MEDIA_W = 150;
const BUILDING_MEDIA_W = 90; const BUILDING_MEDIA_W = 90;
// Event types whose row gets a "View Star System" shortcut — anything // Event types whose row gets a "View Star System" shortcut — anything
@ -95,7 +94,10 @@ export function openTurnReportScreen(scene, rules, state, events, onClose) {
const linesTop = cy; const linesTop = cy;
const isBuilding = ev.type === 'buildingDone'; const isBuilding = ev.type === 'buildingDone';
const isShip = ev.type === 'shipDone'; const isShip = ev.type === 'shipDone';
const mediaW = isShip ? SHIP_MEDIA_W : (isBuilding ? BUILDING_MEDIA_W : 0); // A ship's media block now sits below its text (full-width synopsis);
// only a building's icon still sits beside its text, so only that
// case needs to reserve wrap width off to the side.
const mediaW = isBuilding ? BUILDING_MEDIA_W : 0;
for (const ln of desc.lines) { for (const ln of desc.lines) {
const t = scene.add.text(16, cy, ln.text, { const t = scene.add.text(16, cy, ln.text, {
fontFamily: FONT, fontSize: '15px', color: ln.color ?? '#9fb6cc', fontFamily: FONT, fontSize: '15px', color: ln.color ?? '#9fb6cc',
@ -116,7 +118,15 @@ export function openTurnReportScreen(scene, rules, state, events, onClose) {
fontFamily: FONT, fontSize: '15px', color: '#ffd88a', fontFamily: FONT, fontSize: '15px', color: '#ffd88a',
}); });
col.content.add(label); col.content.add(label);
cy += label.height + 8; cy += label.height + 6;
if (bu.desc) {
const buildingDesc = scene.add.text(16, cy, bu.desc, {
fontFamily: FONT, fontSize: '13px', color: '#9fb6cc', wordWrap: { width: w - 32 },
});
col.content.add(buildingDesc);
cy += buildingDesc.height + 8;
}
const status = scene.add.text(16, cy + 34, '', { const status = scene.add.text(16, cy + 34, '', {
fontFamily: FONT, fontSize: '13px', color: '#7fd8a0', wordWrap: { width: w - 32 }, fontFamily: FONT, fontSize: '13px', color: '#7fd8a0', wordWrap: { width: w - 32 },
@ -145,55 +155,61 @@ export function openTurnReportScreen(scene, rules, state, events, onClose) {
} }
} }
// The built item's picture, right-aligned against the synopsis text // A building's picture sits right-aligned beside its synopsis text
// it sits beside. Reserving mediaW off the wrap width above (rather // (mediaW reserved off the wrap width above keeps long descriptions
// than overlaying it) keeps long descriptions from running under it. // from running under it). A ship's hull icon + commander portrait sit
if (isBuilding || isShip) { // below its (full-width) synopsis text instead, left-aligned to match
// the text above them.
if (isBuilding) {
cy = Math.max(cy, linesTop + MEDIA_SIZE); cy = Math.max(cy, linesTop + MEDIA_SIZE);
const midY = linesTop + MEDIA_SIZE / 2; const midY = linesTop + MEDIA_SIZE / 2;
if (isBuilding) { const icon = scene.add.image(
const icon = scene.add.image( w - 8 - MEDIA_SIZE / 2, midY, scene.art.buildings, buildingFrame(rules, ev.buildingId),
w - 8 - MEDIA_SIZE / 2, midY, scene.art.buildings, buildingFrame(rules, ev.buildingId), ).setDisplaySize(MEDIA_SIZE, MEDIA_SIZE);
).setDisplaySize(MEDIA_SIZE, MEDIA_SIZE); col.content.add(icon);
col.content.add(icon); } else if (isShip) {
} else { const mediaTop = cy;
const emp = state.empires[ev.empire]; const midY = mediaTop + MEDIA_SIZE / 2;
const portrait = makeCommanderPortrait( const emp = state.empires[ev.empire];
scene, rules, scene.art, emp.speciesId, ev.hullId, const hullLeft = 16;
w - 8 - MEDIA_SIZE / 2, midY, MEDIA_SIZE, const hullIcon = makeShipIcon(
); scene, rules, scene.art, emp.speciesId, ev.hullId,
col.content.add(portrait); hullLeft + HULL_ICON_SIZE / 2, midY, HULL_ICON_SIZE,
const hullLeft = w - 8 - MEDIA_SIZE - 8 - HULL_ICON_SIZE; );
const hullIcon = makeShipIcon( col.content.add(hullIcon);
scene, rules, scene.art, emp.speciesId, ev.hullId, const portraitLeft = hullLeft + HULL_ICON_SIZE + 8;
hullLeft + HULL_ICON_SIZE / 2, midY, HULL_ICON_SIZE, const portrait = makeCommanderPortrait(
); scene, rules, scene.art, emp.speciesId, ev.hullId,
col.content.add(hullIcon); portraitLeft + MEDIA_SIZE / 2, midY, MEDIA_SIZE,
);
col.content.add(portrait);
// Same pop-over the fleet side panel opens on a ship row click // Same pop-over the fleet side panel opens on a ship row click
// (VegaSidePanel.detailHit → openShipDetail). It draws its own // (VegaSidePanel.detailHit → openShipDetail). It draws its own
// veil above this modal's depth and doesn't go through // veil above this modal's depth and doesn't go through
// scene.openModal, so it's safe to open without closing the // scene.openModal, so it's safe to open without closing the
// report first. The clip already playing here pauses while the // report first. The clip already playing here pauses while the
// pop-over runs its own copy at full size, same as the side // pop-over runs its own copy at full size, same as the side
// panel's pool does — otherwise the same clip decodes twice. // panel's pool does — otherwise the same clip decodes twice.
const isVideoPortrait = portrait.type === 'Video'; const isVideoPortrait = portrait.type === 'Video';
const hit = scene.add.rectangle(hullLeft, linesTop, w - 8 - hullLeft, MEDIA_SIZE, 0xffffff, 0.001) const hit = scene.add.rectangle(
.setOrigin(0, 0).setInteractive({ useHandCursor: true }); hullLeft, mediaTop, portraitLeft + MEDIA_SIZE - hullLeft, MEDIA_SIZE, 0xffffff, 0.001,
hit.on('pointerup', (p) => { ).setOrigin(0, 0).setInteractive({ useHandCursor: true });
if (!col.contains(p) || shipDetail) return; hit.on('pointerup', (p) => {
if (isVideoPortrait && portrait.scene) portrait.pause?.(); if (!col.contains(p) || shipDetail) return;
shipDetail = openShipDetail( if (isVideoPortrait && portrait.scene) portrait.pause?.();
scene, rules, state, scene.art, shipDetail = openShipDetail(
{ empireIdx: ev.empire, hullId: ev.hullId }, scene, rules, state, scene.art,
() => { { empireIdx: ev.empire, hullId: ev.hullId },
shipDetail = null; () => {
if (isVideoPortrait && portrait.scene) portrait.resume?.(); shipDetail = null;
}, if (isVideoPortrait && portrait.scene) portrait.resume?.();
); },
}); );
col.content.add(hit); });
} col.content.add(hit);
cy = mediaTop + MEDIA_SIZE + 10;
} }
// Discovery and production rows get a shortcut straight to the // Discovery and production rows get a shortcut straight to the