Files
Battle-for-Tismo/client/js/modals/format.js
T

190 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Shared formatting helpers for the modal panels.
import { groupDigits } from "../../../shared/text_format.js";
import { policyById, resourceById } from "../../../shared/data.js";
import { marketPriceIndex, resourceMarketValue } from "../../../shared/resources.js";
import { formatMoneySigned } from "../currency.js";
import { researchNode } from "../ui/card.js";
// The world market's current prices, from the snapshot's market listing.
export function marketPricesFrom(snapshot) {
const prices = {};
for (const entry of (snapshot && snapshot.resourceMarket) || []) {
prices[entry.id] = entry.price;
}
return prices;
}
// A fixed money cost quoted at today's market: a cost with no material bill of
// its own is scaled by the global price index, so every euro figure drifts with
// commodity prices.
export function quotedMoney(amount, prices) {
return Math.round((amount || 0) * marketPriceIndex(prices));
}
// A construction's full price at today's market: the fixed build cost plus the
// market value of its whole material bill, ignoring anything already in store.
export function quotedBuildCost(buildCost, materials, prices) {
return Math.round((buildCost || 0) + resourceMarketValue(materials, prices));
}
export function formatEffect(effect, level) {
const value = level <= 0 ? 0 : effect.baseValue * Math.pow(level, effect.exponent);
switch (effect.stat) {
case "production":
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% production`;
case "research":
return `${value >= 0 ? "+" : ""}${value.toFixed(2)} research/h`;
case "culture":
return `${value >= 0 ? "+" : ""}${value.toFixed(2)} culture/h`;
case "infantry_attack":
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% infantry attack`;
case "infantry_defense":
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% infantry defense`;
case "artillery_attack":
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% artillery attack`;
case "construction_speed":
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% construction speed`;
case "rail_speed":
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% railway speed`;
case "energy_efficiency":
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% electricity per fuel`;
case "steel_efficiency":
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% steel per energy`;
case "food_efficiency":
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% food per energy`;
case "luxury_efficiency":
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% luxury per energy`;
case "hightech_efficiency":
return `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}% high-tech per energy`;
default:
return `${effect.stat} ${value.toFixed(2)}`;
}
}
export function effectsText(effects, level) {
const parts = effects.map((e) => formatEffect(e, level));
return parts.length > 0 ? parts.join(" · ") : "No effect";
}
// The effect line for a building: its stat effects, or, for a utility building
// whose benefit is a rule, a short description of that rule at `level`.
export function buildingEffectText(proto, level) {
if (proto.effects && proto.effects.length > 0) return effectsText(proto.effects, level);
switch (proto.mechanic) {
case "parallel_training":
return `${1 + level} parallel training line${level === 0 ? "" : "s"}`;
case "parallel_building":
return `${1 + level} parallel building line${level === 0 ? "" : "s"}`;
case "civilian_shelter": {
const saved = Math.round((proto.civilianProtectionPerLevel || 0) * level * 100);
return `−${saved}% civilian bombing losses · very hard to destroy`;
}
case "air_defense":
return "Fires one volley at hostile aircraft over the city";
case "port_production": {
const per = (proto.productionPerSeaTilePerLevel || 0) * level;
const food = (proto.foodPerSeaTilePerLevel || 0) * level * 100;
const parts = [`+${groupDigits(per)} production per person per adjacent sea tile to the region`];
if (food > 0) parts.push(`+${food.toFixed(1)}% food output per adjacent sea tile`);
return parts.join(" · ");
}
case "air_immigration":
return "Immigration from every other city with an airport";
default:
return "No effect";
}
}
// The effect list as DOM, so a research effect can carry the erlenmeyer icon in
// place of the word "research". Every other effect reads as its plain text.
export function effectNodes(effects, level) {
const $frag = $("<span class='effect-text'></span>");
(effects || []).forEach((effect, index) => {
if (index > 0) $frag.append(document.createTextNode(" · "));
if (effect.stat === "research") {
const value = level <= 0 ? 0 : effect.baseValue * Math.pow(level, effect.exponent);
$frag.append(researchNode(value, { sign: value >= 0 ? "+" : "", decimals: 2 }));
$frag.append(document.createTextNode("/h"));
} else {
$frag.append(document.createTextNode(formatEffect(effect, level)));
}
});
return $frag;
}
// The effect line for a building as DOM: a stats building shows its effects with
// the research icon, a utility building its rule text.
export function buildingEffectNodes(proto, level) {
if (proto.effects && proto.effects.length > 0) return effectNodes(proto.effects, level);
return document.createTextNode(buildingEffectText(proto, level));
}
export const SOURCE_LABELS = { building: "building", government: "government", technology: "research" };
export function signedMoney(value) {
return `${formatMoneySigned(value)}/h`;
}
// A signed money amount with no period suffix, for the monthly budget report.
export function money(value) {
return formatMoneySigned(value);
}
export function percent(value) {
return `${value < 0 ? "−" : "+"}${(Math.abs(value) * 100).toFixed(1)}%`;
}
// Renders one news entry as text. Government actions name the specific actors
// doing them — "French troops", "French scientists" — rather than the nation as
// a whole, and the victim or target is named in the people's form ("the
// Vietnamese").
export function describeNews(entry, civs) {
const name = (index) => (civs[index] ? civs[index].name : `Nation ${index + 1}`);
// The attributive form of a nation, for putting before an actor noun. It
// falls back to the demonym, which doubles as an adjective for most of them;
// the few person-nouns (New Zealander, New Hampshirite) carry an explicit one.
const adjective = (index) => {
const civ = civs[index];
if (!civ) return `Nation ${index + 1}`;
return civ.adjective || civ.demonym || civ.name;
};
const people = (index) => {
const civ = civs[index];
return civ && civ.demonym ? `the ${civ.demonym}` : name(index);
};
switch (entry.type) {
case "war":
return `${adjective(entry.actor)} leaders have declared war on ${name(entry.target)}`;
case "capture":
return `${adjective(entry.to)} troops have captured ${entry.city} from ${people(entry.from)}`;
case "policy": {
const policy = policyById(entry.policy);
const policyName = policy ? policy.name : entry.policy;
const who =
civs[entry.ethnicity] && civs[entry.ethnicity].demonym
? `the ${civs[entry.ethnicity].demonym}`
: name(entry.ethnicity);
return `${adjective(entry.civ)} lawmakers have enacted ${policyName} for ${who}`;
}
case "technology":
return entry.repeatable
? `${adjective(entry.civ)} scientists have improved ${entry.name}`
: `${adjective(entry.civ)} scientists have discovered ${entry.name}`;
case "join":
return `${entry.player} took control of ${name(entry.civ)}`;
case "tariff": {
const rate = `${(entry.rate * 100).toFixed(1)}%`;
const resource = entry.resource
? (resourceById(entry.resource) ? resourceById(entry.resource).name : entry.resource)
: "all resources";
const kind = entry.kind === "export" ? "export tariff" : "import tariff";
return `${adjective(entry.civ)} leaders set the ${kind} on ${resource} to ${rate}`;
}
case "famine":
return `${people(entry.civ)} are starving in ${entry.region}: a famine has begun`;
default:
return entry.type;
}
}