// The three modal panels of the game HUD: city training, city buildings, and // the nation panel (government + technology). They only display state supplied // by the game screen and forward the player's choices as orders. import { groupDigits, hours } from "../../shared/text_format.js"; import { ECONOMY } from "../../shared/data.js"; import { buildingBuildCost, buildingRefund, buildingUpkeep, governmentChangeCost, prerequisitesMet, } from "../../shared/rules.js"; function formatEffect(effect, level) { const value = level <= 0 ? 0 : effect.baseValue * Math.pow(level, effect.exponent); switch (effect.stat) { case "gdp": return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% GDP`; case "budget_income": return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% budget income`; case "research": return `${value >= 0 ? "+" : ""}${value.toFixed(2)} research/h`; case "culture": return `${value >= 0 ? "+" : ""}${value.toFixed(2)} culture/h`; default: return `${effect.stat} ${value.toFixed(2)}`; } } function effectsText(effects, level) { const parts = effects.map((e) => formatEffect(e, level)); return parts.length > 0 ? parts.join(" · ") : "No effect"; } export class TrainModal { constructor() { this.$modal = $("#modal-train"); this.$title = $("#train-title"); this.$list = $("#train-list"); this.$status = $("#train-status"); this.$label = $("#train-label"); this.$progress = $("#train-progress"); this.cityId = 0; this.gdp = 0; this.onTrain = () => {}; this.$modal.find(".modal-close").on("click", () => this.close()); } isShowing(cityId) { return !this.$modal.hasClass("hidden") && this.cityId === cityId; } show(city, gdp, trainableUnits) { this.cityId = city.id; this.gdp = gdp; this.$title.text(`${city.name} — Train`); this.$list.empty(); this.$status.addClass("hidden"); for (let index = 0; index < trainableUnits.length; index++) { const proto = trainableUnits[index]; if (!proto.cost && proto.cost !== 0) continue; const button = $(""); const duration = ECONOMY.trainingHours(gdp, proto.cost); button.text(`${proto.name} — Cost ${groupDigits(proto.cost)} — ${hours(duration)}`); const serverIndex = proto.serverIndex !== undefined ? proto.serverIndex : index; button.on("click", () => this.onTrain(city.id, serverIndex)); this.$list.append(button); } this.$modal.removeClass("hidden"); } beginTraining(name, totalHours, elapsedHours = 0) { this.$label.text(`Training ${name}…`); this.$progress.css("width", `${Math.min(100, (elapsedHours / Math.max(totalHours, 0.0001)) * 100)}%`); this.$status.removeClass("hidden"); this.$list.find("button").prop("disabled", true); } setTrainingProgress(name, elapsedHours, totalHours) { const remaining = Math.max(0, Math.ceil(totalHours - elapsedHours)); this.$label.text(`Training ${name}… (${remaining} h left)`); this.$progress.css("width", `${Math.min(100, (elapsedHours / Math.max(totalHours, 0.0001)) * 100)}%`); } endTraining() { this.$label.text("Training complete"); this.$progress.css("width", "100%"); this.$list.find("button").prop("disabled", false); } close() { this.$modal.addClass("hidden"); this.cityId = 0; } } export class BuildingsModal { constructor() { this.$modal = $("#modal-buildings"); this.$title = $("#buildings-title"); this.$budget = $("#buildings-budget"); this.$list = $("#buildings-list"); this.cityId = 0; this.cityName = ""; this.buildings = []; this.rows = []; this.onBuild = () => {}; this.onDemolish = () => {}; this.$modal.find(".modal-close").on("click", () => this.close()); } isShowing(cityId) { return !this.$modal.hasClass("hidden") && this.cityId === cityId; } show(city, gdp, budget, buildings, levels) { this.cityId = city.id; this.cityName = city.name; this.buildings = buildings; this.$title.text(`${city.name} — Buildings`); this.$list.empty(); this.rows = buildings.map((proto, index) => this._makeRow(proto, index)); this.refresh(gdp, budget, levels); this.$modal.removeClass("hidden"); } _makeRow(proto, index) { const $card = $("
"); const $info = $("
"); const $name = $("
"); const $desc = $("
").text(proto.description); const $effects = $("
"); const $cost = $("
"); $info.append($name, $desc, $effects, $cost); const $actions = $("
"); const $upgrade = $(""); const $downgrade = $(""); $upgrade.on("click", () => this.onBuild(this.cityId, index)); $downgrade.on("click", () => this.onDemolish(this.cityId, index)); $actions.append($upgrade, $downgrade); $card.append($info, $actions); this.$list.append($card); return { proto, index, $name, $effects, $cost, $upgrade, $downgrade }; } refresh(gdp, budget, levels) { this.$budget.text(`Budget: €${groupDigits(Math.round(budget))}`); for (const row of this.rows) { const level = levels[row.index] || 0; row.$name.text(`${row.proto.name} — Level ${level} / ${row.proto.maxLevel}`); row.$effects.text(effectsText(row.proto.effects, level > 0 ? level : 1)); const parts = []; if (level > 0) parts.push(`Upkeep €${groupDigits(Math.round(buildingUpkeep(row.proto, gdp, level)))}/h`); if (level < row.proto.maxLevel) { parts.push(`Build cost €${groupDigits(Math.round(buildingBuildCost(row.proto, gdp, level)))}`); } row.$cost.text(parts.join(" · ")); if (level < row.proto.maxLevel) { const cost = buildingBuildCost(row.proto, gdp, level); row.$upgrade.text(`Upgrade €${groupDigits(Math.round(cost))}`).prop("disabled", budget < cost); } else { row.$upgrade.text("Max level").prop("disabled", true); } if (level > 0) { row.$downgrade .text(`Downgrade +€${groupDigits(Math.round(buildingRefund(row.proto, gdp, level)))}`) .prop("disabled", false); } else { row.$downgrade.text("Not built").prop("disabled", true); } } } close() { this.$modal.addClass("hidden"); this.cityId = 0; } } export class NationModal { constructor() { this.$modal = $("#modal-nation"); this.$governmentSub = $("#government-sub"); this.$governmentList = $("#government-list"); this.$technologySub = $("#technology-sub"); this.$technologyList = $("#technology-list"); this.governments = []; this.technologies = []; this.currentGovernment = 0; this.researched = new Set(); this.governmentRows = []; this.technologyRows = []; this.onGovernment = () => {}; this.onTechnology = () => {}; this.$modal.find(".modal-close").on("click", () => this.close()); this.$modal.find(".tab").on("click", (event) => { const tab = $(event.currentTarget).data("tab"); this.$modal.find(".tab").removeClass("active"); $(event.currentTarget).addClass("active"); this.$modal.find(".tab-page").removeClass("active"); $(`#tab-${tab}`).addClass("active"); }); } isShowing() { return !this.$modal.hasClass("hidden"); } show(governments, currentGovernment, technologies, researched, gdp, budget, research) { this.governments = governments; this.technologies = technologies; this.governmentRows = governments.map((proto, index) => this._makeRow(this.$governmentList, proto, index, "government")); this.technologyRows = technologies.map((proto, index) => this._makeRow(this.$technologyList, proto, index, "technology")); this.refresh(currentGovernment, researched, gdp, budget, research); this.$modal.removeClass("hidden"); } _makeRow($list, proto, index, kind) { const $card = $("
"); const $info = $("
"); const $name = $("
"); const $desc = $("
").text(proto.description || ""); const $effects = $("
"); const $cost = $("
"); $info.append($name, $desc, $effects, $cost); const $actions = $("
"); const $action = $(""); $action.on("click", () => { if (kind === "government") this.onGovernment(index); else this.onTechnology(index); }); $actions.append($action); $card.append($info, $actions); $list.append($card); return { proto, index, kind, $name, $effects, $cost, $action }; } refresh(currentGovernment, researched, gdp, budget, research) { this.currentGovernment = currentGovernment; this.researched = researched instanceof Set ? researched : new Set(Object.keys(researched).map(Number)); this.$governmentSub.text( `Government — ${this.governments[currentGovernment] ? this.governments[currentGovernment].name : "None"} · Budget: €${groupDigits(Math.round(budget))}` ); this.$technologySub.text(`Research points: ${groupDigits(Math.round(research))}`); for (const row of this.governmentRows) { const proto = row.proto; row.$name.text(proto.name); row.$effects.text(effectsText(proto.effects, 1)); if (row.index === currentGovernment) { row.$cost.text("Currently in power"); row.$action.text("Current government").prop("disabled", true); } else { const cost = governmentChangeCost(proto, gdp); row.$cost.text(`Adoption cost €${groupDigits(Math.round(cost))}`); row.$action.text(`Adopt €${groupDigits(Math.round(cost))}`).prop("disabled", budget < cost); } } for (const row of this.technologyRows) { const proto = row.proto; row.$name.text(proto.name); row.$effects.text(effectsText(proto.effects, 1)); if (this.researched.has(row.index)) { row.$cost.text("Researched"); row.$action.text("Researched").prop("disabled", true); } else if (!prerequisitesMet(proto, this.researched, this.technologies)) { const names = proto.prerequisites .map((id) => { const found = this.technologies.find((t) => t.id === id); return found ? found.name : id; }) .join(", "); row.$cost.text(`Requires: ${names}`); row.$action.text("Locked").prop("disabled", true); } else { row.$cost.text(`Cost ${groupDigits(Math.round(proto.cost))} research points`); row.$action.text(`Research ${groupDigits(Math.round(proto.cost))}`).prop("disabled", research < proto.cost); } } } close() { this.$modal.addClass("hidden"); } }