600 lines
22 KiB
JavaScript
600 lines
22 KiB
JavaScript
// The game screen's HUD: the top and bottom panels, selection and stack menus,
|
||
// hover tooltips, the pillage forecast and the modal wiring. These methods run
|
||
// on the GameScreen instance (composed in ../game_screen.js) and only read the
|
||
// snapshot and forward orders.
|
||
|
||
import { describeNews } from "../modals.js";
|
||
import { compact, groupDigits, improvements as fmtImprovements } from "../../../shared/text_format.js";
|
||
import {
|
||
CIVILISATIONS,
|
||
TRAINABLE_UNIT_IDS,
|
||
BUILDINGS,
|
||
GOVERNMENTS,
|
||
TECHNOLOGIES,
|
||
NEWS_WAR,
|
||
ECONOMY,
|
||
pillageSteps,
|
||
} from "../../../shared/data.js";
|
||
import { defenseFactors, unitDefense, gdpPerCapitaFactors, gdpPerCapitaFromFactors } from "../../../shared/rules.js";
|
||
import { dateString } from "../../../shared/game_clock.js";
|
||
import { DEFAULT_UNIT_SPEED } from "../../../shared/game_state/constants.js";
|
||
|
||
export const panelMethods = {
|
||
_bindHud() {
|
||
$("#city-train").on("click", () => this._openTrainModal());
|
||
$("#city-build").on("click", () => this._openBuildingsModal());
|
||
$("#civ-flag").on("click", () => this._openNationModal());
|
||
$("#news-button").on("click", () => this._openNewsModal());
|
||
|
||
this.trainModal.onTrain = (cityId, protoIndex) =>
|
||
this.sendOrder({ type: "train", city: cityId, proto: protoIndex });
|
||
this.trainModal.onCancel = (cityId, index) =>
|
||
this.sendOrder({ type: "cancel_train", city: cityId, index });
|
||
this.buildingsModal.onBuild = (cityId, index) =>
|
||
this.sendOrder({ type: "build", city: cityId, building: index });
|
||
this.buildingsModal.onDemolish = (cityId, index) =>
|
||
this.sendOrder({ type: "demolish", city: cityId, building: index });
|
||
this.nationModal.onGovernment = (index) =>
|
||
this.sendOrder({ type: "government", government: index });
|
||
this.nationModal.onTechnology = (index) =>
|
||
this.sendOrder({ type: "technology", technology: index });
|
||
this.nationModal.onDeclareWar = (target) =>
|
||
this.sendOrder({ type: "declare_war", target });
|
||
|
||
$("#unit-attack")
|
||
.on("mouseenter", (event) => this._showStatTooltip($(event.currentTarget)))
|
||
.on("mouseleave", () => this._hideStatTooltip());
|
||
$("#unit-defense")
|
||
.on("mouseenter", (event) => this._showStatTooltip($(event.currentTarget)))
|
||
.on("mouseleave", () => this._hideStatTooltip());
|
||
$("#unit-pillage")
|
||
.on("click", () => this._pillageSelected())
|
||
.on("mouseenter", (event) => this._showStatTooltip($(event.currentTarget)))
|
||
.on("mouseleave", () => this._hideStatTooltip());
|
||
|
||
// A click anywhere outside the stack menu dismisses it.
|
||
$(document).on("mousedown.stackmenu", (event) => {
|
||
if (!this.$stackMenu.hasClass("hidden") && !$(event.target).closest("#stack-menu").length) {
|
||
this._hideStackMenu();
|
||
}
|
||
});
|
||
|
||
$(window).on("keydown.game", (event) => this._onKey(event));
|
||
},
|
||
|
||
// A small hover tooltip anchored above a stat, fed by its data-tooltip text.
|
||
_showStatTooltip($stat) {
|
||
const text = $stat.attr("data-tooltip");
|
||
if (!text) return;
|
||
this._hoveredStat = $stat;
|
||
this.$statTooltip.text(text).removeClass("hidden");
|
||
const offset = $stat.offset() || { left: 0, top: 0 };
|
||
const width = this.$statTooltip.outerWidth() || 0;
|
||
const height = this.$statTooltip.outerHeight() || 0;
|
||
this.$statTooltip.css({
|
||
left: `${Math.max(8, offset.left + $stat.outerWidth() / 2 - width / 2)}px`,
|
||
top: `${Math.max(8, offset.top - height - 8)}px`,
|
||
});
|
||
},
|
||
|
||
_hideStatTooltip() {
|
||
this._hoveredStat = null;
|
||
this.$statTooltip.addClass("hidden");
|
||
},
|
||
|
||
// ------------------------------------------------------- selection ------
|
||
|
||
// Selects the given unit ids (already filtered to our own), refreshing the
|
||
// map highlight and the bottom panel. A single id is a normal selection; an
|
||
// empty list clears; several ids form a stack order.
|
||
_selectUnits(ids) {
|
||
const units = [];
|
||
for (const id of ids) {
|
||
const unit = this._unit(id);
|
||
if (unit && unit.civ === this.localCiv) units.push(unit);
|
||
}
|
||
this.selectedUnitIds = units.map((unit) => unit.id);
|
||
this.selectedCityId = 0;
|
||
this.map.setSelectedUnits(this.selectedUnitIds);
|
||
this._hideStackMenu();
|
||
this._showUnits(units);
|
||
},
|
||
|
||
_selectedUnits() {
|
||
return this.selectedUnitIds.map((id) => this._unit(id)).filter(Boolean);
|
||
},
|
||
|
||
_openStackMenu(coords, ids, clientX, clientY) {
|
||
const units = [];
|
||
for (const id of ids) {
|
||
const unit = this._unit(id);
|
||
if (unit && unit.civ === this.localCiv) units.push(unit);
|
||
}
|
||
if (units.length === 0) return;
|
||
const $menu = this.$stackMenu.empty();
|
||
const selection = this._selectedUnits();
|
||
if (selection.length > 0) {
|
||
const $move = $("<button class='stack-item stack-move'></button>").text("Move here");
|
||
$move.on("click", () => {
|
||
this._hideStackMenu();
|
||
this._orderMove(selection, coords);
|
||
});
|
||
$menu.append($move);
|
||
}
|
||
for (const unit of units) {
|
||
const proto = this.protoUnits[unit.proto];
|
||
const $item = $("<button class='stack-item'></button>")
|
||
.text(`${proto ? proto.name : "Unit"} — ${Math.round(unit.hp)}/${unit.maxHp} HP`);
|
||
$item.on("click", () => this._selectUnits([unit.id]));
|
||
$menu.append($item);
|
||
}
|
||
const $all = $("<button class='stack-item stack-all'></button>").text(
|
||
`Select all (${units.length})`
|
||
);
|
||
$all.on("click", () => this._selectUnits(units.map((unit) => unit.id)));
|
||
$menu.append($all);
|
||
$menu.removeClass("hidden").css({ left: `${clientX}px`, top: `${clientY}px` });
|
||
},
|
||
|
||
_hideStackMenu() {
|
||
if (this.$stackMenu && this.$stackMenu.length) this.$stackMenu.addClass("hidden");
|
||
},
|
||
|
||
// The slowest member of a group sets the pace. The fallback matches the
|
||
// server's DEFAULT_UNIT_SPEED so prediction cannot disagree with the
|
||
// authoritative movement cost.
|
||
_groupSpeed(units) {
|
||
let speed = Infinity;
|
||
for (const unit of units) {
|
||
const proto = this.protoUnits[unit.proto];
|
||
const value = proto && proto.speed ? proto.speed : DEFAULT_UNIT_SPEED;
|
||
speed = Math.min(speed, value);
|
||
}
|
||
return Number.isFinite(speed) ? speed : null;
|
||
},
|
||
|
||
async _groupMovePlan(units, coords) {
|
||
const speed = units.length > 1 ? this._groupSpeed(units) : null;
|
||
const targets = [];
|
||
const routes = new Map();
|
||
// Same-type units in one stack share a route, so search once per type.
|
||
const cache = new Map();
|
||
for (const unit of units) {
|
||
const route = await this.map.planMove(unit, coords, speed, cache);
|
||
if (!route || route.length < 2) continue;
|
||
routes.set(unit.id, route);
|
||
if (!this._isMilitaryUnit(unit)) continue;
|
||
for (const point of route) {
|
||
const owner = this.map.territory.get(`${point.x},${point.y}`);
|
||
if (owner === undefined || owner < 0 || owner === unit.civ) continue;
|
||
if (this._isAtWar(unit.civ, owner)) continue;
|
||
if (!targets.includes(owner)) targets.push(owner);
|
||
}
|
||
}
|
||
return { targets, speed, routes };
|
||
},
|
||
|
||
// ------------------------------------------------------- pillage -------
|
||
|
||
// What the selected units would remove, gain and heal by pillaging the tile
|
||
// they stand on, or null when they cannot pillage.
|
||
_pillageForecast(units) {
|
||
if (units.length === 0) return null;
|
||
const coords = units[0].coords;
|
||
const sameTile = units.every((unit) => unit.coords[0] === coords[0] && unit.coords[1] === coords[1]);
|
||
if (!sameTile) return null;
|
||
const owner = this._territoryOwner.has(`${coords[0]},${coords[1]}`)
|
||
? this._territoryOwner.get(`${coords[0]},${coords[1]}`)
|
||
: -1;
|
||
if (owner < 0 || owner === this.localCiv) return null;
|
||
// A unit in combat cannot pillage.
|
||
if (this._battles.has(`${coords[0]},${coords[1]}`)) return null;
|
||
const population = this._population.get(`${coords[0]},${coords[1]}`) || 0;
|
||
const { steps, gdpMultiplier } = pillageSteps(population, units.length);
|
||
const ordered = units.slice().sort((a, b) => a.hp / a.maxHp - b.hp / b.maxHp);
|
||
const healed = ordered.map((unit, index) =>
|
||
Math.min(unit.maxHp, unit.hp + steps[index].heal) - unit.hp
|
||
);
|
||
return { population, steps, gdpMultiplier, healed };
|
||
},
|
||
|
||
_updatePillageButton(units) {
|
||
const $button = $("#unit-pillage");
|
||
const forecast = this._pillageForecast(units);
|
||
if (!forecast) {
|
||
$button.addClass("hidden").removeAttr("data-tooltip");
|
||
return;
|
||
}
|
||
const removed = forecast.population - forecast.steps[forecast.steps.length - 1].remaining;
|
||
const share = forecast.population > 0 ? (removed / forecast.population) * 100 : 0;
|
||
const heals = forecast.healed.map((hp) => `+${Math.round(hp)}`).join(", ");
|
||
$button
|
||
.removeClass("hidden")
|
||
.attr(
|
||
"data-tooltip",
|
||
`Removes ${groupDigits(Math.round(removed))} people (${share.toFixed(0)}%) · ` +
|
||
`GDP/capita ×${forecast.gdpMultiplier.toFixed(2)} · heals ${heals} HP`
|
||
);
|
||
},
|
||
|
||
_isMilitaryUnit(unit) {
|
||
const proto = this.protoUnits[unit.proto];
|
||
return !!(proto && proto.military);
|
||
},
|
||
|
||
_isAtWar(a, b) {
|
||
if (a < 0 || b < 0 || a === b) return false;
|
||
return this._wars.has(a < b ? `${a}:${b}` : `${b}:${a}`);
|
||
},
|
||
|
||
_civName(index) {
|
||
const civ = this.civilisations[index];
|
||
return civ ? civ.name : `Nation ${index + 1}`;
|
||
},
|
||
|
||
_civDemonym(index) {
|
||
const civ = this.civilisations[index];
|
||
return civ && civ.demonym ? civ.demonym : this._civName(index);
|
||
},
|
||
|
||
// ------------------------------------------------------- top panel -----
|
||
|
||
_updateTopPanel(state) {
|
||
const civ = this.civilisations[this.localCiv];
|
||
if (civ) {
|
||
$("#civ-name").text(civ.name);
|
||
const flag = civ.flag || (CIVILISATIONS.find((c) => c.id === civ.id) || {}).flag;
|
||
if (flag) $("#civ-flag-img").attr("src", `assets/flags/${flag}`);
|
||
}
|
||
$("#player-name").text(this.config.playerName || (this.network.players[this.network.localPlayerId] || {}).name || "");
|
||
$("#admin-label").toggle(this.network.isAdmin);
|
||
$("#stat-date").text(dateString(state.totalHours));
|
||
const stats = state.viewerStats || {};
|
||
$("#stat-pop").text(compact(stats.population || 0));
|
||
$("#stat-gdp").text(`€${compact(stats.gdp || 0)}`);
|
||
$("#stat-gdppc").text(`€${compact(stats.gdpPerCapita || 0)}`);
|
||
$("#stat-budget").text(`€${compact(stats.budget || 0)}`);
|
||
},
|
||
|
||
_updateBottomPanel() {
|
||
if (this.selectedUnitIds.length > 0) {
|
||
this._showUnits(this._selectedUnits());
|
||
} else if (this.selectedCityId) {
|
||
const city = this._city(this.selectedCityId);
|
||
if (city) this._showCity(city);
|
||
}
|
||
},
|
||
|
||
// Shows the bottom panel for one or many selected units. A stack shows its
|
||
// size and combined health, plus the pillage action when it can use it.
|
||
_showUnits(units) {
|
||
if (units.length === 0) {
|
||
$("#unit-info").addClass("hidden");
|
||
this._hideStatTooltip();
|
||
this._refreshBottomVisibility();
|
||
return;
|
||
}
|
||
if (units.length === 1) {
|
||
this._showUnit(units[0]);
|
||
return;
|
||
}
|
||
const hp = units.reduce((sum, unit) => sum + unit.hp, 0);
|
||
const maxHp = units.reduce((sum, unit) => sum + unit.maxHp, 0);
|
||
$("#unit-name").text(`${units.length} units selected`);
|
||
$("#unit-attack, #unit-defense").addClass("hidden");
|
||
$("#unit-health-fill").css("width", `${maxHp > 0 ? Math.min(100, (hp / maxHp) * 100) : 0}%`);
|
||
$("#unit-health-label").text(`${Math.round(hp)} / ${Math.round(maxHp)} HP`);
|
||
this._updatePillageButton(units);
|
||
$("#tile-info, #city-info").addClass("hidden");
|
||
$("#unit-info").removeClass("hidden");
|
||
this._refreshBottomVisibility();
|
||
},
|
||
|
||
_showUnit(unit) {
|
||
if (!unit) {
|
||
$("#unit-info").addClass("hidden");
|
||
this._hideStatTooltip();
|
||
this._refreshBottomVisibility();
|
||
return;
|
||
}
|
||
const proto = this.protoUnits[unit.proto];
|
||
$("#unit-name").text(proto ? proto.name : "Unit");
|
||
$("#unit-attack, #unit-defense").removeClass("hidden");
|
||
this._showUnitStats(unit, proto);
|
||
const ratio = unit.maxHp > 0 ? (unit.hp / unit.maxHp) * 100 : 0;
|
||
$("#unit-health-fill").css("width", `${Math.max(0, Math.min(100, ratio))}%`);
|
||
$("#unit-health-label").text(`${Math.round(unit.hp)} / ${Math.round(unit.maxHp)} HP`);
|
||
this._updatePillageButton([unit]);
|
||
$("#tile-info, #city-info").addClass("hidden");
|
||
$("#unit-info").removeClass("hidden");
|
||
this._refreshBottomVisibility();
|
||
},
|
||
|
||
// Attack and defense of the unit where it stands, with the hovering tooltips
|
||
// listing each factor that shaped the number.
|
||
_showUnitStats(unit, proto) {
|
||
const coords = { x: unit.coords[0], y: unit.coords[1] };
|
||
const tile = this.map.getTile(coords);
|
||
const hasCity = ((this.snapshot && this.snapshot.cities) || []).some(
|
||
(city) => city.coords[0] === coords.x && city.coords[1] === coords.y
|
||
);
|
||
const baseAttack = proto && typeof proto.attack === "number" ? proto.attack : 0;
|
||
const baseDefense = proto && typeof proto.defense === "number" ? proto.defense : 0;
|
||
$("#unit-attack")
|
||
.text(`Attack ${Math.round(baseAttack)}`)
|
||
.attr("data-tooltip", `Base ${Math.round(baseAttack)}`);
|
||
|
||
const factors = defenseFactors(tile, hasCity);
|
||
const defense = unitDefense(baseDefense, tile, hasCity);
|
||
const parts = [`Base ${Math.round(baseDefense)}`];
|
||
for (const factor of factors) {
|
||
parts.push(`${factor.label} +${Math.round(baseDefense * factor.bonus)}`);
|
||
}
|
||
$("#unit-defense")
|
||
.text(`Defense ${Math.round(defense)}`)
|
||
.attr("data-tooltip", parts.join(", "));
|
||
// Keep an open tooltip in step with the unit it describes across snapshots.
|
||
if (this._hoveredStat && !this.$statTooltip.hasClass("hidden")) {
|
||
const text = this._hoveredStat.attr("data-tooltip");
|
||
if (text) this.$statTooltip.text(text);
|
||
} else {
|
||
this._hideStatTooltip();
|
||
}
|
||
},
|
||
|
||
_showCity(city) {
|
||
if (!city) {
|
||
$("#city-info").addClass("hidden");
|
||
this._refreshBottomVisibility();
|
||
return;
|
||
}
|
||
$("#city-name").text(city.name);
|
||
$("#city-population").text(`Population: ${groupDigits(city.population)}`);
|
||
$("#city-improvements").text(`Improvements: ${fmtImprovements(city.improvements)}`);
|
||
$("#tile-info, #unit-info").addClass("hidden");
|
||
$("#city-info").removeClass("hidden");
|
||
this._refreshBottomVisibility();
|
||
},
|
||
|
||
_showTile(coords) {
|
||
const k = `${coords.x},${coords.y}`;
|
||
const owner = this._territoryOwner.has(k) ? this._territoryOwner.get(k) : -1;
|
||
const civ = this.civilisations[owner];
|
||
const tile = this.map.getTile(coords);
|
||
const city = (this.snapshot.cities || []).find(
|
||
(c) => c.coords[0] === coords.x && c.coords[1] === coords.y
|
||
);
|
||
$("#tile-coords").text(`Tile (${coords.x}, ${coords.y})`);
|
||
$("#tile-owner").text(`Owner: ${civ ? civ.name : "Unclaimed"}`);
|
||
$("#tile-terrain").text(`Terrain: ${tile ? tile.terrainType : "Unknown"}`);
|
||
$("#tile-population").text(`Population: ${groupDigits(this._population.get(k) || 0)}`);
|
||
this._renderTileGdp(owner >= 0 ? this._tileGdpBreakdown(coords, owner, city, tile) : null);
|
||
$("#tile-improvements").text(`Improvements: ${fmtImprovements(city ? city.improvements : [])}`);
|
||
$("#city-info, #unit-info").addClass("hidden");
|
||
$("#tile-info").removeClass("hidden");
|
||
this._refreshBottomVisibility();
|
||
},
|
||
|
||
// The base GDP per capita of a claimable tile plus every factor that shapes
|
||
// it, mirroring the server's getTileGdpPerCapita through the shared helper.
|
||
_tileGdpBreakdown(coords, owner, city, tile) {
|
||
const k = `${coords.x},${coords.y}`;
|
||
const factors = gdpPerCapitaFactors(tile, {
|
||
cityKind: city ? (city.isCapital ? "capital" : "city") : null,
|
||
nextToCity: !city && this._isNextToCity(coords, owner),
|
||
pillagePenalty: this._tileGdpPenalties.has(k) ? this._tileGdpPenalties.get(k) : 1,
|
||
battleDeficit: this._tileBattleGdpDeficits.get(k) || 0,
|
||
});
|
||
return {
|
||
base: ECONOMY.baseGdpPerCapita,
|
||
factors,
|
||
total: gdpPerCapitaFromFactors(ECONOMY.baseGdpPerCapita, factors),
|
||
};
|
||
},
|
||
|
||
_isNextToCity(coords, owner) {
|
||
for (const neighbour of this.map.topology.neighbours(coords.x, coords.y)) {
|
||
const found = (this.snapshot.cities || []).some(
|
||
(city) =>
|
||
city.civ === owner &&
|
||
city.coords[0] === neighbour.x &&
|
||
city.coords[1] === neighbour.y
|
||
);
|
||
if (found) return true;
|
||
}
|
||
return false;
|
||
},
|
||
|
||
// Lists the tile's GDP per capita and each factor behind it, so a click
|
||
// explains exactly why the figure is what it is.
|
||
_renderTileGdp(breakdown) {
|
||
const $container = $("#tile-gdp").empty();
|
||
if (!breakdown) {
|
||
$container.text("GDP/capita: —");
|
||
return;
|
||
}
|
||
$container.append(
|
||
$("<div></div>").text(`GDP/capita: €${groupDigits(Math.round(breakdown.total))}`)
|
||
);
|
||
$container.append(
|
||
$("<div class='tile-gdp-mod'></div>").text(`Base €${groupDigits(breakdown.base)}`)
|
||
);
|
||
for (const factor of breakdown.factors) {
|
||
// A modifier that changes nothing (×1 or ±€0) is left out.
|
||
if (factor.amount !== undefined) {
|
||
if (factor.amount === 0) continue;
|
||
} else if (factor.multiplier === 1) {
|
||
continue;
|
||
}
|
||
const value =
|
||
factor.amount !== undefined
|
||
? `−€${groupDigits(Math.round(Math.abs(factor.amount)))}`
|
||
: `×${factor.multiplier.toFixed(2)}`;
|
||
$container.append(
|
||
$("<div class='tile-gdp-mod'></div>").text(`${factor.label} ${value}`)
|
||
);
|
||
}
|
||
},
|
||
|
||
_refreshBottomVisibility() {
|
||
const visible =
|
||
!$("#tile-info").hasClass("hidden") ||
|
||
!$("#city-info").hasClass("hidden") ||
|
||
!$("#unit-info").hasClass("hidden");
|
||
$("#bottom-panel").toggleClass("hidden", !visible);
|
||
},
|
||
|
||
_unit(id) {
|
||
return (this.snapshot.units || []).find((u) => u.id === id) || null;
|
||
},
|
||
|
||
_city(id) {
|
||
return (this.snapshot.cities || []).find((c) => c.id === id) || null;
|
||
},
|
||
|
||
// -------------------------------------------------------------- modals ---
|
||
|
||
_openTrainModal() {
|
||
const city = this._city(this.selectedCityId);
|
||
if (!city) return;
|
||
const trainable = TRAINABLE_UNIT_IDS.map((id) => {
|
||
const index = this.protoUnits.findIndex((p) => p.id === id);
|
||
return index >= 0 ? { ...this.protoUnits[index], serverIndex: index } : null;
|
||
}).filter(Boolean);
|
||
this.buildingsModal.close();
|
||
this.trainModal.show(
|
||
{ id: city.id, name: city.name },
|
||
(this.snapshot.viewerStats || {}).gdp || 0,
|
||
trainable,
|
||
this._trainingQueue(city.id)
|
||
);
|
||
this._updateTrainModal();
|
||
},
|
||
|
||
_trainingQueue(cityId) {
|
||
return (this.snapshot.training || {})[cityId] || [];
|
||
},
|
||
|
||
_updateTrainModal() {
|
||
if (!this.snapshot || !this.selectedCityId) return;
|
||
if (!this.trainModal.isShowing(this.selectedCityId)) {
|
||
this._trainingCityId = 0;
|
||
return;
|
||
}
|
||
const queue = this._trainingQueue(this.selectedCityId);
|
||
this.trainModal.renderQueue(queue);
|
||
const entry = queue[0];
|
||
if (!entry) {
|
||
if (this._trainingCityId === this.selectedCityId) {
|
||
this.trainModal.endTraining();
|
||
this._trainingCityId = 0;
|
||
}
|
||
return;
|
||
}
|
||
const proto = this.protoUnits[entry.protoIndex];
|
||
const name = proto ? proto.name : "unit";
|
||
if (this._trainingCityId !== this.selectedCityId) {
|
||
this._trainingCityId = this.selectedCityId;
|
||
this.trainModal.beginTraining(name, entry.totalHours, entry.elapsedHours);
|
||
} else {
|
||
this.trainModal.setTrainingProgress(name, entry.elapsedHours, entry.totalHours);
|
||
}
|
||
},
|
||
|
||
_openBuildingsModal() {
|
||
const city = this._city(this.selectedCityId);
|
||
if (!city) return;
|
||
this.trainModal.close();
|
||
this.buildingsModal.show(
|
||
{ id: city.id, name: city.name },
|
||
(this.snapshot.viewerStats || {}).budget || 0,
|
||
BUILDINGS,
|
||
this._cityBuildings.get(city.id) || {}
|
||
);
|
||
},
|
||
|
||
_updateBuildingsModal() {
|
||
if (!this.snapshot || !this.buildingsModal.isShowing(this.selectedCityId)) return;
|
||
const stats = this.snapshot.viewerStats || {};
|
||
this.buildingsModal.refresh(
|
||
stats.budget || 0,
|
||
this._cityBuildings.get(this.selectedCityId) || {}
|
||
);
|
||
},
|
||
|
||
_openNationModal() {
|
||
const stats = this.snapshot.viewerStats || {};
|
||
this.nationModal.show(
|
||
GOVERNMENTS,
|
||
stats.government || 0,
|
||
TECHNOLOGIES,
|
||
this._researched,
|
||
stats.gdp || 0,
|
||
stats.budget || 0,
|
||
stats.research || 0,
|
||
this._finances(stats),
|
||
this._diplomacyInfo(),
|
||
this._conflicts
|
||
);
|
||
},
|
||
|
||
_updateNationModal() {
|
||
if (!this.snapshot || !this.nationModal.isShowing()) return;
|
||
const stats = this.snapshot.viewerStats || {};
|
||
this.nationModal.refresh(
|
||
stats.government || 0,
|
||
this._researched,
|
||
stats.gdp || 0,
|
||
stats.budget || 0,
|
||
stats.research || 0,
|
||
this._finances(stats),
|
||
this._diplomacyInfo(),
|
||
this._conflicts
|
||
);
|
||
},
|
||
|
||
_diplomacyInfo() {
|
||
return {
|
||
civs: this.civilisations,
|
||
localCiv: this.localCiv,
|
||
relations: (this.snapshot && this.snapshot.diplomacy) || [],
|
||
stats: (this.snapshot && this.snapshot.civStats) || [],
|
||
};
|
||
},
|
||
|
||
_openNewsModal() {
|
||
this.newsModal.show(this._news, this.civilisations);
|
||
},
|
||
|
||
_updateNewsModal() {
|
||
if (!this.snapshot || !this.newsModal.isShowing()) return;
|
||
this.newsModal.refresh(this._news, this.civilisations);
|
||
},
|
||
|
||
// Pops newly arrived news into the corner feed. The first snapshot only
|
||
// records what is already known, so history never floods the screen.
|
||
_updateFeed(news) {
|
||
if (this._seenNews === null) {
|
||
this._seenNews = new Set(news.map((entry) => entry.id));
|
||
return;
|
||
}
|
||
for (const entry of news) {
|
||
if (this._seenNews.has(entry.id)) continue;
|
||
this._seenNews.add(entry.id);
|
||
this._pushNews(entry);
|
||
}
|
||
},
|
||
|
||
_pushNews(entry) {
|
||
if (entry.type === NEWS_WAR && entry.target === this.localCiv) {
|
||
this.feed.push(`${this._civName(entry.actor)} has declared war on you!`, { alert: true });
|
||
return;
|
||
}
|
||
this.feed.push(describeNews(entry, this.civilisations));
|
||
},
|
||
|
||
_finances(stats) {
|
||
return stats.breakdown || null;
|
||
},
|
||
};
|