// 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 = $(""); 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 = $(""); if (proto) { $span.append($("").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 = $(""); if (proto) { $span.append($("").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 = $(""); if (label) $bill.append($("").text(label)); for (const item of items) $bill.append(resourceLine(item.id, item.amount, sign)); if (suffix) $bill.append($("").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 = $("
"); for (const section of sections) { if (section.desc) { $tooltip.append($("
").text(section.desc)); continue; } const $section = $("
"); $section.append($("
").text(section.label)); if (!section.items || section.items.length === 0) { $section.append($("
").text("nothing")); } else { for (const item of section.items) { const $line = $("
").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 = $("
"); if (spec.built) $card.addClass("built"); if (spec.locked) $card.addClass("locked"); const $top = $("
"); if (spec.icon) { $top.append( $("").attr("src", `assets/${spec.icon}`).attr("alt", spec.name) ); } const $id = $("
"); $id.append($("
").text(spec.name)); const $status = $("
") .text(spec.status || "") .toggleClass("built", !!spec.statusBuilt); $id.append($status); $top.append($id); $card.append($top); let $note = null; if (spec.note) { $note = $("
"); if (typeof spec.note === "string") $note.text(spec.note); else $note.append(spec.note); $card.append($note); } const action = spec.action || {}; const $action = $("").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 = $(""); $action.append($progressFill); } $action.append($("").text(action.label || "")); const $meta = $(""); 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 = $("") .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, }; }