// 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 }; }