diff --git a/src/games/mastervega/MasterOfVegaGame.js b/src/games/mastervega/MasterOfVegaGame.js index d8e76ee..faed684 100644 --- a/src/games/mastervega/MasterOfVegaGame.js +++ b/src/games/mastervega/MasterOfVegaGame.js @@ -821,9 +821,16 @@ export default class MasterOfVegaGame extends Phaser.Scene { onChanged: () => this.refreshAll(), onClose: done, }))], - ['Council', () => this.openModal((done) => - openCouncilScreen(this, this.rules, this.state, done))], ]; + // The Council only earns a spot in this menu once it has actually + // convened for the first time — before that, `lastResult` is still null + // (VegaLogic.js's createGame) and the button would just open onto + // openCouncilScreen's "The Council has not yet convened" empty state + // (Brian's ask, 2026-08-14). + if (this.state.council.lastResult) { + items.push(['Council', () => this.openModal((done) => + openCouncilScreen(this, this.rules, this.state, done))]); + } const BTN_W = 200; const BTN_H = 42; diff --git a/src/games/mastervega/VegaColoniesScreen.js b/src/games/mastervega/VegaColoniesScreen.js index 6c3b5ed..012c7ae 100644 --- a/src/games/mastervega/VegaColoniesScreen.js +++ b/src/games/mastervega/VegaColoniesScreen.js @@ -741,12 +741,40 @@ export function openColoniesScreen(scene, rules, state, e, art, opts = {}) { (p) => openFocusDropdown(colony, 'alloc', p.x, p.y), { fontSize: 14 }); const canSend = maxSendablePopulation(colony) > 0 && empireColonies(state, e).length > 1; - pill(scroller.content, COL_SEND, y + 10, 44, 40, '⇄', armed ? '#0b1220' : '#cfe8ff', + const sendPill = pill(scroller.content, COL_SEND, y + 10, 44, 40, '⇄', armed ? '#0b1220' : '#cfe8ff', armed ? 0xffd88a : 0x16253c, () => { armedSourceId = armed ? null : colony.id; refreshBanner(); rebuildRows(); }, { enabled: canSend, fontSize: 18 }); + // pill() only calls setInteractive() when enabled — done here too so a + // disabled send button can still explain over a tooltip WHY it's off. + if (!canSend) sendPill.setInteractive(); + tooltip.attachTo(sendPill, () => ({ + title: 'Send Population', + lines: [ + { text: "Ships part of this colony's population to another colony you own." }, + !canSend + ? { + text: empireColonies(state, e).length <= 1 + ? 'You need at least one other colony to send population to.' + : 'Too little population here to spare any — at least 0.5 must stay behind.', + color: '#e08a8a', + } + : { + text: armed + ? 'Armed — click a destination colony now, or click this again to cancel.' + : `Click to arm this colony as the source, then click another colony's row to pick ` + + `an amount and send it. Up to ${maxSendablePopulation(colony).toFixed(1)} available.`, + color: '#9fd8ff', + }, + { + text: "Travels as an unescorted transport — arrival takes time based on distance, and " + + "anything beyond the destination's own population cap is wasted, not returned.", + color: '#6f8aa3', + }, + ], + })); pill(scroller.content, COL_MANAGE, y + 10, 100, 40, 'Manage', '#cfe8ff', 0x16253c, () => openManage(colony)); diff --git a/src/games/mastervega/VegaColonyView.js b/src/games/mastervega/VegaColonyView.js index 2f88a7d..12fa44c 100644 --- a/src/games/mastervega/VegaColonyView.js +++ b/src/games/mastervega/VegaColonyView.js @@ -82,6 +82,7 @@ export const CHANNEL_COLOUR = { // actually does; this is just the picker's label/description copy. export const COLONY_FOCUS_OPTIONS = [ { value: 'manual', label: 'Manual', desc: 'No automation — you queue everything yourself.' }, + { value: 'expansion', label: 'Galactic Expansion', desc: 'Builds a colony ship for every known unclaimed world, then two frigate escorts per colony ship in the fleet once that demand is covered.' }, { value: 'improvement', label: 'Colony Improvement', desc: 'Industry buildings first, then any other building not yet built.' }, { value: 'research', label: 'Research Focus', desc: 'Only queues research buildings. Once none are left, the queue stays empty and construction spills into research.' }, { value: 'fleet', label: 'Fleet Production', desc: 'Builds a diversified warship fleet at this system, targeting a 4:3:2:1 mix of frigates : destroyers : cruisers : battleships.' }, @@ -96,8 +97,12 @@ export const ALLOCATION_FOCUS_OPTIONS = [ { key: 'default', label: 'Default', sliders: { ships: 0.20, defense: 0.10, industry: 0.40, ecology: 0.10, research: 0.20 } }, { key: 'research', label: 'Research Focus', sliders: { ships: 0.10, defense: 0.05, industry: 0.20, ecology: 0.15, research: 0.50 } }, { key: 'growth', label: 'Population Growth', sliders: { ships: 0.15, defense: 0.10, industry: 0.30, ecology: 0.35, research: 0.10 } }, - { key: 'production', label: 'Production', sliders: { ships: 0.20, defense: 0.05, industry: 0.55, ecology: 0.10, research: 0.10 } }, + { key: 'production', label: 'Industrial Buildout', sliders: { ships: 0.20, defense: 0.05, industry: 0.55, ecology: 0.10, research: 0.10 } }, { key: 'military', label: 'Military Buildup', sliders: { ships: 0.45, defense: 0.25, industry: 0.15, ecology: 0.10, research: 0.05 } }, + // Military Buildup with its Defence share folded into Construction instead + // — Expansion wants colony ships and their frigate escorts (both built via + // the ships/Construction channel), not planetary batteries. + { key: 'expansion', label: 'Expansion', sliders: { ships: 0.70, defense: 0.00, industry: 0.15, ecology: 0.10, research: 0.05 } }, ]; export const etaText = (turns) => (Number.isFinite(turns) diff --git a/src/games/mastervega/VegaLogic.js b/src/games/mastervega/VegaLogic.js index 95fa613..6383c5b 100644 --- a/src/games/mastervega/VegaLogic.js +++ b/src/games/mastervega/VegaLogic.js @@ -294,6 +294,41 @@ export function habitableForEmpire(rules, state, e, starIdx, orbit) { return type.hostility <= comps.colonizeHostility; } +// How many colonizable worlds this empire has actually SEEN (explored[]) +// that nobody has claimed yet — the "Galactic Expansion" Colony Focus's +// build target, and what the advisor recommendations gauge "is the galaxy +// still open" against. Deliberately explored-gated rather than every +// canColonize world in the galaxy (which would recommend expansion into +// worlds the player has no idea exist yet), and deliberately NOT further +// gated by current fuel range like VegaAI.js's own targeting scan — a world +// just past today's range is still "available" once a colony ship reaches +// it, and fuel range only grows with tech. +export function discoveredOpenWorlds(rules, state, e) { + const emp = state.empires[e]; + let count = 0; + for (let starIdx = 0; starIdx < state.galaxy.stars.length; starIdx += 1) { + if (!emp.explored[starIdx]) continue; + const star = state.galaxy.stars[starIdx]; + for (let orbit = 0; orbit < star.planets.length; orbit += 1) { + if (canColonize(rules, state, e, starIdx, orbit)) count += 1; + } + } + return count; +} + +// Whether "Galactic Expansion" (Colony Focus) / "Expansion" (Allocation +// Focus) should be the advisors' top pick right now (Brian's ask, +// 2026-08-14): early game, before any rival has been met, land grabs are +// unambiguously the right call — but the instant either condition flips +// (first contact, or every known world already claimed) it drops out of the +// running entirely rather than fading gradually, so the player gets a clean +// "that phase is over" signal instead of a slowly-diminishing nudge. +function expansionFavored(rules, state, e) { + const hasMetAnyone = state.empires.some((o) => o.alive && o.idx !== e && state.empires[e].contacted[o.idx]); + const openTargets = discoveredOpenWorlds(rules, state, e); + return { favored: !hasMetAnyone && openTargets > 0, openTargets }; +} + // -------------------------------------------------------------------------- // Range — the star map's "range as light", and the engine's movement rule. @@ -2174,10 +2209,53 @@ function pickFleet(rules, state, colony) { enqueue(rules, state, colony, 'ship', best ?? 'frigate'); } +// How many of `hullId` this empire already has committed, empire-wide: built +// and sitting in a fleet, PLUS still under construction in any colony's +// queue. Counting the queue too keeps every expansion-focused colony reading +// the same live total instead of each one independently topping up toward +// the same target the instant its own queue goes empty — pickExpansion calls +// this every time it is asked for the next item, so double-committing would +// otherwise be the default outcome, not an edge case. +function empireHullCommitment(state, e, hullId) { + let n = 0; + for (const f of empireFleets(state, e)) { + for (const s of f.ships) if (s.hullId === hullId) n += s.count; + } + for (const colony of empireColonies(state, e)) { + for (const item of colony.queue) if (item.kind === 'ship' && item.id === hullId) n += 1; + } + return n; +} + +// Colony Focus: Galactic Expansion (Brian's ask, 2026-08-14). One colony +// ship for every discovered-but-unclaimed world (discoveredOpenWorlds), then +// — once that demand is covered — a standing 2-frigates-per-colony-ship +// escort ratio empire-wide. This is a RATIO, not a literal per-ship escort +// assignment (mastervega has no ship-to-ship escort tagging); "two frigates +// exist for every colony ship in the fleet" is what satisfies it. No +// fallback once both targets are met, same as pickResearchOnly — an empty +// queue is correct and processColony spills the unspent BC into research. +function pickExpansion(rules, state, colony) { + const e = colony.empireIdx; + const openTargets = discoveredOpenWorlds(rules, state, e); + if (empireHullCommitment(state, e, 'colonyship') < openTargets) { + enqueue(rules, state, colony, 'ship', 'colonyship'); + return; + } + let colonyShipsBuilt = 0; + for (const f of empireFleets(state, e)) { + for (const s of f.ships) if (s.hullId === 'colonyship') colonyShipsBuilt += s.count; + } + if (empireHullCommitment(state, e, 'frigate') < colonyShipsBuilt * 2) { + enqueue(rules, state, colony, 'ship', 'frigate'); + } +} + const FOCUS_PICKERS = { improvement: pickImprovement, research: pickResearchOnly, fleet: pickFleet, + expansion: pickExpansion, growth: pickGrowth, trade: pickTrade, defense: pickDefense, @@ -2218,6 +2296,33 @@ export function empireFleetPower(rules, state, e) { // reused rather than inventing a separate number. const fleetAdequacyThreshold = (state) => 400 + state.turn * 4; +// How urgently the advisors push "Fleet Production" (recommendColonyFocus's +// fleet rung) — a multiplier on fleetAdequacyThreshold rather than a flat +// yes/no, so it fires on a sliding bar instead of ever getting stuck fully +// on or off. Before first contact there is nobody to threaten the empire or +// be threatened by, so the rung is throttled hard rather than firing off the +// raw baseline alone (Brian's ask). Once contact exists, the COLDEST +// contacted relationship sets the tone rather than the average — one +// species turning hostile matters more than three others staying friendly — +// so a fleet the player hasn't needed yet can suddenly read as advisable +// again, and eases back off once every contact is warm. Mirrors +// VegaDiplomacy.js's moodOf tiers (-60/-20/20/60) without importing it: +// VegaDiplomacy.js imports FROM this file, so the reverse import would be +// circular. +export function fleetUrgency(state, e) { + const others = state.empires.filter((o) => o.alive && o.idx !== e && state.empires[e].contacted[o.idx]); + if (!others.length) return { multiplier: 0.25, threat: null }; + let coldest = others[0]; + for (const o of others) { + if ((o.attitude[e] ?? 0) < (coldest.attitude[e] ?? 0)) coldest = o; + } + const att = coldest.attitude[e] ?? 0; + if (att <= -60) return { multiplier: 1.5, threat: { empire: coldest, moodWord: 'hostile' } }; + if (att <= -20) return { multiplier: 1.15, threat: { empire: coldest, moodWord: 'cold' } }; + if (att < 20) return { multiplier: 0.85, threat: null }; + return { multiplier: 0.5, threat: null }; +} + /** * First applicable rung of a priority ladder — deliberately mirrors * FOCUS_PICKERS' order, but reports the first CATEGORY that applies rather @@ -2232,32 +2337,60 @@ export function recommendColonyFocus(rules, state, colony) { const maxPop = colonyMaxPop(rules, state, colony); const e = colony.empireIdx; + const expansion = expansionFavored(rules, state, e); + if (expansion.favored) { + return { + value: 'expansion', label: 'Galactic Expansion', + reason: `${expansion.openTargets} known world${expansion.openTargets === 1 ? '' : 's'} ` + + `${expansion.openTargets === 1 ? 'is' : 'are'} still unclaimed and no rival has been met yet — ` + + 'grab territory now while the galaxy is still open.', + }; + } + const growthId = firstEligibleBuildingId(rules, state, colony, growthBuildingIds(rules)); if (colony.pop < maxPop * 0.7 && growthId) { const pct = Math.round((colony.pop / maxPop) * 100); return { value: 'growth', label: 'Population Growth', reason: `This ${type.name.toLowerCase()} world is at ${pct}% of its population ceiling — ` - + `a ${rules.buildings[growthId].name} would raise it.`, + + `a ${rules.buildings[growthId].name} would raise it before this colony turns to military objectives.`, }; } + // Two separate reasons to recommend Colony Improvement here, both ahead of + // Fleet Production in this ladder: factories deep in the red (< 70% of cap + // — Brian's ask, 2026-08-14, same "red" line recommendAllocationFocus's + // own Industrial Buildout rung already used) mean this colony hasn't built + // out its own infrastructure yet and shouldn't be steered toward warships + // before it has; factories nearly AT cap (>= 90%) is the opposite + // situation — capacity is about to run out and another building keeps + // growth going. Both point at the same fix, so they share one rung. const industryId = firstEligibleBuildingId(rules, state, colony, industryBuildingIds(rules)); const factoryCap = colonyFactoryCap(rules, state, colony); const effF = effectiveFactories(rules, state, colony); - if (effF >= factoryCap * 0.9 && industryId) { + const factoriesBehind = effF < factoryCap * 0.7; + const factoriesNearCap = effF >= factoryCap * 0.9; + if ((factoriesBehind || factoriesNearCap) && industryId) { return { value: 'improvement', label: 'Colony Improvement', - reason: `Factories are running at ${Math.floor(effF)}/${factoryCap} — ` - + `a ${rules.buildings[industryId].name} would raise your production ceiling.`, + reason: factoriesBehind + ? `Factories are well below your cap (${Math.floor(effF)}/${factoryCap}) — build up this colony's ` + + `infrastructure with a ${rules.buildings[industryId].name} before turning toward military objectives.` + : `Factories are running at ${Math.floor(effF)}/${factoryCap} — ` + + `a ${rules.buildings[industryId].name} would raise your production ceiling.`, }; } - if (empireFleetPower(rules, state, e) < fleetAdequacyThreshold(state)) { + const { multiplier: fleetMultiplier, threat } = fleetUrgency(state, e); + if (empireFleetPower(rules, state, e) < fleetAdequacyThreshold(state) * fleetMultiplier) { return { value: 'fleet', label: 'Fleet Production', - reason: "Empire-wide fleet strength is below what's typical this far into the game — " - + 'additional warships would help, built toward a 4:3:2:1 frigate/destroyer/cruiser/battleship mix.', + reason: threat + ? `Empire-wide fleet strength is below what's typical this far into the game, and relations ` + + `with ${threat.empire.name} have turned ${threat.moodWord} — additional warships would help, ` + + 'built toward a 4:3:2:1 frigate/destroyer/cruiser/battleship mix.' + : "Empire-wide fleet strength is below what's typical this far into the game — " + + 'additional warships would help, built toward a 4:3:2:1 frigate/destroyer/cruiser/battleship mix.', }; } @@ -2312,6 +2445,16 @@ export function recommendAllocationFocus(rules, state, colony) { const effF = effectiveFactories(rules, state, colony); const e = colony.empireIdx; + const expansion = expansionFavored(rules, state, e); + if (expansion.favored) { + return { + key: 'expansion', label: 'Expansion', + reason: `${expansion.openTargets} known world${expansion.openTargets === 1 ? '' : 's'} ` + + `${expansion.openTargets === 1 ? 'is' : 'are'} still unclaimed and no rival has been met yet — ` + + 'fund colony ships and their escorts over everything else.', + }; + } + if (colony.pop < maxPop * 0.7 || colony.waste > 3) { return { key: 'growth', label: 'Population Growth', @@ -2325,7 +2468,7 @@ export function recommendAllocationFocus(rules, state, colony) { if (effF < factoryCap * 0.7) { return { - key: 'production', label: 'Production', + key: 'production', label: 'Industrial Buildout', reason: `Factories are still well below your cap (${Math.floor(effF)}/${factoryCap}) — ` + 'more Industry funding builds them out faster.', }; diff --git a/tools/verifyMasterOfVega.js b/tools/verifyMasterOfVega.js index 46db882..0aa47cf 100644 --- a/tools/verifyMasterOfVega.js +++ b/tools/verifyMasterOfVega.js @@ -1858,8 +1858,8 @@ section('6. Colony economy'); st2.rules = RULES; const c = st2.colonies[0]; const emp2 = st2.empires[c.empireIdx]; - const COLONY_FOCUS_VALUES = new Set(['manual', 'improvement', 'research', 'fleet', 'growth', 'trade', 'defense']); - const ALLOC_FOCUS_KEYS = new Set(['default', 'research', 'growth', 'production', 'military']); + const COLONY_FOCUS_VALUES = new Set(['manual', 'expansion', 'improvement', 'research', 'fleet', 'growth', 'trade', 'defense']); + const ALLOC_FOCUS_KEYS = new Set(['default', 'research', 'growth', 'production', 'military', 'expansion']); check('a fresh colony has no advisor tracking armed yet', c.advisor.focusQuietUntil === null && c.advisor.allocQuietUntil === null @@ -1882,6 +1882,20 @@ section('6. Colony economy'); c.factories = factoryCap; rec = Logic.recommendColonyFocus(RULES, st2, c); check('capped factories recommend Colony Improvement', rec.value === 'improvement', rec.value); + + // Factories deep in the red (well below cap) recommend Colony + // Improvement too — Brian's ask, 2026-08-14: infrastructure catch-up + // takes priority over military on both ladders. recommendAllocationFocus + // already had this via its Industrial Buildout rung; recommendColonyFocus + // previously only fired its improvement rung near the cap, never below it. + c.factories = Math.round(factoryCap * 0.5); + rec = Logic.recommendColonyFocus(RULES, st2, c); + check('factories well below cap recommend Colony Improvement, not Fleet Production', + rec.value === 'improvement', rec.value); + recA = Logic.recommendAllocationFocus(RULES, st2, c); + check('factories well below cap recommend Industrial Buildout, not Military Buildup', + recA.key === 'production', recA.key); + // Neither "capped" (colony focus) nor "still building out" (allocation // focus) — clears both factory rungs so the fleet check below is the // first thing either ladder actually trips on. @@ -1894,6 +1908,49 @@ section('6. Colony economy'); check('a fleetless empire this early recommends the Military Buildup allocation preset', recA.key === 'military', recA.key); + // fleetUrgency: how hard recommendColonyFocus's fleet rung pushes, + // gauged off the COLDEST contacted relationship rather than fleet power + // alone (Brian's ask, 2026-08-14). Tested directly rather than through + // recommendColonyFocus's power/threshold comparison, which would need a + // fragile hand-picked fleet size to land in the right band. + { + const other = st2.empires[1]; // kkrix, per this block's speciesIds order + let urg = Logic.fleetUrgency(st2, c.empireIdx); + check('with nobody met yet, fleet-production urgency is throttled well below baseline', + urg.multiplier < 0.5 && urg.threat === null, JSON.stringify(urg)); + + emp2.contacted[other.idx] = true; + other.contacted[emp2.idx] = true; + other.attitude[emp2.idx] = 0; + urg = Logic.fleetUrgency(st2, c.empireIdx); + check('a single neutral contact no longer throttles urgency to the pre-contact floor', + urg.multiplier > 0.5 && urg.threat === null, JSON.stringify(urg)); + + other.attitude[emp2.idx] = -70; + urg = Logic.fleetUrgency(st2, c.empireIdx); + check('a hostile contact raises urgency above the plain baseline', + urg.multiplier > 1 && urg.threat?.moodWord === 'hostile' && urg.threat.empire.idx === other.idx, + JSON.stringify(urg)); + + other.attitude[emp2.idx] = -30; + urg = Logic.fleetUrgency(st2, c.empireIdx); + check('a merely cold contact raises urgency less than an outright hostile one', + urg.multiplier > 1 && urg.multiplier < 1.5 && urg.threat?.moodWord === 'cold', JSON.stringify(urg)); + + other.attitude[emp2.idx] = 40; + urg = Logic.fleetUrgency(st2, c.empireIdx); + check('an all-warm galaxy lowers urgency below the plain baseline', + urg.multiplier < 1 && urg.threat === null, JSON.stringify(urg)); + + // Undo contact/attitude entirely so later checks in this block (which + // reuse st2/c, including the still-no-contact Galactic Expansion + // checks further down) aren't reading a met, warmed-up galaxy by + // accident. + other.attitude[emp2.idx] = 0; + emp2.contacted[other.idx] = false; + other.contacted[emp2.idx] = false; + } + // A strong fleet moves the recommendation on. Logic.addFleet(RULES, st2, c.empireIdx, c.starIdx, [{ hullId: 'battleship', mark: 1, count: 10 }]); rec = Logic.recommendColonyFocus(RULES, st2, c); @@ -2004,6 +2061,89 @@ section('6. Colony economy'); dirtyLines.some((l) => l.toLowerCase().includes('waste'))); c.waste = savedWaste; } + + // Galactic Expansion (Colony Focus) / Expansion (Allocation Focus) — + // Brian's ask, 2026-08-14: favored pre-contact while an unclaimed world + // is still on the map, and pickExpansion's actual build behavior once + // selected — a colony ship per open world, then frigate escorts at 2:1 + // once colony-ship demand is covered. + { + const findExpansionTarget = () => { + for (let si = 0; si < st2.galaxy.stars.length; si += 1) { + if (si === c.starIdx) continue; + const star = st2.galaxy.stars[si]; + for (let orbit = 0; orbit < star.planets.length; orbit += 1) { + if (Logic.canColonize(RULES, st2, c.empireIdx, si, orbit)) return si; + } + } + return -1; + }; + const targetStar = findExpansionTarget(); + check('a colonizable target exists somewhere in this galaxy for the expansion test', targetStar >= 0); + + if (targetStar >= 0) { + const before = Logic.discoveredOpenWorlds(RULES, st2, c.empireIdx); + emp2.explored[targetStar] = true; + const after = Logic.discoveredOpenWorlds(RULES, st2, c.empireIdx); + check('discovering an open world raises discoveredOpenWorlds by exactly one', + after === before + 1, `${before} -> ${after}`); + + check('with no contact and an open world, Colony Focus favors Galactic Expansion', + Logic.recommendColonyFocus(RULES, st2, c).value === 'expansion'); + check('with no contact and an open world, Allocation Focus favors Expansion', + Logic.recommendAllocationFocus(RULES, st2, c).key === 'expansion'); + + // First contact takes it off the table immediately, even though the + // world is still open — "quickly takes a back seat," not a fade. + const rival = st2.empires.find((o) => o.idx !== c.empireIdx && o.alive); + emp2.contacted[rival.idx] = true; + rival.contacted[emp2.idx] = true; + check('first contact drops Galactic Expansion even with an open world still on the map', + Logic.recommendColonyFocus(RULES, st2, c).value !== 'expansion'); + check('first contact drops Expansion from Allocation Focus too', + Logic.recommendAllocationFocus(RULES, st2, c).key !== 'expansion'); + emp2.contacted[rival.idx] = false; + rival.contacted[emp2.idx] = false; + + // pickExpansion via autoQueueColonies: a colony ship first... Every + // empire starts with one colony ship already in its fleet + // (createGame's opening scout+colonyship pair), which alone would + // already cover a single discovered target — stripped out here (not + // the whole fleet, which may also carry the starting scout) so the + // commitment count starts at zero and this test actually exercises + // "build one," not "recognize we already have one." + for (const f of st2.fleets) { + if (f.empireIdx !== c.empireIdx) continue; + f.ships = f.ships.filter((s) => s.hullId !== 'colonyship'); + } + st2.fleets = st2.fleets.filter((f) => f.ships.length > 0); + c.focus = 'expansion'; + c.queue = []; + Logic.autoQueueColonies(RULES, st2, c.empireIdx); + check('Galactic Expansion queues a colony ship while an open world is unclaimed', + c.queue.length === 1 && c.queue[0].kind === 'ship' && c.queue[0].id === 'colonyship', + JSON.stringify(c.queue[0])); + + // ...then, once every open world already has a colony ship + // committed, frigate escorts at a 2:1 ratio. + Logic.addFleet(RULES, st2, c.empireIdx, c.starIdx, [{ hullId: 'colonyship', mark: 1, count: 1 }]); + c.queue = []; + Logic.autoQueueColonies(RULES, st2, c.empireIdx); + check('Galactic Expansion switches to frigate escorts once colony-ship demand is met', + c.queue.length === 1 && c.queue[0].kind === 'ship' && c.queue[0].id === 'frigate', + JSON.stringify(c.queue[0])); + + Logic.addFleet(RULES, st2, c.empireIdx, c.starIdx, [{ hullId: 'frigate', mark: 1, count: 2 }]); + c.queue = []; + Logic.autoQueueColonies(RULES, st2, c.empireIdx); + check('Galactic Expansion leaves the queue empty once colony ships and escorts both meet target', + c.queue.length === 0, JSON.stringify(c.queue)); + + // Leave st2/c/emp2 as found for anything appended after this block. + emp2.explored[targetStar] = false; + c.focus = 'trade'; + } + } } colony.sliders = restore.sliders;