PageSourceSearch

https://aoe2.up.railway.app/static/js/matchup.js

js aoe2.up.railway.app collected 2026-10-03 12:11:28 UTC 10,959 bytes, 272 lines download raw bytes

1/* ==========================================================================
2   AoE2 Unit Analyzer - Civ Analysis Page Logic
3   Depends on: constants.js (ICON_BASE, NAME_TO_ICON, CIV_EMBLEM_BASE, getIconUrl)
4   Expects global: CIVS (injected by inline <script> from Jinja2)
5   ========================================================================== */
6
7/* ---- Constants ---- */
8const COLUMN_DEFS = {
9    cavalry: ["light_cav", "knight", "camel", "steppe_lancer", "elephant"],
10    ranged: ["skirmisher", "archer", "cav_archer", "gunpowder", "scorpion"],
11    infantry: ["militia", "spear", "shock_infantry"],
12    siege: ["ram", "mangonel", "bombard_cannon", "trebuchet", "cannon_galleon"],
13    navy: ["galleon", "fire", "hulk", "demo"],
14};
15
16const BUILDING_LABELS = {
17    barracks: "Barracks",
18    archery_range: "Archery Range",
19    stable: "Stable",
20    castle: "Castle",
21    siege_workshop: "Siege Workshop",
22    dock: "Dock",
23};
24
25const COLUMN_LABELS = {
26    cavalry: "Cavalry",
27    ranged: "Ranged",
28    infantry: "Infantry",
29    siege: "Siege",
30    navy: "Navy",
31};
32
33const LINE_NAMES = {
34    light_cav: "Light Cavalry",
35    knight: "Knight Line",
36    camel: "Camel Line",
37    steppe_lancer: "Steppe Lancer",
38    elephant: "Battle Elephant",
39    skirmisher: "Skirmisher",
40    archer: "Archer Line",
41    cav_archer: "Cavalry Archer",
42    gunpowder: "Gunpowder",
43    scorpion: "Scorpion",
44    militia: "Militia Line",
45    spear: "Spear Line",
46    shock_infantry: "Shock Infantry",
47    ram: "Rams",
48    mangonel: "Mangonel Line",
49    bombard_cannon: "Bombard Cannon",
50    trebuchet: "Trebuchet",
51    cannon_galleon: "Cannon Galleon",
52    galleon: "Galleon Line",
53    fire: "Fire Ship Line",
54    hulk: "Hulk Line",
55    demo: "Demo Ship Line",
56};
57
58const COLUMN_ORDER = ["cavalry", "ranged", "infantry", "siege", "navy"];
59const BUILDING_ORDER_CIV = [
60    "barracks", "archery_range", "stable", "castle", "siege_workshop", "dock"
61];
62
63/* Player-facing tier ladder (best -> worst). The backend stamps `unit.tier`;
64   these supply the human label + a one-line hint for the tooltip. Border edges
65   and pill colours are driven by the .is-tier-<tier> / .tier-<tier> CSS classes
66   (theme-aware design tokens), so colour follows the theme. See best_units.py
67   (_classify_tier) for how each tier's limits are set per unit line. */
68const TIER_META = {
69    signature:   { label: "Signature",   hint: "Civ-defining: clearly the best version of this unit, and part of the civ's identity." },
70    good:        { label: "Good",        hint: "A clearly above-average version of this unit." },
71    generic:     { label: "Generic",     hint: "A standard version — the same as most civs, nothing special." },
72    bad:         { label: "Bad",         hint: "A clearly below-average version of this unit." },
73    situational: { label: "Situational", hint: "Weak overall, but still worth building for its niche (countering a specific unit)." },
74    worst:       { label: "Worst",       hint: "Among the weakest in the game — this civ effectively never builds it." },
75};
76
77const SUMMARY_TEMPLATES = {
78    multi_flexible: "This civ is strong across {areas}, so it can pursue flexible strategies and adapt to any opponent.",
79    one_area_strong: "This civ is strongest in {primary_strength}, so it must leverage that advantage to win.",
80    none_exceptional: "This civ doesn't scale exceptionally in late game. Focus on doing early damage and maintaining a lead.",
81};
82
83/* ---- DOM refs ---- */
84const stepLabel = document.getElementById("step-label");
85const civGrid = document.getElementById("civ-grid");
86const resultsEl = document.getElementById("results");
87
88let selectedCiv = null;
89const civilizationData = import("./shared/page-data.js").then(m => m.createPageData());
90
91/* ---- Build/enhance civ grid ----
92   The template supplies real links so the picker remains useful without JS and
93   search engines can discover each civilization without a duplicate summary.
94   JS enhances those links into the in-page selector. */
95CIVS.forEach(function (name) {
96    var slug = name.toLowerCase();
97    var card = Array.from(civGrid.querySelectorAll(".civ-card")).find(function (candidate) {
98        return candidate.dataset.civ === name;
99    });
100    if (!card) {
101        card = document.createElement("a");
102        card.className = "civ-card";
103        card.dataset.civ = name;
104        card.href = "/civilizations/" + slug;
105        var img = document.createElement("img");
106        img.className = "civ-emblem";
107        img.src = (typeof CIV_EMBLEMS !== "undefined" && CIV_EMBLEMS[name]) || CIV_EMBLEM_BASE + slug + ".png";
108        img.alt = name;
109        img.loading = "lazy";
110        var label = document.createElement("span");
111        label.className = "civ-card-name";
112        label.textContent = name;
113        card.appendChild(img);
114        card.appendChild(label);
115        civGrid.appendChild(card);
116    }
117    card.addEventListener("click", function (event) {
118        event.preventDefault();
119        onCivClick(name);
120    });
121});
122
123/* ---- Per-civ landing page preselect (set by civ_detail.html) ---- */
124if (window.PRESELECT_CIV && CIVS.indexOf(window.PRESELECT_CIV) !== -1) {
125    onCivClick(window.PRESELECT_CIV);
126}
127
128/* ---- Civ click handler ---- */
129function onCivClick(name) {
130    if (selectedCiv === name) {
131        /* Keep the selected analysis open; a second tap is a convenient way to
132           return to it after browsing the picker at the bottom of the page. */
133        resultsEl.scrollIntoView({ behavior: "smooth", block: "start" });
134        return;
135    } else {
136        selectedCiv = name;
137        stepLabel.textContent = "Choose another civilization";
138        stepLabel.className = "step-label step-selected";
139        loadAnalysis(name);
140    }
141    updateGrid();
142}
143
144function updateGrid() {
145    civGrid.querySelectorAll(".civ-card").forEach(function (card) {
146        var name = card.dataset.civ;
147        card.classList.remove("selected-civ1", "disabled");
148        if (name === selectedCiv) {
149            card.classList.add("selected-civ1");
150            card.setAttribute("aria-current", "page");
151        } else {
152            card.removeAttribute("aria-current");
153        }
154    });
155}
156
157/* ---- Load analysis from API ---- */
158async function loadAnalysis(civName) {
159    resetPinnedTooltips();
160    resultsEl.className = "results-container visible";
161    const bootstrap = window.CIV_ANALYSIS?.civ_name === civName ? window.CIV_ANALYSIS : null;
162    if (!bootstrap) resultsEl.innerHTML = '<div class="loading-spinner"><div class="spinner"></div><div>Loading analysis…</div></div>';
163
164    try {
165        var data = await (await civilizationData).select("/api/civilizations/" + encodeURIComponent(civName), bootstrap);
166        if (!data || selectedCiv !== civName) return;
167        resultsEl.innerHTML = renderAnalysis(civName, data);
168    } catch (e) {
169        if (e.name === "AbortError" || selectedCiv !== civName) return;
170        resultsEl.innerHTML = '<div class="no-data">Error: ' + escapeHtml(e.message) + '</div>';
171    }
172    resultsEl.scrollIntoView({ behavior: "smooth", block: "start" });
173}
174
175/* ==========================================================================
176   Tap-to-pin tooltips (touch + viewport-safe)
177   The unit tooltips are CSS hover bubbles on desktop, but hover doesn't exist
178   on touch, so the breakdown is unreachable there. This adds a tap/click that
179   pins a badge's tooltip (toggling a `.pinned` class the CSS reveals), with a
180   visible × close and outside-tap / Escape dismiss. Only one tooltip is pinned
181   at a time. To focus the selection we highlight the pinned card and fade the
182   rest (a `.tooltip-focus` class on #results) rather than laying a full-screen
183   overlay over the cards — the results grid sits inside transformed containers
184   (animation wrappers), so an overlay would paint on top of and dim the very
185   card it's meant to spotlight, and swallow taps on the open sheet.
186   Uses event delegation on #results so it survives every re-render.
187   ========================================================================== */
188(function () {
189    if (!resultsEl) return;
190
191    var pinnedTooltip = null;
192    var pinnedBadge = null;
193
194    function unpin() {
195        if (pinnedTooltip) {
196            pinnedTooltip.classList.remove("pinned");
197            pinnedTooltip = null;
198        }
199        if (pinnedBadge) {
200            pinnedBadge.classList.remove("tooltip-pinned");
201            pinnedBadge = null;
202        }
203        resultsEl.classList.remove("tooltip-focus");
204    }
205
206    function pin(tooltip) {
207        if (pinnedTooltip === tooltip) {
208            unpin();
209            return;
210        }
211        unpin();
212        tooltip.classList.add("pinned");
213        pinnedTooltip = tooltip;
214        /* Highlight the owning card and fade the rest so the selected unit
215           stands out instead of every card looking the same (or dimmed). */
216        pinnedBadge = tooltip.closest(".unit-badge");
217        if (pinnedBadge) pinnedBadge.classList.add("tooltip-pinned");
218        resultsEl.classList.add("tooltip-focus");
219    }
220
221    /* Tap / click on a badge toggles its pinned tooltip. */
222    resultsEl.addEventListener("click", function (e) {
223        /* Close button inside a pinned tooltip. */
224        if (e.target.closest(".unit-badge-tooltip-close")) {
225            e.preventDefault();
226            e.stopPropagation();
227            unpin();
228            return;
229        }
230
231        var badge = e.target.closest(".unit-badge");
232        if (!badge) return;
233
234        var tooltip = badge.querySelector(".unit-badge-tooltip");
235        if (!tooltip) return; /* badge with no special info -> no tooltip */
236
237        /* Don't swallow taps that land inside an already-pinned tooltip
238           (e.g. selecting text or scrolling the sheet). */
239        if (pinnedTooltip === tooltip && e.target.closest(".unit-badge-tooltip")) {
240            return;
241        }
242
243        e.stopPropagation();
244        pin(tooltip);
245    });
246
247    /* Outside tap anywhere on the document dismisses the pinned tooltip. */
248    document.addEventListener("click", function (e) {
249        if (!pinnedTooltip) return;
250        if (e.target.closest(".unit-badge-tooltip")) return; /* tap inside sheet */
251        if (e.target.closest(".unit-badge")) return;         /* handled above */
252        unpin();
253    });
254
255    /* Escape closes it. */
256    document.addEventListener("keydown", function (e) {
257        if (e.key === "Escape" && pinnedTooltip) unpin();
258    });
259
260    /* A re-render (selecting a different civ, or deselecting) destroys the
261       pinned tooltip node;
261 this lets those paths clear our reference + focus
262       state so nothing dangles. */
263    resultsEl.addEventListener("unit-tooltip-reset", unpin);
264})();
265
266/* Fired before #results is re-rendered so the pinned-tooltip controller can
267   tear down cleanly (see loadAnalysis / onCivClick). */
268function resetPinnedTooltips() {
269    if (resultsEl) {
270        resultsEl.dispatchEvent(new CustomEvent("unit-tooltip-reset"));
271    }
272}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.