The simple economy pays for propaganda out of the nation's culture stock and lapses a campaign it can no longer fund, but the tab quoted money and never named the store. Give culture the clapperboard icon as a non-tradeable resource like research, read the stock and the campaigns' daily drain out on the Propaganda tab, and price every campaign figure in culture when the simple economy runs.
247 lines
9.7 KiB
JavaScript
247 lines
9.7 KiB
JavaScript
// The building / improvement card. The city buildings tab and the HUD's tile
|
||
// improvement panel draw one card per type: an icon, a name and a status up
|
||
// top, then an action button carrying only the estimated price and the build
|
||
// time. What the work produces, consumes and costs lives in a hover tooltip, so
|
||
// the card itself stays bare. Shared by both panels so they read the same.
|
||
|
||
import { resourceById } from "../../../shared/data.js";
|
||
import { formatResourceAmount } from "../../../shared/resources.js";
|
||
import { groupDigits } from "../../../shared/text_format.js";
|
||
|
||
// The materials a construction bills in, in reading order.
|
||
export const CARD_RESOURCES = ["steel", "energy", "hightech"];
|
||
|
||
// A commodity's icon as an image node, for a row that names a resource with a
|
||
// picture rather than the word.
|
||
export function resourceIcon(id) {
|
||
const proto = resourceById(id);
|
||
const $img = $("<img>");
|
||
if (proto) $img.attr("src", `assets/${proto.icon}`).attr("alt", proto.name);
|
||
return $img;
|
||
}
|
||
|
||
// A resource amount with its commodity icon. `sign` marks the direction of the
|
||
// flow: "+" for output, "−" for a cost.
|
||
function resourceLine(id, amount, sign) {
|
||
const proto = resourceById(id);
|
||
const $span = $("<span class='res'></span>");
|
||
if (proto) {
|
||
$span.append($("<img>").attr("src", `assets/${proto.icon}`).attr("alt", proto.name));
|
||
}
|
||
$span.append(document.createTextNode(`${sign}${formatResourceAmount(id, amount)}`));
|
||
return $span;
|
||
}
|
||
|
||
// A research-point figure with its erlenmeyer icon. The icon stands in for the
|
||
// word "research", so no unit text follows: the science buildings, the
|
||
// technology cards and the research rate all read the same way.
|
||
export function researchNode(amount, { sign = "", decimals = 0 } = {}) {
|
||
const proto = resourceById("research");
|
||
const $span = $("<span class='res research-points'></span>");
|
||
if (proto) {
|
||
$span.append($("<img>").attr("src", `assets/${proto.icon}`).attr("alt", proto.name));
|
||
}
|
||
const value = Number(amount) || 0;
|
||
const text = decimals > 0 ? value.toFixed(decimals) : groupDigits(Math.round(value));
|
||
$span.append(document.createTextNode(`${sign}${text}`));
|
||
return $span;
|
||
}
|
||
|
||
// A culture-point figure with its clapperboard icon, standing in for the word
|
||
// "culture" the way the erlenmeyer stands in for research. The simple economy
|
||
// spends culture on propaganda, so the campaign panels read with the same
|
||
// grammar as the research figures.
|
||
export function cultureNode(amount, { sign = "", decimals = 0 } = {}) {
|
||
const proto = resourceById("culture");
|
||
const $span = $("<span class='res culture-points'></span>");
|
||
if (proto) {
|
||
$span.append($("<img>").attr("src", `assets/${proto.icon}`).attr("alt", proto.name));
|
||
}
|
||
const value = Number(amount) || 0;
|
||
const text = decimals > 0 ? value.toFixed(decimals) : groupDigits(Math.round(value));
|
||
$span.append(document.createTextNode(`${sign}${text}`));
|
||
return $span;
|
||
}
|
||
|
||
// An inline resource bill for a card: a muted label, then one icon-and-amount
|
||
// per material, then an optional suffix. Used for a unit's daily upkeep, which
|
||
// is paid in materials rather than money.
|
||
export function resourceBill(items, { label = "", sign = "−", suffix = "" } = {}) {
|
||
const $bill = $("<span class='bt-bill'></span>");
|
||
if (label) $bill.append($("<span class='bt-bill-label'></span>").text(label));
|
||
for (const item of items) $bill.append(resourceLine(item.id, item.amount, sign));
|
||
if (suffix) $bill.append($("<span class='bt-bill-suffix'></span>").text(suffix));
|
||
return $bill;
|
||
}
|
||
|
||
// The three tooltip sections from a work's flows. Each entry is a resource
|
||
// { id, amount } or free text { text }; free text is shown exactly as written.
|
||
// `desc` is an optional prose line shown first, with no section heading.
|
||
export function tooltipSections({
|
||
desc = "",
|
||
produce = [],
|
||
produceSuffix = "",
|
||
consume = [],
|
||
consumeSuffix = "",
|
||
cost = [],
|
||
} = {}) {
|
||
const sections = [
|
||
{ label: "Produces", cls: "tip-produce", sign: "+", suffix: produceSuffix, items: produce },
|
||
{ label: "Consumes", cls: "tip-consume", sign: "−", suffix: consumeSuffix, items: consume },
|
||
{ label: "Costs", cls: "tip-cost", sign: "−", suffix: "", items: cost },
|
||
];
|
||
if (desc) sections.unshift({ desc });
|
||
return sections;
|
||
}
|
||
|
||
let $tooltip = null;
|
||
|
||
// Removes any open card tooltip. Called on mouseleave and whenever the panel
|
||
// rebuilds, so a stale tooltip never lingers over fresh content.
|
||
export function hideCardTooltip() {
|
||
if ($tooltip) {
|
||
$tooltip.remove();
|
||
$tooltip = null;
|
||
}
|
||
}
|
||
|
||
// Opens the tooltip beside `$anchor`, flipping left or up when it would run off
|
||
// the viewport. It is fixed to the body, so a scrolling card list never clips
|
||
// it.
|
||
export function showCardTooltip($anchor, sections) {
|
||
hideCardTooltip();
|
||
if (!$anchor || !$anchor.length) return;
|
||
if (!sections || !sections.length) return;
|
||
$tooltip = $("<div class='card-tooltip'></div>");
|
||
for (const section of sections) {
|
||
if (section.desc) {
|
||
$tooltip.append($("<div class='tip-section tip-desc'></div>").text(section.desc));
|
||
continue;
|
||
}
|
||
const $section = $("<div class='tip-section'></div>");
|
||
$section.append($("<div class='tip-head'></div>").text(section.label));
|
||
if (!section.items || section.items.length === 0) {
|
||
$section.append($("<div class='tip-line tip-none'></div>").text("nothing"));
|
||
} else {
|
||
for (const item of section.items) {
|
||
const $line = $("<div></div>").addClass(`tip-line ${section.cls}`);
|
||
if (item.node) $line.append(item.node);
|
||
else if (item.id) $line.append(resourceLine(item.id, item.amount, section.sign));
|
||
else $line.text(item.text);
|
||
if (section.suffix) $line.append(document.createTextNode(section.suffix));
|
||
$section.append($line);
|
||
}
|
||
}
|
||
$tooltip.append($section);
|
||
}
|
||
$("body").append($tooltip);
|
||
const rect = $anchor[0].getBoundingClientRect();
|
||
$tooltip.css({ left: rect.right + 10, top: rect.top });
|
||
const tip = $tooltip[0].getBoundingClientRect();
|
||
if (tip.right > window.innerWidth - 8 && tip.width) {
|
||
$tooltip.css("left", rect.left - tip.width - 10);
|
||
}
|
||
if (tip.bottom > window.innerHeight - 8) {
|
||
$tooltip.css("top", Math.max(8, window.innerHeight - tip.height - 8));
|
||
}
|
||
}
|
||
|
||
// Puts the action button's material cost and build time in its meta line.
|
||
// `resources` is the work's material bill (an array of { id, amount }), drawn
|
||
// with the commodity icons and a minus sign; the hours follow it. A work with
|
||
// nothing to spend (locked, or already standing) shows nothing. Callers whose
|
||
// cost is not a resource bill pass free `text` instead, shown verbatim. The
|
||
// estimated euro price lives in the hover tooltip, never here, so a long bill
|
||
// cannot overflow the button.
|
||
export function setCardMeta($meta, { resources = null, hours = "", text = "" } = {}) {
|
||
$meta.empty();
|
||
if (resources && resources.length) {
|
||
$meta.append(resourceBill(resources, { sign: "−" }));
|
||
if (hours) $meta.append(document.createTextNode(` · ${hours}`));
|
||
} else if (text) {
|
||
$meta.text(text);
|
||
} else if (hours) {
|
||
$meta.text(hours);
|
||
}
|
||
}
|
||
|
||
// Builds a card. `spec` carries:
|
||
// icon, name, status, statusBuilt, built, locked
|
||
// note (a muted line under the header, e.g. a unit's upkeep)
|
||
// action { label, money, hours, text, disabled }
|
||
// progress (the action button carries a fill the caller sets,
|
||
// e.g. focused research; label and meta sit above it)
|
||
// remove { label, title, enabled } (omit for no remove control)
|
||
// tip () => tooltip sections
|
||
// onAction, onRemove
|
||
// It returns the pieces the caller keeps to refresh the card in place.
|
||
export function buildCard(spec) {
|
||
const $card = $("<div class='building-tile'></div>");
|
||
if (spec.built) $card.addClass("built");
|
||
if (spec.locked) $card.addClass("locked");
|
||
|
||
const $top = $("<div class='bt-top'></div>");
|
||
if (spec.icon) {
|
||
$top.append(
|
||
$("<img class='building-icon'>").attr("src", `assets/${spec.icon}`).attr("alt", spec.name)
|
||
);
|
||
}
|
||
const $id = $("<div class='bt-id'></div>");
|
||
$id.append($("<div class='bt-name'></div>").text(spec.name));
|
||
const $status = $("<div class='bt-level'></div>")
|
||
.text(spec.status || "")
|
||
.toggleClass("built", !!spec.statusBuilt);
|
||
$id.append($status);
|
||
$top.append($id);
|
||
$card.append($top);
|
||
|
||
let $note = null;
|
||
if (spec.note) {
|
||
$note = $("<div class='bt-note'></div>");
|
||
if (typeof spec.note === "string") $note.text(spec.note);
|
||
else $note.append(spec.note);
|
||
$card.append($note);
|
||
}
|
||
|
||
const action = spec.action || {};
|
||
const $action = $("<button class='aero bt-upgrade'></button>").prop("disabled", !!action.disabled);
|
||
// A progress fill lives behind the button's own label and meta, so the
|
||
// button itself reads as the bar.
|
||
let $progressFill = null;
|
||
if (spec.progress) {
|
||
$progressFill = $("<span class='bt-progress-fill'></span>");
|
||
$action.append($progressFill);
|
||
}
|
||
$action.append($("<span class='bt-up-label'></span>").text(action.label || ""));
|
||
const $meta = $("<span class='bt-meta'></span>");
|
||
setCardMeta($meta, action);
|
||
$action.append($meta);
|
||
$card.append($action);
|
||
if (spec.onAction) $action.on("click", spec.onAction);
|
||
if (spec.tip) {
|
||
$action.on("mouseenter", () => showCardTooltip($action, spec.tip()));
|
||
$action.on("mouseleave", hideCardTooltip);
|
||
}
|
||
|
||
let $remove = null;
|
||
if (spec.remove) {
|
||
$remove = $("<button class='aero bt-downgrade'></button>")
|
||
.text(spec.remove.label || "✕")
|
||
.attr("title", spec.remove.title || "")
|
||
.prop("disabled", !spec.remove.enabled);
|
||
if (spec.onRemove) $remove.on("click", spec.onRemove);
|
||
$card.append($remove);
|
||
}
|
||
|
||
return {
|
||
$card,
|
||
$status,
|
||
$action,
|
||
$label: $action.find(".bt-up-label"),
|
||
$meta,
|
||
$remove,
|
||
$note,
|
||
$progressFill,
|
||
};
|
||
}
|