1(function () { 2 'use strict'; 3 4 var STORAGE_KEY = 'hh-theme'; 5 var root = document.documentElement; 6 var systemDark = window.matchMedia('(prefers-color-scheme: dark)'); 7 8 function storedTheme() { 9 try { 10 return localStorage.getItem(STORAGE_KEY); 11 } catch (e) { 12 return null; 13 } 14 } 15 16 function currentTheme() { 17 return root.dataset.theme || (systemDark.matches ? 'dark' : 'light'); 18 } 19 20 function applyTheme(theme) { 21 var systemTheme = systemDark.matches ? 'dark' : 'light'; 22 try { 23 // choice equal to the system setting means "follow the system" again 24 if (theme === systemTheme) { 25 localStorage.removeItem(STORAGE_KEY); 26 } else { 27 localStorage.setItem(STORAGE_KEY, theme); 28 } 29 } catch (e) { 30 } 31 if (theme === systemTheme) { 32 delete root.dataset.theme; 33 } else { 34 root.dataset.theme = theme; 35 } 36 syncThemeUi(); 37 document.dispatchEvent(new CustomEvent('hh:themechange', {detail: {theme: currentTheme()}})); 38 track('theme_change', {theme: currentTheme()}); 39 } 40 41 /** 42 * GA4 event (gtag is defined in the page head, it only queues when the GA script is blocked). 43 */ 44 function track(name, params) { 45 if (typeof window.gtag === 'function') { 46 window.gtag('event', name, params || {}); 47 } 48 } 49 50 function syncThemeUi() { 51 var theme = currentTheme(); 52 document.querySelectorAll('[data-theme-toggle]').forEach(function (button) { 53 button.setAttribute('aria-pressed', theme === 'dark' ? 'true' : 'false'); 54 button.setAttribute('title', button.dataset[theme === 'dark' ? 'labelLight' : 'labelDark'] || ''); 55 }); 56 var meta = document.querySelector('meta[name="theme-color"]'); 57 if (meta) { 58 meta.setAttribute('content', theme === 'dark' ? '#0b0f17' : '#ffffff'); 59 } 60 } 61 62 // <a data-track="event_name" data-track-game="Doom"> sends event_name {game: 'Doom'} 63 document.addEventListener('click', function (event) { 64 var element = event.target.closest('[data-track]'); 65 if (!element) { 66 return; 67 } 68 // FAQ: count only opening of the question 69 if (element.tagName === 'SUMMARY' && element.parentElement && element.parentElement.open) { 70 return; 71 } 72 var params = {}; 73 Object.keys(element.dataset).forEach(function (key) { 74 if (key.indexOf('track') === 0 && key.length > 5) { 75 var name = key.slice(5).replace(/[A-Z]/g, function (letter) { return '_' + letter.toLowerCase(); }).replace(/^_/, ''); 76 params[name] = element.dataset[key]; 77 } 78 }); 79 if (element.href) { 80 params.link_url = element.href; 81 } 82 track(element.dataset.track, params); 83 }, true); 84 85 // search redirects directly to the found tag/game, so GA4 would not see the ?q= parameter 86 document.addEventListener('submit', function (event) { 87 var form = event.target.closest('form.search'); 88 var input = form ? form.querySelector('input[name="q"]') : null; 89 if (input && input.value.trim() !== '') { 90 track('search', {search_term: input.value.trim()}); 91 } 92 }, true); 93 94 document.addEventListener('click', function (event) { 95 var themeButton = event.target.closest('[data-theme-toggle]'); 96 if (themeButton) { 97 applyTheme(currentTheme() === 'dark' ? 'light' : 'dark'); 98 return; 99 } 100 101 var navButton = event.target.closest('[data-nav-toggle]'); 102 if (navButton) { 103 var header = navButton.closest('.site-header'); 104 var open = header.classList.toggle('is-open'); 105 navButton.setAttribute('aria-expanded', open ? 'true' : 'false'); 106 } 107 }); 108 109 systemDark.addEventListener('change', function () { 110 if (!storedTheme()) { 111 syncThemeUi(); 112 document.dispatchEvent(new CustomEvent('hh:themechange', {detail: {theme: currentTheme(
112)}})); 113 } 114 }); 115 116 syncThemeUi(); 117 118 // TOPlist counter with referrer (instead of blocking document.write), only with consent to analytics 119 function loadToplist() { 120 document.querySelectorAll('img[data-toplist]').forEach(function (img) { 121 if (img.dataset.loaded) { 122 return; 123 } 124 img.dataset.loaded = '1'; 125 img.src = 'https://toplist.cz/count.asp?id=97824&logo=mc&http=' + encodeURIComponent(document.referrer) 126 + '&t=' + encodeURIComponent(document.title) + '&wi=' + screen.width + '&he=' + screen.height + '&cd=' + screen.colorDepth; 127 }); 128 } 129 130 /* ---------- Cookie consent (Google Consent Mode v2, defaults are set in the page head) ---------- */ 131 132 var CONSENT_KEY = 'hh-consent'; 133 var consentBar = document.querySelector('[data-consent]'); 134 135 function applyConsent(consent) { 136 var ads = consent.ads ? 'granted' : 'denied'; 137 if (typeof window.gtag === 'function') { 138 window.gtag('consent', 'update', { 139 analytics_storage: consent.analytics ? 'granted' : 'denied', 140 ad_storage: ads, 141 ad_user_data: ads, 142 ad_personalization: ads, 143 personalization_storage: ads 144 }); 145 window.gtag('set', 'ads_data_redaction', !consent.ads); 146 } 147 if (consent.analytics) { 148 loadToplist(); 149 } 150 } 151 152 function saveConsent(analytics, ads) { 153 var consent = {v: 1, analytics: !!analytics, ads: !!ads, ts: Date.now()}; 154 try { 155 localStorage.setItem(CONSENT_KEY, JSON.stringify(consent)); 156 } catch (e) { 157 } 158 window.hhConsent = consent; 159 applyConsent(consent); 160 track('consent_update', {analytics: consent.analytics ? 'granted' : 'denied', ads: consent.ads ? 'granted' : 'denied'}); 161 hideConsentBar(); 162 } 163 164 function showConsentBar(withSettings) { 165 if (!consentBar) { 166 return; 167 } 168 var current = window.hhConsent || {analytics: false, ads: false}; 169 consentBar.querySelector('[data-consent-analytics]').checked = !!current.analytics; 170 consentBar.querySelector('[data-consent-ads]').checked = !!current.ads; 171 toggleConsentSettings(!!withSettings); 172 consentBar.hidden = false; 173 document.body.classList.add('has-consent-bar'); 174 } 175 176 function hideConsentBar() { 177 if (consentBar) { 178 consentBar.hidden = true; 179 document.body.classList.remove('has-consent-bar'); 180 } 181 } 182 183 function toggleConsentSettings(open) { 184 consentBar.querySelector('[data-consent-settings]').hidden = !open; 185 consentBar.querySelector('[data-consent-save]').hidden = !open; 186 consentBar.querySelector('[data-consent-toggle]').hidden = open; 187 } 188 189 if (consentBar) { 190 consentBar.addEventListener('click', function (event) { 191 if (event.target.closest('[data-consent-accept]')) { 192 saveConsent(true, true); 193 } else if (event.target.closest('[data-consent-reject]')) { 194 saveConsent(false, false); 195 } else if (event.target.closest('[data-consent-toggle]')) { 196 toggleConsentSettings(true); 197 } else if (event.target.closest('[data-consent-save]')) { 198 saveConsent(consentBar.querySelector('[data-consent-analytics]').checked, consentBar.querySelector('[data-consent-ads]').checked); 199 } 200 }); 201 if (!window.hhConsent) { 202 showConsentBar(false); 203 } 204 } 205 document.addEventListener('click', function (event) { 206 if (event.target.closest('[data-consent-open]')) { 207 event.preventDefault(); 208 showConsentBar(true); 209 } 210 }); 211 if (window.hhConsent && window.hhConsent.analytics) { 212 loadToplist(); 213 } 214 215 /* ---------- Game detail ---------- */ 216 217 // light YouTube player: the iframe is created after click 218 function playVideo(player, videoId) { 219 player.dataset.videoId = videoId; 220 var iframe = document.createElement('iframe'); 221 // enablejsapi lets GA4 enhanced measurement track video start/progress/complete 222 iframe.src = 'https://www.youtube-nocookie.com/embed/' + encodeURIComponent(videoId) + '?autoplay=1&rel=0&enablejsapi=1&origin=' + encodeURIComponent(location.origin); 223 iframe.title = 'Video'; 224 iframe.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'; 225 iframe.allowFullscreen = true; 226 player.innerHTML = ''; 227 player.appendChild(iframe); 228 } 229 230 document.addEventListener('click', function (event) {
231 var play = event.target.closest('[data-video-play]'); 232 if (play) { 233 var player = play.closest('[data-video-player]'); 234 track('video_play_click', {video_id: player.dataset.videoId}); 235 playVideo(player, player.dataset.videoId); 236 return; 237 } 238 239 var select = event.target.closest('[data-video-select]'); 240 if (select) { 241 var mainPlayer = document.querySelector('[data-video-player]'); 242 if (!mainPlayer) { 243 return; 244 } 245 document.querySelectorAll('[data-video-select]').forEach(function (item) { 246 item.classList.toggle('is-active', item === select); 247 }); 248 playVideo(mainPlayer, select.dataset.videoSelect); 249 mainPlayer.scrollIntoView({behavior: 'smooth', block: 'center'}); 250 } 251 }); 252 253 // gallery lightbox 254 var dialog = document.querySelector('[data-lightbox-dialog]'); 255 if (dialog && typeof dialog.showModal === 'function') { 256 var items = Array.prototype.slice.call(document.querySelectorAll('[data-lightbox]')); 257 var image = dialog.querySelector('[data-lightbox-image]'); 258 var current = 0; 259 260 var show = function (index) { 261 current = (index + items.length) % items.length; 262 image.src = items[current].getAttribute('href'); 263 image.alt = items[current].querySelector('img') ? items[current].querySelector('img').alt : ''; 264 }; 265 266 items.forEach(function (item, index) { 267 item.addEventListener('click', function (event) { 268 event.preventDefault(); 269 show(index); 270 dialog.showModal(); 271 }); 272 }); 273 dialog.querySelector('[data-lightbox-close]').addEventListener('click', function () { dialog.close(); }); 274 dialog.querySelector('[data-lightbox-prev]').addEventListener('click', function () { show(current - 1); }); 275 dialog.querySelector('[data-lightbox-next]').addEventListener('click', function () { show(current + 1); }); 276 dialog.addEventListener('click', function (event) { 277 if (event.target === dialog) { 278 dialog.close(); 279 } 280 }); 281 dialog.addEventListener('keydown', function (event) { 282 if (event.key === 'ArrowLeft') { show(current - 1); } 283 if (event.key === 'ArrowRight') { show(current + 1); } 284 }); 285 } 286 287 // search suggestions (SearchController::suggest) 288 document.querySelectorAll('form.search[data-suggest-url]').forEach(function (form) { 289 var input = form.querySelector('input[name="q"]'); 290 var list = form.querySelector('.search-suggest'); 291 if (!input || !list || !window.fetch) { 292 return; 293 } 294 var cache = {}; 295 var items = []; 296 var active = -1; 297 var timer = null; 298 var lastTerm = ''; 299 300 function close() { 301 list.hidden = true; 302 list.innerHTML = ''; 303 items = []; 304 active = -1; 305 input.setAttribute('aria-expanded', 'false'); 306 input.removeAttribute('aria-activedescendant'); 307 } 308 309 function setActive(index) { 310 var options = list.querySelectorAll('[role="option"]'); 311 if (!options.length) { 312 return; 313 } 314 active = (index + options.length) % options.length; 315 options.forEach(function (option, i) { 316 option.setAttribute('aria-selected', i === active ? 'true' : 'false'); 317 }); 318 input.setAttribute('aria-activedescendant', options[active].id); 319 } 320 321 function scoreClass(rating) { 322 return rating >= 75 ? 'score--high' : (rating >= 50 ? 'score--mid' : 'score--low'); 323 } 324 325 function render(term, data) { 326 if (term !== input.value.trim()) { 327 return; 328 } 329 items = data; 330 active = -1; 331 list.innerHTML = ''; 332 if (!items.length) { 333 close(); 334 return; 335 }
336 items.forEach(function (item, i) { 337 var li = document.createElement('li'); 338 li.id = 'search-suggest-' + i; 339 li.setAttribute('role', 'option'); 340 li.setAttribute('aria-selected', 'false'); 341 var link = document.createElement('a'); 342 link.href = item.url; 343 link.tabIndex = -1; 344 link.dataset.track = 'search_suggest_select'; 345 link.dataset.trackSearchTerm = term; 346 link.dataset.trackPosition = String(i + 1); 347 var name = document.createElement('span'); 348 name.className = 'search-suggest__name'; 349 name.textContent = item.name; 350 var meta = document.createElement('span'); 351 meta.className = 'search-suggest__meta'; 352 var parts = []; 353 if (item.year) { 354 parts.push(item.year); 355 } 356 if (item.reviews > 0) { 357 parts.push(form.dataset.suggestReviews + ': ' + item.reviews); 358 } 359 meta.textContent = parts.join(' · '); 360 var body = document.createElement('span'); 361 body.className = 'search-suggest__body'; 362 body.appendChild(name); 363 body.appendChild(meta); 364 link.appendChild(body); 365 if (item.rating !== null) { 366 var score = document.createElement('span'); 367 score.className = 'score score--sm ' + scoreClass(item.rating); 368 score.textContent = item.rating + '%'; 369 link.appendChild(score); 370 } 371 li.appendChild(link); 372 list.appendChild(li); 373 }); 374 list.hidden = false; 375 input.setAttribute('aria-expanded', 'true'); 376 } 377 378 function load(term) { 379 if (cache[term]) { 380 render(term, cache[term]); 381 return; 382 } 383 fetch(form.dataset.suggestUrl + '?q=' + encodeURIComponent(term), {headers: {Accept: 'application/json'}}) 384 .then(function (response) { return response.ok ? response.json() : {items: []}; }) 385 .then(function (data) { 386 cache[term] = data.items || []; 387 render(term, cache[term]); 388 }) 389 .catch(function () {}); 390 } 391 392 input.addEventListener('input', function () { 393 var term = input.value.trim(); 394 clearTimeout(timer); 395 if (term.length < 2) { 396 lastTerm = ''; 397 close(); 398 return; 399 } 400 if (term === lastTerm) { 401 return; 402 } 403 lastTerm = term; 404 timer = setTimeout(function () { load(term); }, 150); 405 }); 406 407 input.addEventListener('keydown', function (event) { 408 if (list.hidden) { 409 return; 410 } 411 if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { 412 event.preventDefault();
413 setActive(active + (event.key === 'ArrowDown' ? 1 : -1)); 414 } else if (event.key === 'Enter' && active >= 0) { 415 event.preventDefault(); 416 list.querySelectorAll('[role="option"] a')[active].click(); 417 } else if (event.key === 'Escape') { 418 close(); 419 } 420 }); 421 422 // links are clicked before the input loses the focus 423 list.addEventListener('mousedown', function (event) { 424 event.preventDefault(); 425 }); 426 input.addEventListener('blur', close); 427 input.addEventListener('focus', function () { 428 if (input.value.trim().length >= 2 && input.value.trim() === lastTerm && cache[lastTerm]) { 429 render(lastTerm, cache[lastTerm]); 430 } 431 }); 432 }); 433 434 // countdown to the release date 435 document.querySelectorAll('[data-countdown]').forEach(function (element) { 436 var target = new Date(element.dataset.countdown).getTime(); 437 var value = element.querySelector('[data-countdown-value]'); 438 if (!value || isNaN(target)) { 439 return; 440 } 441 var update = function () { 442 var diff = Math.max(0, target - Date.now()); 443 var days = Math.floor(diff / 86400000); 444 var hours = Math.floor(diff % 86400000 / 3600000); 445 var minutes = Math.floor(diff % 3600000 / 60000); 446 value.textContent = days + ' d ' + String(hours).padStart(2, '0') + ' h ' + String(minutes).padStart(2, '0') + ' min'; 447 }; 448 update(); 449 setInterval(update, 30000); 450 }); 451 452 // mini header of the game shown when the hero is not visible 453 var sticky = document.querySelector('[data-game-sticky]'); 454 var hero = document.querySelector('[data-game-hero]'); 455 if (sticky && hero && 'IntersectionObserver' in window) { 456 new IntersectionObserver(function (entries) { 457 var hidden = !entries[0].isIntersecting && entries[0].boundingClientRect.top < 0; 458 sticky.classList.toggle('is-visible', hidden); 459 sticky.setAttribute('aria-hidden', hidden ? 'false' : 'true'); 460 }).observe(hero); 461 } 462})();
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.