fix Vega game: reset fleet selection on show, offset fleet animations, add building queue UI

- Reset fleet selection when showing a new fleet so it starts at full strength
- Offset moving fleet sprites by (-26, -22) to prevent blocking star click targets
- Track newly unlocked building IDs in research completion events
- Add one-click bulk-queue buttons in turn report for newly researched buildings
  - "All Colonies" queues at every colony
  - "Colonies with positive BC" filters by net income (trade minus upkeep)
This commit is contained in:
Brian Fertig 2026-08-06 16:50:28 -06:00
parent 2f99d5ce68
commit a7c3ef20e4
4 changed files with 71 additions and 10 deletions

View File

@ -135,6 +135,9 @@ export default class VegaSidePanel {
showFleet(fleet) { showFleet(fleet) {
this.mode = 'fleet'; this.mode = 'fleet';
this.fleet = fleet; this.fleet = fleet;
// A fresh selection always starts at full strength: any dial-down from a
// prior selection (this fleet or another) must not carry over here.
this.sel = [];
this.syncSelection(); this.syncSelection();
this.rebuild(); this.rebuild();
this.open(); this.open();
@ -507,9 +510,12 @@ export default class VegaSidePanel {
/** /**
* Rebuild the per-stack selection, keeping whatever the player had already * Rebuild the per-stack selection, keeping whatever the player had already
* dialled in where the stack still exists. Immobile hulls (star bases) are * dialled in where the stack still exists. This carry-over only matters for
* excluded outright rather than shown at zero: they defend the system they * refresh() (same selection, engine state changed under us); showFleet()
* were built in and can never be part of a move order. * clears this.sel first so a fresh selection always starts at full
* strength. Immobile hulls (star bases) are excluded outright rather than
* shown at zero: they defend the system they were built in and can never
* be part of a move order.
*/ */
syncSelection() { syncSelection() {
const fleet = this.fleet; const fleet = this.fleet;

View File

@ -335,8 +335,10 @@ export default class VegaStarMap {
if (!a || !b) continue; if (!a || !b) continue;
if (viewer && !own && !viewer.explored[fleet.toStar]) continue; if (viewer && !own && !viewer.explored[fleet.toStar]) continue;
const t = fleet.total > 0 ? Phaser.Math.Clamp(fleet.progress / fleet.total, 0, 1) : 0; const t = fleet.total > 0 ? Phaser.Math.Clamp(fleet.progress / fleet.total, 0, 1) : 0;
x = a.x + (b.x - a.x) * t; // Offset up-left of the lane so a fleet just leaving its origin star
y = a.y + (b.y - a.y) * t; // doesn't sit directly on top of it and block the star's own click target.
x = a.x + (b.x - a.x) * t - 26;
y = a.y + (b.y - a.y) * t - 22;
} }
const colour = Phaser.Display.Color.HexStringToColor(emp.color).color; const colour = Phaser.Display.Color.HexStringToColor(emp.color).color;
@ -501,8 +503,8 @@ export default class VegaStarMap {
const from = this.state.galaxy.stars[f.fromStar]; const from = this.state.galaxy.stars[f.fromStar];
if (!from) return null; if (!from) return null;
const t = f.total > 0 ? Phaser.Math.Clamp(f.progress / f.total, 0, 1) : 0; const t = f.total > 0 ? Phaser.Math.Clamp(f.progress / f.total, 0, 1) : 0;
ax = from.x + (dest.x - from.x) * t; ax = from.x + (dest.x - from.x) * t - 26;
ay = from.y + (dest.y - from.y) * t; ay = from.y + (dest.y - from.y) * t - 22;
} }
return { return {
ax, ay, bx: dest.x, by: dest.y, ax, ay, bx: dest.x, by: dest.y,

View File

@ -103,16 +103,20 @@ function describeTechDone(rules, state, ev) {
if (effects.length) { if (effects.length) {
lines.push(line(`Effects: ${effects.map(([k, v]) => `${k} ${v > 0 ? '+' : ''}${v}`).join(', ')}`, '#7fd8a0')); 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 newBuildingIds = gate.buildings.filter((id) => rules.buildings[id]);
const newTechs = gate.prereqOf const newTechs = gate.prereqOf
.filter((id) => emp.available[id] && !emp.known[id]) .filter((id) => emp.available[id] && !emp.known[id])
.map((id) => rules.techs[id]?.name) .map((id) => rules.techs[id]?.name)
.filter(Boolean); .filter(Boolean);
const unlocks = [...newBuildings, ...newTechs]; const unlocks = [...newBuildingIds.map((id) => rules.buildings[id].name), ...newTechs];
if (unlocks.length) { if (unlocks.length) {
lines.push(line(`Now available: ${unlocks.join(', ')}`, '#ffd88a')); lines.push(line(`Now available: ${unlocks.join(', ')}`, '#ffd88a'));
} }
return { headline: `Research completed: ${tech.name}.`, lines }; return {
headline: `Research completed: ${tech.name}.`,
lines,
buildingUnlocks: newBuildingIds.map((id) => ({ id, name: rules.buildings[id].name })),
};
} }
function describeBuildingDone(rules, state, ev) { function describeBuildingDone(rules, state, ev) {

View File

@ -17,6 +17,16 @@ import { buildingFrame } from './VegaArt.js';
import { turnToYear } from './VegaRules.js'; import { turnToYear } from './VegaRules.js';
import { Button } from '../../ui/Button.js'; import { Button } from '../../ui/Button.js';
import { describeEvent, categoryWeight } from './VegaTurnReport.js'; import { describeEvent, categoryWeight } from './VegaTurnReport.js';
import { enqueue, empireColonies, colonyTrade } from './VegaLogic.js';
// A colony's own trade minus what its existing buildings already cost to run
// — there's no per-colony BC in this economy (only the empire-wide pool), so
// this is the closest local stand-in for "can this colony carry one more
// building's upkeep on its own income."
function colonyNetIncome(rules, state, colony) {
const upkeep = colony.buildings.reduce((s, bid) => s + (rules.buildings[bid]?.upkeep ?? 0), 0);
return colonyTrade(rules, state, colony) - upkeep;
}
const ACCENT = 0x6fc4ff; const ACCENT = 0x6fc4ff;
const ROW_BG = 0x142238; const ROW_BG = 0x142238;
@ -96,6 +106,45 @@ export function openTurnReportScreen(scene, rules, state, events, onClose) {
} }
cy += 4; cy += 4;
// A newly-researched tech that unlocks a colony building gets a
// one-click bulk-queue offer right in the report — the whole reason
// to interrupt the player for research news instead of leaving it in
// the ticker log.
if (ev.type === 'techDone') {
for (const bu of (desc.buildingUnlocks ?? [])) {
const label = scene.add.text(16, cy, `Build ${bu.name}:`, {
fontFamily: FONT, fontSize: '15px', color: '#ffd88a',
});
col.content.add(label);
cy += label.height + 8;
const status = scene.add.text(16, cy + 34, '', {
fontFamily: FONT, fontSize: '13px', color: '#7fd8a0', wordWrap: { width: w - 32 },
});
const queueAt = (colonies) => {
let n = 0;
for (const c of colonies) if (enqueue(rules, state, c, 'building', bu.id)) n += 1;
status.setText(n > 0
? `Queued at ${n} colon${n === 1 ? 'y' : 'ies'}.`
: 'Already built or queued at every colony.');
};
const allBtn = new Button(scene, 16 + 84, cy + 14, 'All Colonies', () => {
queueAt(empireColonies(state, ev.empire));
}, { width: 168, height: 28, fontSize: 13 });
col.content.add(allBtn);
const bcBtn = new Button(scene, 16 + 168 + 16 + 105, cy + 14, 'Colonies with positive BC', () => {
queueAt(empireColonies(state, ev.empire).filter((c) => colonyNetIncome(rules, state, c) > 0));
}, { width: 210, height: 28, fontSize: 13 });
col.content.add(bcBtn);
col.content.add(status);
cy += 28 + 14 + status.height + 10;
}
}
// The built item's picture, right-aligned against the synopsis text // The built item's picture, right-aligned against the synopsis text
// it sits beside. Reserving mediaW off the wrap width above (rather // it sits beside. Reserving mediaW off the wrap width above (rather
// than overlaying it) keeps long descriptions from running under it. // than overlaying it) keeps long descriptions from running under it.