288 lines
11 KiB
JavaScript
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");
|
|
}
|
|
}
|