Files
Battle-for-Tismo/client/js/modals.js
T
2026-09-16 19:59:18 +02:00

288 lines
11 KiB
JavaScript

// 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 = $("<button class='aero'></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 = $("<div class='row-card'></div>");
const $info = $("<div class='info'></div>");
const $name = $("<div class='name'></div>");
const $desc = $("<div class='desc'></div>").text(proto.description);
const $effects = $("<div class='effects'></div>");
const $cost = $("<div class='cost'></div>");
$info.append($name, $desc, $effects, $cost);
const $actions = $("<div class='actions'></div>");
const $upgrade = $("<button class='aero'></button>");
const $downgrade = $("<button class='aero'></button>");
$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 = $("<div class='row-card'></div>");
const $info = $("<div class='info'></div>");
const $name = $("<div class='name'></div>");
const $desc = $("<div class='desc'></div>").text(proto.description || "");
const $effects = $("<div class='effects'></div>");
const $cost = $("<div class='cost'></div>");
$info.append($name, $desc, $effects, $cost);
const $actions = $("<div class='actions'></div>");
const $action = $("<button class='aero'></button>");
$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");
}
}