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:
parent
a7c3ef20e4
commit
0adb689c19
|
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue