From 8b5aa6064a19b61cac1be3f7f2fb15e9d1c2dbf2 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Fri, 17 Jul 2026 11:46:27 -0600 Subject: [PATCH] Updates to research display --- src/games/civilization/CivilizationGame.js | 70 +++++++++++++++++----- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/src/games/civilization/CivilizationGame.js b/src/games/civilization/CivilizationGame.js index 7bc96e1..36c6bc1 100644 --- a/src/games/civilization/CivilizationGame.js +++ b/src/games/civilization/CivilizationGame.js @@ -1236,8 +1236,18 @@ export default class CivilizationGame extends Phaser.Scene { const civ = this.state.civs[human]; if (!civ.alive) { this.onGameOver(); return; } - // Research prompt. - if (!civ.researching && Logic.availableTechs(this.rules, civ).length) { + // Research: announce the completed discovery FIRST, open the picker as + // that popup is dismissed (onDismiss replaces the queue-resume; the tech + // screen's close handler resumes it), and let everything else queue up + // behind. Without a completion event (fresh game, resumed save) the + // picker still opens directly. + const techEvent = this.state.events.find((e) => e.type === 'techDone' && e.civ === human && !e.announced); + const needsPick = !civ.researching && Logic.availableTechs(this.rules, civ).length > 0; + if (techEvent) { + techEvent.announced = true; + this.announceStatus(`Research complete: ${this.rules.techs[techEvent.tech].name}`, + needsPick ? () => this.openTech() : undefined); + } else if (needsPick) { this.openTech(); } this.presentAIProposals(); @@ -1283,20 +1293,26 @@ export default class CivilizationGame extends Phaser.Scene { // "resolve now, animate after" trick as the player's own moves, just // via a before/after snapshot instead of a waypoint path. const combatEvents = this.collectNewCombatEvents(); - // Combats against any city the human has explored — their own or a - // rival's — are always deferred to a dedicated cinematic (camera pan + - // replay + outcome popup) played at the start of the human's next - // turn, rather than silently replayed here (possibly off-screen) like - // ordinary field battles. City tiles stay marked once explored (see - // MapView.refresh()'s `explored[idx]` check), so this reaches a known - // rival city even if it's currently outside the fog-of-war vision set. + // Combats against the HUMAN's cities are deferred to a dedicated + // cinematic (camera pan + replay + outcome popup) at the start of the + // human's next turn — those demand a response. Rival-vs-rival battles + // at cities the human has explored play live right here instead: pan, + // replay, and a status-log line, no blocking popup. City tiles stay + // marked once explored (see MapView.refresh()'s `explored[idx]` + // check), so this reaches a known rival city even if it's currently + // outside the fog-of-war vision set. `e.defenderCiv === human` catches + // a city the human just lost this step (city.civ already shows the + // conqueror by the time we classify). const human = this.state.humanIndex; const elsewhere = []; + const rivalCityCombats = []; for (const e of combatEvents) { const city = Logic.cityAt(this.state, e.x, e.y); const cityExplored = city && this.state.explored[human][Logic.tileIndex(this.state.world, city.x, city.y)]; - if (city && (city.civ === human || cityExplored)) { + if (city && (city.civ === human || e.defenderCiv === human)) { this.pendingCityAttacks.push({ e, cityId: city.id, cityName: city.name, x: city.x, y: city.y }); + } else if (city && cityExplored) { + rivalCityCombats.push({ e, city }); } else { elsewhere.push(e); } @@ -1318,8 +1334,25 @@ export default class CivilizationGame extends Phaser.Scene { this.time.delayedCall(90, stepCiv); }); }; - if (visibleCombat.length) this.view.animateCombat(visibleCombat, afterCombat); - else afterCombat(); + // Rival-vs-rival city battles first (pan + replay + log line, one at a + // time), then ordinary visible field combats, then the move glide. + const playRivalCityCombats = (done) => { + const item = rivalCityCombats.shift(); + if (!item) { done(); return; } + const { e, city } = item; + const attackerName = this.state.civs[e.attackerCiv]?.name ?? 'An enemy'; + const captured = e.attackerWon && city.civ === e.attackerCiv; + const outcome = captured ? `${city.name} has fallen to ${attackerName}!` + : e.attackerWon ? `${city.name}'s defenders were defeated!` + : `${city.name} held its ground!`; + this.logMessage(`${attackerName} attacked ${city.name} — ${outcome}`); + this.view.panToTile(city.x, city.y); + this.view.animateCombat([e], () => playRivalCityCombats(done)); + }; + playRivalCityCombats(() => { + if (visibleCombat.length) this.view.animateCombat(visibleCombat, afterCombat); + else afterCombat(); + }); }; stepCiv(); } @@ -1342,10 +1375,10 @@ export default class CivilizationGame extends Phaser.Scene { } } - // Combats against any city the human has explored (their own or a - // rival's), deferred from runToHumanTurn()'s per-civ stepping (see - // stepCiv there) so they always get a camera pan + replay + outcome popup - // instead of possibly playing silently off-screen. + // Combats against the human's own cities, deferred from runToHumanTurn()'s + // per-civ stepping (see stepCiv there) so they always get a camera pan + + // replay + outcome popup at the start of the human's turn. Rival-vs-rival + // city battles play live during the AI phase instead. announceCityAttacks() { const attacks = this.pendingCityAttacks; this.pendingCityAttacks = []; @@ -1504,6 +1537,11 @@ export default class CivilizationGame extends Phaser.Scene { openTechScreen(this, this.rules, this.state, () => { this.modalOpen = false; this.refreshHud(); + // Resume any statuses that queued while the tech screen was up — in + // particular the start-of-turn flow, where the research-complete + // popup's onDismiss opens this screen with the rest of the turn's + // announcements still waiting in the queue. + this.showNextStatus(); }); }