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.