fertig-classic-games/src/games/mastervega/VegaTurnReport.js

232 lines
10 KiB
JavaScript

// Master of Vega — turn-report classification and detail text. Headless, no
// Phaser: this only turns entries from the event bus (`state.events`, pushed
// by `pushEvent` in VegaLogic.js) into the headline+lines the "New Turn"
// popup renders. Keeping the per-type text formatting out of
// MasterOfVegaGame.js/VegaScreens.js keeps those files from ballooning.
import { habitableForEmpire } from './VegaLogic.js';
// 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
// in the next turn's report on top of that would announce it twice — the
// 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', 'buildingDone', 'shipDone', 'contact', 'captured',
'colonyDestroyed', 'warDeclared', 'peace', 'alliance', 'council',
'councilRefused', 'eliminated',
]);
// 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', 'buildingDone', 'shipDone',
'populationSent', 'populationDelivered',
]);
export function isRelevantToHuman(ev, me) {
if (PERSONAL_TYPES.has(ev.type)) return ev.empire === me;
return true;
}
export const TYPE_LABEL = {
discovered: 'Discovery',
techDone: 'Research',
buildingDone: 'Building Complete',
shipDone: 'Ship Complete',
contact: 'First Contact',
captured: 'Colony Captured',
colonyDestroyed: 'Colony Destroyed',
warDeclared: 'War Declared',
peace: 'Peace',
alliance: 'Alliance',
council: 'Galactic Council',
councilRefused: 'Council Refused',
eliminated: 'Empire Eliminated',
};
// Sort weight — diplomacy/territory/council news first (most consequential),
// 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;
const line = (text, color) => ({ text, color });
function describeDiscovered(rules, state, ev) {
const star = state.galaxy.stars[ev.starIdx];
const cls = rules.starClasses[star.classId];
const lines = [line(`${cls.name} star — ${cls.desc}`, '#9fb6cc')];
if (!star.planets.length) {
lines.push(line('No planets in this system.', '#6b7f96'));
} else {
for (const p of star.planets) {
const type = rules.planetTypes[p.typeId];
const size = rules.planetSizes[p.sizeId];
const rich = rules.richness[p.richId];
const grav = rules.gravity[p.gravId];
const habitable = habitableForEmpire(rules, state, ev.empire, ev.starIdx, p.orbit);
lines.push(line(
`Planet ${p.orbit + 1}: ${size.name} ${type.name}, ${rich.name} minerals, ${grav.name}`
+ `${habitable ? ' — habitable' : ''}`,
habitable ? '#7fd8a0' : '#9fb6cc',
));
}
}
return { headline: `Our ships have discovered the ${star.name} system.`, lines };
}
function describeTechDone(rules, state, ev) {
const tech = rules.techs[ev.techId];
const emp = state.empires[ev.empire];
const gate = rules.techGates[ev.techId] ?? { buildings: [], prereqOf: [] };
const lines = [line(tech.desc, '#9fb6cc')];
const effects = Object.entries(tech.effects ?? {});
if (effects.length) {
lines.push(line(`Effects: ${effects.map(([k, v]) => `${k} ${v > 0 ? '+' : ''}${v}`).join(', ')}`, '#7fd8a0'));
}
const newBuildings = gate.buildings.map((id) => rules.buildings[id]?.name).filter(Boolean);
const newTechs = gate.prereqOf
.filter((id) => emp.available[id] && !emp.known[id])
.map((id) => rules.techs[id]?.name)
.filter(Boolean);
const unlocks = [...newBuildings, ...newTechs];
if (unlocks.length) {
lines.push(line(`Now available: ${unlocks.join(', ')}`, '#ffd88a'));
}
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;
if (ev.empire === me) otherIdx = ev.other;
else if (ev.other === me) otherIdx = ev.empire;
if (otherIdx === undefined) {
return {
headline: `The ${name(ev.empire)} and the ${name(ev.other)} have made contact.`,
lines: [],
};
}
const other = state.empires[otherIdx];
const spec = rules.species[other.speciesId];
const lines = [line(spec.desc, '#9fb6cc')];
if (spec.strengths?.length) lines.push(line(`Strengths: ${spec.strengths.join(', ')}`, '#7fd8a0'));
if (spec.weaknesses?.length) lines.push(line(`Weaknesses: ${spec.weaknesses.join(', ')}`, '#e08a8a'));
return { headline: `First contact: the ${spec.name}.`, lines };
}
function describeCaptured(rules, state, ev, name) {
const me = state.humanIndex;
const star = state.galaxy.stars[ev.starIdx];
let headline;
if (ev.empire === me) headline = `We have captured ${star.name} from the ${name(ev.from)}.`;
else if (ev.from === me) headline = `The ${name(ev.empire)} have captured ${star.name} from us.`;
else headline = `The ${name(ev.empire)} have captured ${star.name} from the ${name(ev.from)}.`;
return { headline, lines: [] };
}
function describeColonyDestroyed(rules, state, ev, name) {
const me = state.humanIndex;
const star = state.galaxy.stars[ev.starIdx];
let headline;
if (ev.target === me) headline = `The ${name(ev.empire)} have bombed ${star.name} out of existence — our colony is lost.`;
else if (ev.empire === me) headline = `We have bombed ${star.name} out of existence, destroying the ${name(ev.target)}'s colony.`;
else headline = `${star.name} has been bombed out of existence by the ${name(ev.empire)}.`;
return { headline, lines: [] };
}
function describeTreaty(rules, state, ev, name, verb) {
const me = state.humanIndex;
let headline;
if (ev.empire === me) headline = `We ${verb} the ${name(ev.other)}.`;
else if (ev.other === me) headline = `The ${name(ev.empire)} ${verb} us.`;
else headline = `The ${name(ev.empire)} ${verb} the ${name(ev.other)}.`;
return { headline, lines: [] };
}
function describeCouncil(rules, state, ev, name) {
const need = Math.ceil(ev.totalPop * rules.council.winFraction);
const lines = (ev.candidates ?? []).map((idx) => line(
`${name(idx)}: ${Math.round(ev.votes?.[idx] ?? 0)} votes`, state.empires[idx]?.color,
));
lines.push(line(`Abstained: ${Math.round(ev.abstained ?? 0)} · ${need} votes of ${Math.round(ev.totalPop)} needed`, '#7f97b3'));
const headline = ev.winner >= 0
? `${name(ev.winner)} is elected High Guardian of the Galaxy.`
: (ev.refused
? 'The Council election was refused — war has begun.'
: 'The Council failed to elect a High Guardian.');
return { headline, lines };
}
function describeCouncilRefused(rules, state, ev, name) {
const me = state.humanIndex;
let headline;
if (ev.empire === me) headline = `We refuse to submit to the ${name(ev.winner)}. The Council election is void.`;
else if (ev.winner === me) headline = `The ${name(ev.empire)} refuse to submit to us. The Council election is void — war has begun.`;
else headline = `The ${name(ev.empire)} refuse to submit to the ${name(ev.winner)}. The Council election is void.`;
return { headline, lines: [] };
}
function describeEliminated(rules, state, ev, name) {
const me = state.humanIndex;
const headline = ev.empire === me
? 'We have been eliminated from the galaxy.'
: `The ${name(ev.empire)} have been eliminated from the galaxy.`;
return { headline, lines: [] };
}
/** Turns one event into {category, headline, lines} for the turn-report row. */
export function describeEvent(rules, state, ev) {
const name = (i) => state.empires[i]?.name ?? '?';
let out;
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;
case 'warDeclared': out = describeTreaty(rules, state, ev, name, 'declare war on'); break;
case 'peace': out = describeTreaty(rules, state, ev, name, 'make peace with'); break;
case 'alliance': out = describeTreaty(rules, state, ev, name, 'form an alliance with'); break;
case 'council': out = describeCouncil(rules, state, ev, name); break;
case 'councilRefused': out = describeCouncilRefused(rules, state, ev, name); break;
case 'eliminated': out = describeEliminated(rules, state, ev, name); break;
default: out = { headline: ev.type, lines: [] };
}
return { category: TYPE_LABEL[ev.type] ?? ev.type, ...out };
}