feat(mastervega): add visual reports for building and ship completion
Promote buildingDone and shipDone events from ticker-only to notable interrupting events with dedicated report rows. Each row now displays: - A building icon or ship hull icon + commander portrait - Clickable ship detail overlay (video/image portrait) - "View Star System" shortcut button for quick navigation Mirrors the existing treatment of discoveries and research completions, giving players immediate visual feedback on production milestones.
This commit is contained in:
parent
01c5957b59
commit
d4daa990a1
|
|
@ -6,10 +6,9 @@
|
|||
|
||||
import { habitableForEmpire } from './VegaLogic.js';
|
||||
|
||||
// Events worth interrupting the player for. Everything else (buildingDone,
|
||||
// shipDone, refit, spyCaught, techStolen, invasionFailed, leaderHired,
|
||||
// victory — which already has its own showVictoryOverlay) stays in the
|
||||
// small ticker log only.
|
||||
// Events worth interrupting the player for. Everything else (refit,
|
||||
// spyCaught, techStolen, invasionFailed, leaderHired, victory — which
|
||||
// already has its own showVictoryOverlay) stays in the small ticker log only.
|
||||
//
|
||||
// `colonised` is deliberately NOT here. Founding a colony gets its own
|
||||
// full-screen vignette the instant it happens (VegaColonyIntro.js), and a row
|
||||
|
|
@ -17,21 +16,22 @@ import { habitableForEmpire } from './VegaLogic.js';
|
|||
// second time with less weight than a finished refit. Do not add it back
|
||||
// without taking the vignette out.
|
||||
export const NOTABLE_TYPES = new Set([
|
||||
'discovered', 'techDone', 'contact', 'captured',
|
||||
'discovered', 'techDone', 'buildingDone', 'shipDone', 'contact', 'captured',
|
||||
'colonyDestroyed', 'warDeclared', 'peace', 'alliance', 'council',
|
||||
'councilRefused', 'eliminated',
|
||||
]);
|
||||
|
||||
// Exploring, researching and founding a colony are personal — only the
|
||||
// empire that did them cares. Everything else (contact, territory changing
|
||||
// hands, diplomacy, Council results, eliminations) is galaxy news broadcast
|
||||
// to the player regardless of who it happened to, matching the precedent
|
||||
// the old ticker-log code set (MasterOfVegaGame.processTurnEvents(), née
|
||||
// announceEvents()) for captured/colonyDestroyed/warDeclared/contact.
|
||||
// Exploring, researching, founding a colony and finishing a build-queue item
|
||||
// are personal — only the empire that did them cares. Everything else
|
||||
// (contact, territory changing hands, diplomacy, Council results,
|
||||
// eliminations) is galaxy news broadcast to the player regardless of who it
|
||||
// happened to, matching the precedent the old ticker-log code set
|
||||
// (MasterOfVegaGame.processTurnEvents(), née announceEvents()) for
|
||||
// captured/colonyDestroyed/warDeclared/contact.
|
||||
//
|
||||
// `colonised` stays classified here even though it is no longer notable: the
|
||||
// ticker log runs every event past isRelevantToHuman() too.
|
||||
const PERSONAL_TYPES = new Set(['discovered', 'techDone', 'colonised']);
|
||||
const PERSONAL_TYPES = new Set(['discovered', 'techDone', 'colonised', 'buildingDone', 'shipDone']);
|
||||
|
||||
export function isRelevantToHuman(ev, me) {
|
||||
if (PERSONAL_TYPES.has(ev.type)) return ev.empire === me;
|
||||
|
|
@ -41,6 +41,8 @@ export function isRelevantToHuman(ev, me) {
|
|||
export const TYPE_LABEL = {
|
||||
discovered: 'Discovery',
|
||||
techDone: 'Research',
|
||||
buildingDone: 'Building Complete',
|
||||
shipDone: 'Ship Complete',
|
||||
contact: 'First Contact',
|
||||
captured: 'Colony Captured',
|
||||
colonyDestroyed: 'Colony Destroyed',
|
||||
|
|
@ -53,12 +55,14 @@ export const TYPE_LABEL = {
|
|||
};
|
||||
|
||||
// Sort weight — diplomacy/territory/council news first (most consequential),
|
||||
// then discoveries, then research, mirroring how the plan orders the list.
|
||||
// then discoveries, then research, then finished production, mirroring how
|
||||
// the plan orders the list.
|
||||
const CATEGORY_ORDER = {
|
||||
contact: 0, warDeclared: 0, peace: 0, alliance: 0, council: 0, councilRefused: 0, eliminated: 0,
|
||||
captured: 1, colonyDestroyed: 1,
|
||||
discovered: 2,
|
||||
techDone: 3,
|
||||
buildingDone: 4, shipDone: 4,
|
||||
};
|
||||
export const categoryWeight = (ev) => CATEGORY_ORDER[ev.type] ?? 9;
|
||||
|
||||
|
|
@ -108,6 +112,18 @@ function describeTechDone(rules, state, ev) {
|
|||
return { headline: `Research completed: ${tech.name}.`, lines };
|
||||
}
|
||||
|
||||
function describeBuildingDone(rules, state, ev) {
|
||||
const b = rules.buildings[ev.buildingId];
|
||||
const star = state.galaxy.stars[ev.starIdx];
|
||||
return { headline: `${b.name} completed at ${star.name}.`, lines: [line(b.desc, '#9fb6cc')] };
|
||||
}
|
||||
|
||||
function describeShipDone(rules, state, ev) {
|
||||
const h = rules.hulls[ev.hullId];
|
||||
const star = state.galaxy.stars[ev.starIdx];
|
||||
return { headline: `${h.name} completed at ${star.name}.`, lines: [line(h.desc, '#9fb6cc')] };
|
||||
}
|
||||
|
||||
function describeContact(rules, state, ev, name) {
|
||||
const me = state.humanIndex;
|
||||
let otherIdx;
|
||||
|
|
@ -195,6 +211,8 @@ export function describeEvent(rules, state, ev) {
|
|||
switch (ev.type) {
|
||||
case 'discovered': out = describeDiscovered(rules, state, ev); break;
|
||||
case 'techDone': out = describeTechDone(rules, state, ev); break;
|
||||
case 'buildingDone': out = describeBuildingDone(rules, state, ev); break;
|
||||
case 'shipDone': out = describeShipDone(rules, state, ev); break;
|
||||
case 'contact': out = describeContact(rules, state, ev, name); break;
|
||||
case 'captured': out = describeCaptured(rules, state, ev, name); break;
|
||||
case 'colonyDestroyed': out = describeColonyDestroyed(rules, state, ev, name); break;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
|
||||
import { modalShell, FONT } from './VegaScreens.js';
|
||||
import { scrollColumn } from './VegaColonyView.js';
|
||||
import { openSystemView } from './VegaSystemView.js';
|
||||
import { makeShipIcon, makeCommanderPortrait } from './VegaShipMedia.js';
|
||||
import { openShipDetail } from './VegaShipDetail.js';
|
||||
import { buildingFrame } from './VegaArt.js';
|
||||
import { turnToYear } from './VegaRules.js';
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { describeEvent, categoryWeight } from './VegaTurnReport.js';
|
||||
|
|
@ -17,6 +21,19 @@ import { describeEvent, categoryWeight } from './VegaTurnReport.js';
|
|||
const ACCENT = 0x6fc4ff;
|
||||
const ROW_BG = 0x142238;
|
||||
|
||||
// Sizing for the built-item picture on 'buildingDone'/'shipDone' rows. A ship
|
||||
// gets its hull icon plus its commander's portrait (video where the species
|
||||
// has one recorded, an image otherwise — makeCommanderPortrait's own
|
||||
// fallback ladder), a building just its one icon.
|
||||
const MEDIA_SIZE = 72;
|
||||
const HULL_ICON_SIZE = 52;
|
||||
const SHIP_MEDIA_W = 150;
|
||||
const BUILDING_MEDIA_W = 90;
|
||||
|
||||
// Event types whose row gets a "View Star System" shortcut — anything
|
||||
// personal to a single one of the human's own stars.
|
||||
const VIEW_SYSTEM_TYPES = new Set(['discovered', 'buildingDone', 'shipDone']);
|
||||
|
||||
export function openTurnReportScreen(scene, rules, state, events, onClose) {
|
||||
const shell = modalShell(scene, `New Turn: ${turnToYear(state.turn)}`, onClose, { width: 1180, height: 760 });
|
||||
|
||||
|
|
@ -30,11 +47,15 @@ export function openTurnReportScreen(scene, rules, state, events, onClose) {
|
|||
// Everything starts expanded — the player came here to read the news, not
|
||||
// to hunt for it — but each row stays individually collapsible.
|
||||
const expanded = new Set(ordered.map((o) => o.i));
|
||||
// One at a time, same as VegaSidePanel's own openDetail guard — and
|
||||
// survives across render() calls (a row expanding/collapsing elsewhere
|
||||
// rebuilds the whole column), which a variable local to render() would not.
|
||||
let shipDetail = null;
|
||||
|
||||
const render = () => {
|
||||
col.content.removeAll(true);
|
||||
let cy = 0;
|
||||
for (const { i, desc } of ordered) {
|
||||
for (const { i, ev, desc } of ordered) {
|
||||
const rowTop = cy;
|
||||
const isOpen = expanded.has(i);
|
||||
// bg has to be built AFTER the row's text so its final height is known
|
||||
|
|
@ -61,14 +82,94 @@ export function openTurnReportScreen(scene, rules, state, events, onClose) {
|
|||
cy += headline.height + 10;
|
||||
|
||||
if (isOpen) {
|
||||
const linesTop = cy;
|
||||
const isBuilding = ev.type === 'buildingDone';
|
||||
const isShip = ev.type === 'shipDone';
|
||||
const mediaW = isShip ? SHIP_MEDIA_W : (isBuilding ? BUILDING_MEDIA_W : 0);
|
||||
for (const ln of desc.lines) {
|
||||
const t = scene.add.text(16, cy, ln.text, {
|
||||
fontFamily: FONT, fontSize: '15px', color: ln.color ?? '#9fb6cc', wordWrap: { width: w - 46 },
|
||||
fontFamily: FONT, fontSize: '15px', color: ln.color ?? '#9fb6cc',
|
||||
wordWrap: { width: w - 46 - mediaW },
|
||||
});
|
||||
col.content.add(t);
|
||||
cy += t.height + 6;
|
||||
}
|
||||
cy += 4;
|
||||
|
||||
// The built item's picture, right-aligned against the synopsis text
|
||||
// it sits beside. Reserving mediaW off the wrap width above (rather
|
||||
// than overlaying it) keeps long descriptions from running under it.
|
||||
if (isBuilding || isShip) {
|
||||
cy = Math.max(cy, linesTop + MEDIA_SIZE);
|
||||
const midY = linesTop + MEDIA_SIZE / 2;
|
||||
if (isBuilding) {
|
||||
const icon = scene.add.image(
|
||||
w - 8 - MEDIA_SIZE / 2, midY, scene.art.buildings, buildingFrame(rules, ev.buildingId),
|
||||
).setDisplaySize(MEDIA_SIZE, MEDIA_SIZE);
|
||||
col.content.add(icon);
|
||||
} else {
|
||||
const emp = state.empires[ev.empire];
|
||||
const portrait = makeCommanderPortrait(
|
||||
scene, rules, scene.art, emp.speciesId, ev.hullId,
|
||||
w - 8 - MEDIA_SIZE / 2, midY, MEDIA_SIZE,
|
||||
);
|
||||
col.content.add(portrait);
|
||||
const hullLeft = w - 8 - MEDIA_SIZE - 8 - HULL_ICON_SIZE;
|
||||
const hullIcon = makeShipIcon(
|
||||
scene, rules, scene.art, emp.speciesId, ev.hullId,
|
||||
hullLeft + HULL_ICON_SIZE / 2, midY, HULL_ICON_SIZE,
|
||||
);
|
||||
col.content.add(hullIcon);
|
||||
|
||||
// Same pop-over the fleet side panel opens on a ship row click
|
||||
// (VegaSidePanel.detailHit → openShipDetail). It draws its own
|
||||
// veil above this modal's depth and doesn't go through
|
||||
// scene.openModal, so it's safe to open without closing the
|
||||
// report first. The clip already playing here pauses while the
|
||||
// pop-over runs its own copy at full size, same as the side
|
||||
// panel's pool does — otherwise the same clip decodes twice.
|
||||
const isVideoPortrait = portrait.type === 'Video';
|
||||
const hit = scene.add.rectangle(hullLeft, linesTop, w - 8 - hullLeft, MEDIA_SIZE, 0xffffff, 0.001)
|
||||
.setOrigin(0, 0).setInteractive({ useHandCursor: true });
|
||||
hit.on('pointerup', (p) => {
|
||||
if (!col.contains(p) || shipDetail) return;
|
||||
if (isVideoPortrait && portrait.scene) portrait.pause?.();
|
||||
shipDetail = openShipDetail(
|
||||
scene, rules, state, scene.art,
|
||||
{ empireIdx: ev.empire, hullId: ev.hullId },
|
||||
() => {
|
||||
shipDetail = null;
|
||||
if (isVideoPortrait && portrait.scene) portrait.resume?.();
|
||||
},
|
||||
);
|
||||
});
|
||||
col.content.add(hit);
|
||||
}
|
||||
}
|
||||
|
||||
// Discovery and production rows get a shortcut straight to the
|
||||
// orrery — the star is always the human's own (these are all
|
||||
// PERSONAL_TYPES in VegaTurnReport.js), so openSystemViewFor's
|
||||
// explored guard would always pass anyway. Closing this shell first
|
||||
// and handing its onClose (not scene.openModal, which would no-op
|
||||
// with modalOpen already true) to openSystemView keeps the
|
||||
// turn-transition bookkeeping in MasterOfVegaGame.runToHumanTurn()
|
||||
// running exactly once, whichever button ends the modal. Lives at
|
||||
// the bottom-right of the expanded synopsis, not the collapsed
|
||||
// header, so it only appears once the player has read the row.
|
||||
if (VIEW_SYSTEM_TYPES.has(ev.type)) {
|
||||
const viewBtn = new Button(scene, w - 108, cy + 14, 'View Star System', () => {
|
||||
col.destroy();
|
||||
shell.destroy();
|
||||
openSystemView(scene, rules, state, ev.starIdx, scene.art, {
|
||||
viewerIdx: state.humanIndex,
|
||||
onChanged: () => scene.refreshAll?.(),
|
||||
onClose,
|
||||
});
|
||||
}, { width: 168, height: 28, fontSize: 13 });
|
||||
col.content.add(viewBtn);
|
||||
cy += 36;
|
||||
}
|
||||
}
|
||||
|
||||
const bg = scene.add.rectangle(0, rowTop, w, cy - rowTop, ROW_BG, 0).setOrigin(0, 0)
|
||||
|
|
|
|||
Loading…
Reference in New Issue