232 lines
10 KiB
JavaScript
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 };
|
|
}
|