1 (function () { 2 var tools = [ 3 { 4 id: "home", 5 label: "Home", 6 short: "HM", 7 icon: "home", 8 kind: "landing", 9 description: "Start from the tool hub.", 10 }, 11 { 12 id: "events", 13 label: "Events", 14 short: "EV", 15 icon: "calendar", 16 category: "tools", 17 path: "events-page/", 18 description: "Track active and upcoming events.", 19 }, 20 { 21 id: "chat", 22 label: "Chat Archive", 23 short: "CA", 24 icon: "message", 25 category: "community", 26 path: "chat/", 27 description: "Search public chat and copy exact player names.", 28 }, 29 { 30 id: "changelog", 31 label: "Changelog", 32 short: "CL", 33 icon: "history", 34 category: "community", 35 path: "changelog/", 36 description: "See major player-facing additions and improvements.", 37 }, 38 { 39 id: "status", 40 label: "Server Status", 41 short: "SS", 42 icon: "gauge", 43 category: "community", 44 externalUrl: "https://status.angelsonline.wiki/", 45 description: "Check Angels Online services and uptime.", 46 }, 47 { 48 id: "ttk", 49 label: "TTK Calculator", 50 navLabel: "TTK", 51 short: "TK", 52 icon: "gauge", 53 category: "tools", 54 path: "ttk-calculator/", 55 description: "Calculate ability damage and time to kill.", 56 }, 57 { 58 id: "exp-orbs", 59 label: "EXP Orb Calculators", 60 navLabel: "EXP Orbs", 61 short: "EO", 62 icon: "orb", 63 category: "tools", 64 path: "exp-calculators/", 65 description: "Plan Character, Skill, and Pet EXP with Standard or Mini orbs.", 66 }, 67 { 68 id: "pet-stars", 69 label: "Pet Star Calculator", 70 navLabel: "Pet Stars", 71 short: "PS", 72 icon: "star", 73 category: "tools", 74 path: "pet-star-calculator/", 75 description: "Forecast the first 3-star level from current pet attributes.", 76 }, 77 { 78 id: "maps", 79 label: "Maps", 80 short: "MP", 81 icon: "map", 82 category: "game-data", 83 path: "maps/", 84 description: "Browse areas, monsters, NPCs, resources, shops, and quests.", 85 }, 86 { 87 id: "interactive-maps", 88 label: "Interactive Maps", 89 short: "IM", 90 icon: "map", 91 category: "game-data", 92 path: "interactive-maps/", 93 description: "Explore monster, resource, NPC, shop, and quest locations.", 94 }, 95 { 96 id: "stalls", 97 label: "Player Shops", 98 short: "ST", 99 icon: "store", 100 category: "game-data", 101 path: "stalls/", 102 description: "Browse current player shops, items, and prices by server.", 103 }, 104 { 105 id: "bestiary", 106 label: "Bestiary", 107 short: "BE", 108 icon: "skull", 109 category: "game-data", 110 path: "bestiary/", 111 description: "Search monster stats and spawn areas.", 112 }, 113 { 114 id: "resources", 115 label: "Resources", 116 short: "RS", 117 icon: "gem", 118 category: "game-data", 119 path: "resources/", 120 description: "Search gathering nodes, yields, skills, and maps.", 121 }, 122 { 123 id: "pets", 124 label: "Pets", 125 short: "PT", 126 icon: "paw", 127 category: "game-data", 128 path: "pets/", 129 description: "Review pet sources, evolutions, skills, and stats.", 130 }, 131 { 132 id: "pd-preview", 133 label: "PD Preview", 134 short: "PD", 135 icon: "shirt", 136 category: "game-data", 137 path: "pd-preview/", 138 description: "Preview verified Angels Online paper-doll fashion on a customizable character.", 139 }, 140 { 141 id: "skills", 142 label: "Skills", 143 short: "SK", 144 icon: "orb", 145 category: "game-data", 146 path: "skills/", 147 description: "Search player, pet, monster, item, and internal skill definitions.", 148 }, 149 { 150 id: "items", 151 label: "Items", 152 short: "IT", 153 icon: "package", 154 category: "game-data", 155 path: "items/", 156 description: "Search client item definitions, icons, stats, and references.", 157 }, 158 { 159 id: "rides", 160 label: "Rides", 161 short: "RD", 162 icon: "rocket", 163 category: "game-data", 164 path: "rides/", 165 description: "Search rides, mounts, mechs, and robots.", 166 }, 167 { 168 id: "wiki", 169 label: "Wiki", 170 short: "WK", 171 icon: "book", 172 category: "community", 173 externalUrl: "https://wiki.angelsonline.wiki/", 174 description: "Open the Angels Online wiki.", 175 }, 176 { 177 id: "play-angels-online", 178 label: "Play Angels Online", 179 short: "PAO", 180 icon: "gamepad", 181 externalUrl: "https://aog.userjoy.com/", 182 description: "Visit the Angels Online official website to download the game client.", 183 }, 184 { 185 id: "better-angels", 186 label: "Better Angels", 187 short: "BA", 188 icon: "rocket", 189 category: "community", 190 externalUrl: "https://betterangels.app/", 191 description: "Open the Better Angels companion app.", 192 }//, 193 // { 194 // id: "producers", 195 // label: "TheFactory", 196 // short: "PR", 197 // path: "producers/", 198 // description: "Plan recipes, materials, orders, gathering, shops, and craft time.", 199 // } 200 ]; 201 var navGroups = [ 202 { id: "tools", label: "Tools" }, 203 { id: "game-data", label: "Game Data" }, 204 { id: "community", label: "Community" }, 205 ]; 206 207 var root = document.documentElement; 208 var nav = document.getElementById("toolNav"); 209 var sidebarActions = document.getElementById("sidebarActions"); 210 var mobileActionsToggle = document.getElementById("mobileActionsToggle"); 211 var sidebarActionsPanel = document.getElementById("sidebarActionsPanel"); 212 var loading = document.getElementById("loading"); 213 var frameWrap = document.querySelector(".frameWrap"); 214 var landingView = document.getElementById("landingView"); 215 var landingActions = document.getElementById("landingActions"); 216 var gameVersionBadge = document.getElementById("gameVersionBadge"); 217 var gameVersionValue = document.getElementById("gameVersionValue"); 218 var creditsButton = document.getElementById("creditsButton"); 219 var creditsOverlay = document.getElementById("creditsOverlay"); 220 var creditsPanel = document.getElementById("creditsPanel"); 221 var creditsClose = document.getElementById("creditsClose"); 222 var activeTool = null; 223 var framesById = {}; 224 var loadedFrames = {}; 225 var shellRevision = Date.now().toString(36); 226 var eventAlertAudioContext = null; 227 var shellTitleResetTimer = null; 228 var eventAlertSettingsKey = "aogEventAlertSettings"; 229 var changelogSeenStorageKey = "aogChangelogSeen"; 230 var changelogLatestRelease = ""; 231 232 function iconSvg(name, className) { 233 var paths = { 234 home: '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9Z"></path><path d="M9 22V12h6v10"></path>', 235 calendar: '<path d="M8 2v4M16 2v4M3 10h18"></path><rect x="3" y="4" width="18" height="18" rx="2"></rect><path d="M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01"></path>', 236 gauge: '<path d="m12 14 4-4"></path><path d="M3.34 19a10 10 0 1 1 17.32 0"></path>', 237 orb: '<circle cx="12" cy="12" r="9"></circle><path d="M8 9a5 5 0 0 1 6-3M7 15h.01M17 15h.01"></path>', 238 star: '<path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-2.9-5.6 2.9 1.1-6.2L3 9.6l6.2-.9L12 3Z"></path>', 239 map: '<path d="m3 6 6-3 6 3 6-3v15l-6 3-6-3-6 3V6Z"></path><path d="M9 3v15M15 6v15"></path>', 240 store: '<path d="M3 10h18M5 10v10h14V10M4 4h16l1 6H3l1-6Z"></path><path d="M8 14h3v6M15 14h.01"></path>', 241 skull: '<circle cx="9" cy="12" r="1"></circle><circle cx="15" cy="12" r="1"></circle><path d="M8 20v2h8v-2M12.5 17l-.5-1-.5 1h1Z"></path><path d="M16 20a8 8 0 1 0-8 0"></path>', 242 gem: '<path d="M6 3h12l4 6-10 13L2 9l4-6Z"></path><path d="m11 3-3 6 4 13 4-13-3-6M2 9h20"></path>', 243 paw: '<circle cx="11" cy="4" r="2"></circle><circle cx="18" cy="8" r="2"></circle><circle cx="4" cy="8" r="2"></circle><path d="M8.5 14.5c-1.5 1.5-2 3.5-.5 5s3.5 1 4 0c.5 1 2.5 1.5 4 0s1-3.5-.5-5c-1.2-1.2-2-2.5-3.5-2.5s-2.3 1.3-3.5 2.5Z"></path>', 244 package: '<path d="m7.5 4.21 9 5.15M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8Z"></path><path d="m3.3 7 8.7 5 8.7-5M12 22V12"></path>', 245 shirt: '<path d="m15 3 6 3-3 5-3-2v12H9V9l-3 2-3-5 6-3a3 3 0 0 0 6 0Z"></path>', 246 rocket: '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09Z"></path><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.87 12.87 0 0 1 22 2c0 2.72-.78 7.5-6.05 11a22.35 22.35 0 0 1-3.95 2Z"></path><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"></path><circle cx="16" cy="8" r="1"></circle>', 247 gamepad: '<path d="M6 7h12a4 4 0 0 1 4 4v5a3 3 0 0 1-5.12 2.12L15.76 17H8.24l-1.12 1.12A3 3 0 0 1 2 16v-5a4 4 0 0 1 4-4Z"></path><path d="M7 12h4M9 10v4M16 11h.01M18 13h.01"></path>', 248 book: '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2V3ZM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7V3Z"></path>', 249 message: '<path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4v8Z"></path><path d="M8 9h8M8 13h5"></path>', 250 history: '<path d="M3 12a9 9 0 1 0 3-6.7L3 8"></path><path d="M3 3v5h5M12 7v5l4 2"></path>', 251 sun: '<circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.42 1.42M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.42"></path>', 252 moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>', 253 }; 254 var classAttr = className ? ' class="' + className + '"' : ""; 255 return '<svg' + classAttr + ' viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + (paths[name] || paths.home) + '</svg>'; 256 } 257 258 function findTool(id) { 259 return tools.find(function (tool) { 260 return tool.id === id && !tool.externalUrl; 261 }) || tools.find(function (tool) { 262 return tool.id === "home"; 263 }) || tools.find(function (tool) { 264 return tool.path; 265 }) || tools[0]; 266 } 267 268 function localizedPhrase(value) { 269 return window.AOG_I18N && typeof window.AOG_I18N.phrase === "function" 270 ? window.AOG_I18N.phrase(value) 271 : value; 272 } 273 274 function navNewBadgeMarkup(scope) { 275 return '<span class="navNewBadge" data-changelog-new="' 276 + scope 277 + '" hidden>' 278 + localizedPhrase("New") 279 + '</span>'; 280 } 281 282 function readChangelogSeenRelease() { 283 try { 284 return localStorage.getItem(changelogSeenStorageKey) || ""; 285 } catch (_error) { 286 return null; 287 } 288 } 289 290 function validChangelogReleaseDate(value) { 291 if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
292 var parts = value.split("-").map(Number); 293 var date = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2])); 294 return date.getUTCFullYear() === parts[0] 295 && date.getUTCMonth() === parts[1] - 1 296 && date.getUTCDate() === parts[2]; 297 } 298 299 function latestChangelogRelease(markdown) { 300 if (typeof markdown !== "string") return ""; 301 var firstReleaseHeading = markdown 302 .replace(/\r\n?/g, "\n") 303 .split("\n") 304 .find(function (line) { return /^##(?:\s|$)/.test(line); }); 305 if (!firstReleaseHeading) return ""; 306 var match = firstReleaseHeading.match( 307 /^##[ \t]+(\d{4}-\d{2}-\d{2})[ \t]+â[ \t]+.+?\s*$/ 308 ); 309 return match && validChangelogReleaseDate(match[1]) 310 ? match[1] 311 : ""; 312 } 313 314 function updateChangelogBadges() { 315 var seenRelease = readChangelogSeenRelease(); 316 var unread = Boolean( 317 changelogLatestRelease 318 && seenRelease !== null 319 && seenRelease !== changelogLatestRelease 320 ); 321 document.querySelectorAll("[data-changelog-new]").forEach(function (badge) { 322 badge.hidden = !unread; 323 }); 324 document.querySelectorAll('[data-tool="changelog"]').forEach(function (button) { 325 button.setAttribute( 326 "aria-label", 327 localizedPhrase("Changelog") 328 + (unread ? " â " + localizedPhrase("New") : "") 329 ); 330 }); 331 } 332 333 function loadChangelogLatest() { 334 changelogLatestRelease = ""; 335 updateChangelogBadges(); 336 if ( 337 typeof window.fetch !== "function" 338 || readChangelogSeenRelease() === null 339 ) { 340 return; 341 } 342 var sourceUrl = new URL("changelog/CHANGELOG.md", window.location.href); 343 sourceUrl.searchParams.set("_shell", shellRevision); 344 window.fetch(sourceUrl.href, { cache: "no-store" }) 345 .then(function (response) { 346 if (!response.ok) throw new Error("Unable to load changelog"); 347 return response.text(); 348 }) 349 .then(function (markdown) { 350 changelogLatestRelease = latestChangelogRelease(markdown); 351 updateChangelogBadges(); 352 }) 353 .catch(function () { 354 changelogLatestRelease = ""; 355 updateChangelogBadges(); 356 }); 357 } 358 359 function markChangelogSeen(releaseId) { 360 if ( 361 !/^\d{4}-\d{2}-\d{2}$/.test(releaseId) 362 || releaseId !== changelogLatestRelease 363 ) { 364 return; 365 } 366 try { 367 localStorage.setItem(changelogSeenStorageKey, releaseId); 368 } catch (_error) { 369 return; 370 } 371 updateChangelogBadges(); 372 } 373 374 function hideGameVersion() { 375 gameVersionValue.textContent = ""; 376 gameVersionBadge.hidden = true; 377 } 378 379 function loadGameVersion() { 380 hideGameVersion(); 381 if (typeof window.fetch !== "function") return; 382 383 var versionUrl = new URL("data/client_version.json", window.location.href); 384 versionUrl.searchParams.set("_shell", shellRevision); 385 window.fetch(versionUrl.href, { cache: "no-store" }) 386 .then(function (response) { 387 if (!response.ok) throw new Error("Unable to load game version"); 388 return response.json(); 389 }) 390 .then(function (payload) { 391 if ( 392 !payload 393 || payload.schemaVersion !== 1 394 || typeof payload.gameVersion !== "string" 395 || !/^[0-9]+(?:\.[0-9]+)+$/.test(payload.gameVersion) 396 ) { 397 hideGameVersion(); 398 return; 399 } 400 gameVersionValue.textContent = payload.gameVersion; 401 gameVersionBadge.hidden = false;
402 }) 403 .catch(hideGameVersion); 404 } 405 406 function routeState() { 407 var raw = (window.location.hash || "").replace(/^#\/?/, ""); 408 if (!raw) return { id: tools[0].id, childHash: "" }; 409 var splitAt = raw.indexOf("?"); 410 var route = splitAt === -1 ? { 411 id: raw, 412 childHash: "", 413 } : { 414 id: raw.slice(0, splitAt) || tools[0].id, 415 childHash: raw.slice(splitAt + 1), 416 }; 417 if (route.id === "skill-exp") { 418 return { id: "exp-orbs", childHash: "skill", legacy: true }; 419 } 420 return route; 421 } 422 423 function routeId() { 424 return routeState().id; 425 } 426 427 function childHashFromFrame(frame) { 428 try { 429 return (frame.contentWindow.location.hash || "").replace(/^#/, ""); 430 } catch (_error) { 431 return ""; 432 } 433 } 434 435 function childHashForTool(tool) { 436 var frame = framesById[tool.id]; 437 return frame ? childHashFromFrame(frame) : ""; 438 } 439 440 function toolHref(tool, childHash) { 441 if (!tool.path) return "#home"; 442 var url = new URL(tool.path, window.location.href); 443 var language = window.AOG_I18N ? window.AOG_I18N.locale : "en"; 444 if (language && language !== "en") { 445 url.searchParams.set("lang", language); 446 } else { 447 url.searchParams.delete("lang"); 448 } 449 url.searchParams.set("_shell", shellRevision); 450 url.hash = childHash || ""; 451 return url.href; 452 } 453 454 function framePathMatchesTool(frame, tool) { 455 if (!tool.path) return false; 456 try { 457 var frameSrc = frame.getAttribute("src"); 458 if (!frameSrc) return false; 459 var frameUrl = new URL(frameSrc, window.location.href); 460 var toolUrl = new URL(tool.path, window.location.href); 461 return frameUrl.origin === toolUrl.origin 462 && frameUrl.pathname === toolUrl.pathname; 463 } catch (_error) { 464 return false; 465 } 466 } 467 468 function shellUrl(toolId, childHash) { 469 var hash = "#" + toolId + (childHash ? "?" + childHash : ""); 470 return window.location.pathname + window.location.search + hash; 471 } 472 473 function syncShellUrl(tool, childHash, replace) { 474 var next = shellUrl(tool.id, childHash); 475 if (next === window.location.pathname + window.location.search + window.location.hash) { 476 return; 477 } 478 history[replace ? "replaceState" : "pushState"](null, "", next); 479 } 480 481 function openCredits() { 482 creditsOverlay.hidden = false; 483 creditsPanel.focus(); 484 } 485 486 function closeCredits() { 487 creditsOverlay.hidden = true; 488 creditsButton.focus(); 489 } 490 491 function syncFrameTheme(frame, theme) { 492 try { 493 if (frame.contentDocument) { 494 frame.contentDocument.documentElement.dataset.theme = theme || root.dataset.theme || "light"; 495 } 496 } catch (_error) {} 497 } 498 499 function syncAllFrameThemes(theme) {
500 Object.keys(framesById).forEach(function (id) { 501 syncFrameTheme(framesById[id], theme); 502 }); 503 } 504 505 function setShellThemeLabel(theme) { 506 var isDark = theme === "dark"; 507 document.querySelectorAll("[data-theme-toggle]").forEach(function (button) { 508 button.setAttribute("aria-pressed", String(isDark)); 509 var label = button.querySelector("[data-theme-label]"); 510 var icon = button.querySelector("[data-theme-icon]"); 511 if (label) { 512 label.textContent = window.AOG_I18N 513 ? window.AOG_I18N.t(isDark ? "common.lightMode" : "common.darkMode") 514 : (isDark ? "Light mode" : "Dark mode"); 515 } 516 if (icon) icon.innerHTML = iconSvg(isDark ? "sun" : "moon", "appIcon"); 517 }); 518 } 519 520 function bindChildThemeEvents(frame) { 521 try { 522 if (frame.dataset.themeEventBound === "1") return; 523 frame.dataset.themeEventBound = "1"; 524 frame.contentWindow.addEventListener("aog-theme-change", function (event) { 525 var theme = event.detail && event.detail.theme; 526 if (!theme) return; 527 root.dataset.theme = theme; 528 setShellThemeLabel(theme); 529 syncAllFrameThemes(theme); 530 }); 531 } catch (_error) {} 532 } 533 534 function bindChildRouteEvents(frame, tool) { 535 try { 536 if (frame.dataset.routeEventBound === "1") return; 537 frame.dataset.routeEventBound = "1"; 538 frame.contentWindow.addEventListener("hashchange", function () { 539 if (!activeTool || activeTool.id !== tool.id) return; 540 var childHash = childHashFromFrame(frame); 541 syncShellUrl(tool, childHash, true); 542 }); 543 } catch (_error) {} 544 } 545 546 function bindChildNavigationDismissal(frame) { 547 try { 548 var childDocument = frame.contentDocument; 549 if (!childDocument) return; 550 var dismissNavigation = function () { 551 closeNavigationDisclosures(); 552 }; 553 childDocument.addEventListener( 554 "pointerdown", 555 dismissNavigation, 556 true 557 ); 558 childDocument.addEventListener( 559 "focusin", 560 dismissNavigation, 561 true 562 ); 563 } catch (_error) {} 564 } 565 566 function frameForTool(tool) { 567 if (!tool.path) return null; 568 if (framesById[tool.id]) return framesById[tool.id]; 569 570 var frame = document.createElement("iframe"); 571 frame.title = localizedPhrase(tool.label) 572 + " - " 573 + localizedPhrase("Angels Online Tools"); 574 frame.dataset.toolFrame = tool.id; 575 frame.className = "inactive"; 576 frame.setAttribute("aria-hidden", "true"); 577 frame.addEventListener("load", function () { 578 loadedFrames[tool.id] = true; 579 if (activeTool && activeTool.id === tool.id) loading.hidden = true; 580 syncFrameTheme(frame, root.dataset.theme); 581 bindChildThemeEvents(frame); 582 bindChildRouteEvents(frame, tool); 583 bindChildNavigationDismissal(frame); 584 }); 585 frameWrap.appendChild(frame); 586 framesById[tool.id] = frame; 587 return frame; 588 } 589 590 function readEventAlertSettings() { 591 try { 592 var stored = JSON.parse(localStorage.getItem(eventAlertSettingsKey) || "{}"); 593 return { 594 enabled: stored.enabled === true, 595 sound: stored.sound === true, 596 }; 597 } catch (_error) { 598 return { enabled: false, sound: false }; 599 } 600 } 601
602 function ensureBackgroundEventAlerts() { 603 if (!readEventAlertSettings().enabled) return; 604 var eventTool = tools.find(function (tool) { 605 return tool.id === "events" && tool.path; 606 }); 607 if (!eventTool) return; 608 609 var frame = frameForTool(eventTool); 610 if (!frame.getAttribute("src")) { 611 frame.src = toolHref(eventTool, ""); 612 } 613 } 614 615 function ensureEventAlertAudioContext() { 616 var AudioContextClass = window.AudioContext || window.webkitAudioContext; 617 if (!AudioContextClass) return null; 618 if (!eventAlertAudioContext) { 619 eventAlertAudioContext = new AudioContextClass(); 620 } 621 if (eventAlertAudioContext.state === "suspended") { 622 eventAlertAudioContext.resume().catch(function () {}); 623 } 624 return eventAlertAudioContext; 625 } 626 627 function unlockEventAlertAudio() { 628 if (!readEventAlertSettings().sound) return; 629 ensureEventAlertAudioContext(); 630 } 631 632 function playEventAlertSound() { 633 var context = ensureEventAlertAudioContext(); 634 if (!context) return; 635 636 try { 637 var oscillator = context.createOscillator(); 638 var gain = context.createGain(); 639 var now = context.currentTime; 640 oscillator.type = "sine"; 641 oscillator.frequency.setValueAtTime(880, now); 642 oscillator.frequency.setValueAtTime(660, now + 0.16); 643 gain.gain.setValueAtTime(0.0001, now); 644 gain.gain.exponentialRampToValueAtTime(0.12, now + 0.03); 645 gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.42); 646 oscillator.connect(gain); 647 gain.connect(context.destination); 648 oscillator.start(now); 649 oscillator.stop(now + 0.45); 650 } catch (_error) {} 651 } 652 653 function currentShellTitle() { 654 return !activeTool || activeTool.kind === "landing" 655 ? "Angels Online Tools" 656 : activeTool.label + " - Angels Online Tools"; 657 } 658 659 function pulseShellTitle(title) { 660 document.title = "\uD83D\uDD14 " + title; 661 window.clearTimeout(shellTitleResetTimer); 662 shellTitleResetTimer = window.setTimeout(function () { 663 document.title = currentShellTitle(); 664 }, 12000); 665 } 666 667 function showShellEventAlert(title, message, eventType) { 668 var stack = document.getElementById("shellAlertStack"); 669 if (!stack) return; 670 671 var alert = document.createElement("article"); 672 alert.className = "shellEventAlert"; 673 alert.dataset.eventType = ["league", "holy", "knowledge"].includes(eventType) 674 ? eventType 675 : "event"; 676 alert.setAttribute("role", "status"); 677 678 var copy = document.createElement("div"); 679 var heading = document.createElement("strong"); 680 var body = document.createElement("p"); 681 heading.textContent = title; 682 body.textContent = message; 683 copy.appendChild(heading); 684 copy.appendChild(body); 685 686 var close = document.createElement("button"); 687 close.className = "shellEventAlertClose"; 688 close.type = "button"; 689 close.setAttribute("aria-label", "Dismiss event alert"); 690 close.textContent = "\u00D7"; 691 692 function removeAlert() { 693 alert.remove(); 694 } 695 696 close.addEventListener("click", removeAlert); 697 alert.appendChild(copy); 698 alert.appendChild(close); 699 stack.appendChild(alert); 700 window.setTimeout(removeAlert, 12000); 701 } 702 703 function handleEventAlertMessage(event) { 704 var eventFrame = framesById.events; 705 if ( 706 event.origin !== window.location.origin 707 || !eventFrame 708 || event.source !== eventFrame.contentWindow 709 ) { 710 return; 711 } 712 713 var data = event.data || {}; 714 if (data.type !== "aog-event-alert") return; 715 716 var title = String(data.title || "").trim().slice(0, 160); 717 var message = String(data.message || "").trim().slice(0, 600); 718 if (!title || !message) return; 719 720 showShellEventAlert(title, message, String(data.eventType || "")); 721 if (data.sound === true) playEventAlertSound(); 722 pulseShellTitle(title); 723 } 724 725 function navChevronSvg() { 726 return [
727 '<svg class="navChevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" ', 728 'stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">', 729 '<path d="m6 9 6 6 6-6"></path>', 730 '</svg>', 731 ].join(""); 732 } 733 734 function navToolMarkup(tool) { 735 var unreadBadge = tool.id === "changelog" 736 ? navNewBadgeMarkup("tool") 737 : ""; 738 if (tool.externalUrl) { 739 return [ 740 '<a class="toolButton" data-nav-link href="', tool.externalUrl, '" target="_blank" rel="noopener noreferrer" ', 741 'aria-label="', tool.label, '">', 742 '<span class="toolIcon" aria-hidden="true">', iconSvg(tool.icon), '</span>', 743 '<span>', 744 '<span class="toolName">', tool.navLabel || tool.label, unreadBadge, '</span>', 745 '<span class="toolDescription">', tool.description, '</span>', 746 '</span>', 747 '</a>', 748 ].join(""); 749 } 750 751 return [ 752 '<button class="toolButton" type="button" data-nav-link data-tool="', tool.id, '" ', 753 'aria-label="', tool.label, '">', 754 '<span class="toolIcon" aria-hidden="true">', iconSvg(tool.icon), '</span>', 755 '<span>', 756 '<span class="toolName">', tool.navLabel || tool.label, unreadBadge, '</span>', 757 '<span class="toolDescription">', tool.description, '</span>', 758 '</span>', 759 '</button>', 760 ].join(""); 761 } 762 763 function closeNavGroups(restoreFocus) { 764 var focusTarget = null; 765 nav.querySelectorAll(".navGroup.open").forEach(function (group) { 766 var toggle = group.querySelector("[data-nav-group-toggle]"); 767 group.classList.remove("open"); 768 if (toggle) { 769 toggle.setAttribute("aria-expanded", "false"); 770 if (!focusTarget) focusTarget = toggle; 771 } 772 }); 773 if (restoreFocus && focusTarget) focusTarget.focus(); 774 } 775 776 function toggleNavGroup(toggle) {
777 var group = toggle.closest(".navGroup"); 778 if (!group) return; 779 var shouldOpen = !group.classList.contains("open"); 780 closeNavGroups(false); 781 if (!shouldOpen) return; 782 group.classList.add("open"); 783 toggle.setAttribute("aria-expanded", "true"); 784 } 785 786 function setMobileNavOpen(open, restoreFocus) { 787 var toggle = document.getElementById("mobileNavToggle"); 788 if (!toggle) return; 789 nav.classList.toggle("mobile-open", open); 790 toggle.setAttribute("aria-expanded", String(open)); 791 if (open) { 792 setMobileActionsOpen(false, false); 793 closeNavGroups(false); 794 } else if (restoreFocus) { 795 toggle.focus(); 796 } 797 } 798 799 function setMobileActionsOpen(open, restoreFocus) { 800 sidebarActions.classList.toggle("mobile-open", open); 801 mobileActionsToggle.setAttribute("aria-expanded", String(open)); 802 if (open) { 803 setMobileNavOpen(false, false); 804 closeNavGroups(false); 805 } else if (restoreFocus) { 806 mobileActionsToggle.focus(); 807 } 808 } 809 810 function closeNavigationDisclosures() { 811 closeNavGroups(false); 812 setMobileNavOpen(false, false); 813 setMobileActionsOpen(false, false); 814 } 815 816 function renderNav() { 817 var homeTool = tools.find(function (tool) { 818 return tool.id === "home"; 819 }); 820 var topLevelTools = tools.filter(function (tool) { 821 return tool !== homeTool && !tool.category; 822 }); 823 var groupsMarkup = navGroups.map(function (group) { 824 var groupTools = tools.filter(function (tool) { 825 return tool.category === group.id; 826 }); 827 return [ 828 '<div class="navGroup" data-nav-group="', group.id, '">', 829 '<button class="navGroupToggle" type="button" data-nav-group-toggle="', group.id, '" ', 830 'aria-expanded="false" aria-controls="navGroupPanel-', group.id, '">', 831 '<span class="toolName">', group.label, 832 group.id === "community" ? navNewBadgeMarkup("group") : "", 833 '</span>', 834 navChevronSvg(), 835 '</button>', 836 '<div class="mobileNavGroupLabel" data-nav-group-label="', group.id, '" role="heading" aria-level="2">', 837 group.label, 838 group.id === "community" ? navNewBadgeMarkup("mobile-group") : "", 839 '</div>', 840 '<div class="navGroupPanel" id="navGroupPanel-', group.id, '" aria-label="', group.label, '">', 841 groupTools.map(navToolMarkup).join(""), 842 '</div>', 843 '</div>', 844 ].join(""); 845 }).join(""); 846 847 nav.innerHTML = [ 848 '<button class="mobileNavToggle" id="mobileNavToggle" type="button" ', 849 'aria-expanded="false" aria-controls="navContents">', 850 '<svg class="appIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ', 851 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">', 852 '<rect x="3" y="3" width="7" height="7" rx="1"></rect>', 853 '<rect x="14" y="3" width="7" height="7" rx="1"></rect>', 854 '<rect x="3" y="14" width="7" height="7" rx="1"></rect>', 855 '<rect x="14" y="14" width="7" height="7" rx="1"></rect>', 856 '</svg>', 857 '<span>Browse tools</span>', 858 navNewBadgeMarkup("browse"), 859 navChevronSvg(), 860 '</button>', 861 '<div class="navContents" id="navContents">', 862 homeTool ? navToolMarkup(homeTool) : "", 863 groupsMarkup, 864 topLevelTools.map(navToolMarkup).join(""), 865 '</div>', 866 ].join(""); 867 868 nav.addEventListener("click", function (event) { 869 var mobileToggle = event.target.closest("#mobileNavToggle"); 870 if (mobileToggle) { 871 setMobileNavOpen(!nav.classList.contains("mobile-open"), false); 872 return; 873 } 874 875 var groupToggle = event.target.closest("[data-nav-group-toggle]"); 876 if (groupToggle) { 877 toggleNavGroup(groupToggle); 878 return; 879 } 880 881 var navLink = event.target.closest("[data-nav-link]"); 882 if (!navLink) return; 883 if (navLink.dataset.tool) { 884 var tool = findTool(navLink.dataset.tool); 885 syncShellUrl(tool, childHashForTool(tool), false); 886 loadRoute(); 887 } 888 closeNavigationDisclosures(); 889 }); 890 891 nav.addEventListener("keydown", function (event) { 892 var toggle = event.target.closest("[data-nav-group-toggle]"); 893 if (!toggle || (event.key !== "ArrowDown" && event.key !== "ArrowUp")) return; 894 event.preventDefault();
895 if (!toggle.closest(".navGroup").classList.contains("open")) { 896 toggleNavGroup(toggle); 897 } 898 var panel = document.getElementById(toggle.getAttribute("aria-controls")); 899 if (!panel) return; 900 var links = panel.querySelectorAll("a[href], button:not(:disabled)"); 901 if (!links.length) return; 902 links[event.key === "ArrowUp" ? links.length - 1 : 0].focus(); 903 }); 904 905 mobileActionsToggle.addEventListener("click", function () { 906 setMobileActionsOpen(!sidebarActions.classList.contains("mobile-open"), false); 907 }); 908 909 sidebarActionsPanel.addEventListener("click", function (event) { 910 if (event.target.closest("button, a[href]")) { 911 setMobileActionsOpen(false, false); 912 } 913 }); 914 915 document.addEventListener("pointerdown", function (event) { 916 if (!nav.contains(event.target)) { 917 closeNavGroups(false); 918 setMobileNavOpen(false, false); 919 } 920 if (!sidebarActions.contains(event.target)) { 921 setMobileActionsOpen(false, false); 922 } 923 }); 924 925 document.addEventListener("focusin", function (event) { 926 var openGroup = nav.querySelector(".navGroup.open"); 927 if (openGroup && !openGroup.contains(event.target)) { 928 closeNavGroups(false); 929 } 930 }); 931 932 document.addEventListener("keydown", function (event) { 933 if (event.key !== "Escape") return; 934 if (sidebarActions.classList.contains("mobile-open")) { 935 event.preventDefault(); 936 setMobileActionsOpen(false, true); 937 return; 938 } 939 if (nav.classList.contains("mobile-open")) { 940 event.preventDefault(); 941 setMobileNavOpen(false, true); 942 return; 943 } 944 if (nav.querySelector(".navGroup.open")) { 945 event.preventDefault(); 946 closeNavGroups(true); 947 } 948 }); 949 950 var mobileShellMedia = window.matchMedia("(max-width: 820px)"); 951 function resetNavigationForViewport() { 952 if (mobileShellMedia.matches) { 953 closeNavGroups(false); 954 } else { 955 setMobileNavOpen(false, false); 956 setMobileActionsOpen(false, false); 957 } 958 } 959 if (typeof mobileShellMedia.addEventListener === "function") { 960 mobileShellMedia.addEventListener("change", resetNavigationForViewport); 961 } else { 962 mobileShellMedia.addListener(resetNavigationForViewport); 963 } 964 resetNavigationForViewport(); 965 } 966 967 function arrowSvg() { 968 return [ 969 '<svg class="landingCardArrow" viewBox="0 0 24 24" aria-hidden="true">', 970 '<path d="M5 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />', 971 '</svg>', 972 ].join(""); 973 } 974 975 function renderLanding() { 976 landingActions.innerHTML = tools.filter(function (tool) { 977 return tool.path; 978 }).map(function (tool) {
979 var unreadBadge = tool.id === "changelog" 980 ? navNewBadgeMarkup("landing") 981 : ""; 982 return [ 983 '<button class="landingCard" type="button" data-launch-tool="', tool.id, '">', 984 '<span class="landingCardIcon" aria-hidden="true">', iconSvg(tool.icon), '</span>', 985 '<span class="landingCardText">', 986 '<span class="landingCardName">', tool.label, unreadBadge, '</span>', 987 '<span class="landingCardDescription">', tool.description, '</span>', 988 '</span>', 989 arrowSvg(), 990 '</button>', 991 ].join(""); 992 }).join(""); 993 994 document.querySelectorAll("[data-launch-tool]").forEach(function (button) { 995 if (button.dataset.launchBound === "1") return; 996 button.dataset.launchBound = "1"; 997 button.addEventListener("click", function () { 998 var tool = findTool(button.dataset.launchTool); 999 syncShellUrl(tool, childHashForTool(tool), false); 1000 loadRoute(); 1001 }); 1002 }); 1003 } 1004 1005 function setActive(tool, childHash) { 1006 activeTool = tool; 1007 document.title = tool.kind === "landing" 1008 ? localizedPhrase("Angels Online Tools") 1009 : localizedPhrase(tool.label) 1010 + " - " 1011 + localizedPhrase("Angels Online Tools"); 1012 document.querySelectorAll("[data-tool]").forEach(function (button) { 1013 var isActive = button.dataset.tool === tool.id; 1014 button.classList.toggle("active", isActive); 1015 if (isActive) { 1016 button.setAttribute("aria-current", "page"); 1017 } else { 1018 button.removeAttribute("aria-current"); 1019 } 1020 }); 1021 document.querySelectorAll("[data-nav-group-toggle]").forEach(function (button) { 1022 button.classList.toggle("active", button.dataset.navGroupToggle === tool.category); 1023 }); 1024 document.querySelectorAll("[data-nav-group-label]").forEach(function (label) { 1025 label.classList.toggle("active", label.dataset.navGroupLabel === tool.category); 1026 }); 1027 } 1028 1029 function loadRoute() { 1030 closeNavigationDisclosures(); 1031 var route = routeState(); 1032 if (route.legacy) { 1033 history.replaceState(null, "", shellUrl(route.id, route.childHash)); 1034 } 1035 var tool = findTool(routeId()); 1036 var childHash = tool.id === route.id ? route.childHash : "";
1037 setActive(tool, childHash); 1038 1039 if (tool.kind === "landing") { 1040 landingView.hidden = false; 1041 loading.hidden = true; 1042 Object.keys(framesById).forEach(function (id) { 1043 framesById[id].classList.add("inactive"); 1044 framesById[id].setAttribute("aria-hidden", "true"); 1045 }); 1046 return; 1047 } 1048 1049 landingView.hidden = true; 1050 var frame = frameForTool(tool); 1051 if (!frame) return; 1052 1053 Object.keys(framesById).forEach(function (id) { 1054 var isActiveFrame = id === tool.id; 1055 framesById[id].classList.toggle("inactive", !isActiveFrame); 1056 framesById[id].setAttribute("aria-hidden", String(!isActiveFrame)); 1057 }); 1058 1059 if (!loadedFrames[tool.id]) { 1060 loading.hidden = false; 1061 } else { 1062 loading.hidden = true; 1063 } 1064 1065 var nextSrc = toolHref(tool, childHash); 1066 if (!framePathMatchesTool(frame, tool)) { 1067 frame.src = nextSrc; 1068 } else if (childHashFromFrame(frame) !== childHash) { 1069 try { 1070 frame.contentWindow.location.hash = childHash; 1071 } catch (_error) { 1072 frame.src = nextSrc; 1073 } 1074 } 1075 } 1076 1077 window.addEventListener("hashchange", loadRoute); 1078 window.addEventListener("popstate", loadRoute); 1079 window.addEventListener("message", function (event) { 1080 var data = event.data || {}; 1081 if (!data || event.origin !== window.location.origin) return; 1082 var activeFrame = activeTool 1083 ? framesById[activeTool.id] 1084 : null; 1085 if ( 1086 !activeFrame 1087 || event.source !== activeFrame.contentWindow 1088 ) { 1089 return; 1090 } 1091 if (data.type === "aog-changelog-seen") { 1092 if (activeTool && activeTool.id === "changelog") { 1093 markChangelogSeen(String(data.releaseId || "")); 1094 } 1095 return; 1096 } 1097 if (data.type === "aog-open-tool") { 1098 if ( 1099 data.tool !== "interactive-maps" 1100 || typeof data.hash !== "string" 1101 || data.hash.length > 16384 1102 ) { 1103 return; 1104 } 1105 var target = tools.find(function (tool) { 1106 return ( 1107 tool.id === "interactive-maps" 1108 && tool.path === "interactive-maps/" 1109 && !tool.externalUrl 1110 ); 1111 }); 1112 if (!target) return; 1113 var targetUrl = new URL(target.path, window.location.href); 1114 if ( 1115 targetUrl.origin !== window.location.origin 1116 || targetUrl.pathname !== 1117 new URL("interactive-maps/", window.location.href).pathname 1118 ) { 1119 return; 1120 } 1121 var targetHash = data.hash.replace(/^#/, ""); 1122 syncShellUrl(target, targetHash, false); 1123 loadRoute(); 1124 return; 1125 } 1126 if (data.type !== "aog-tool-state") return; 1127 if (!activeTool || data.tool !== activeTool.id) return; 1128 var childHash = String(data.hash || "").replace(/^#/, ""); 1129 syncShellUrl(activeTool, childHash, true); 1130 }); 1131 window.addEventListener("message", handleEventAlertMessage); 1132 window.addEventListener("storage", function (event) { 1133 if (event.key === eventAlertSettingsKey) {
1134 ensureBackgroundEventAlerts(); 1135 } 1136 if (event.key === changelogSeenStorageKey) { 1137 updateChangelogBadges(); 1138 } 1139 }); 1140 window.addEventListener("pointerdown", unlockEventAlertAudio, { 1141 capture: true, 1142 passive: true, 1143 }); 1144 window.addEventListener("keydown", unlockEventAlertAudio, { 1145 capture: true, 1146 }); 1147 window.addEventListener("aog-theme-change", function (event) { 1148 var theme = event.detail && event.detail.theme; 1149 setShellThemeLabel(theme); 1150 syncAllFrameThemes(theme); 1151 }); 1152 creditsButton.addEventListener("click", openCredits); 1153 creditsClose.addEventListener("click", closeCredits); 1154 creditsOverlay.addEventListener("click", function (event) { 1155 if (event.target === creditsOverlay) closeCredits(); 1156 }); 1157 window.addEventListener("keydown", function (event) { 1158 if (event.key === "Escape" && !creditsOverlay.hidden) { 1159 closeCredits(); 1160 return; 1161 } 1162 if (event.key !== "Tab" || creditsOverlay.hidden) return; 1163 var focusable = creditsPanel.querySelectorAll('a[href], button:not(:disabled), [tabindex]:not([tabindex="-1"])'); 1164 if (!focusable.length) { 1165 event.preventDefault(); 1166 creditsPanel.focus(); 1167 return; 1168 } 1169 var first = focusable[0]; 1170 var last = focusable[focusable.length - 1]; 1171 if (event.shiftKey && (document.activeElement === first || !creditsPanel.contains(document.activeElement))) { 1172 event.preventDefault(); 1173 last.focus(); 1174 } else if (!event.shiftKey && document.activeElement === last) { 1175 event.preventDefault(); 1176 first.focus(); 1177 } 1178 }); 1179 1180 function initializeLocalizedShell() { 1181 renderNav(); 1182 renderLanding(); 1183 loadGameVersion(); 1184 loadChangelogLatest(); 1185 loadRoute(); 1186 ensureBackgroundEventAlerts(); 1187 setShellThemeLabel(root.dataset.theme || "light"); 1188 syncAllFrameThemes(root.dataset.theme); 1189 } 1190 if (window.AOG_I18N) { 1191 window.AOG_I18N.ready.then(initializeLocalizedShell); 1192 window.AOG_I18N.ready.then(function () { 1193 if (activeTool) {
1194 setActive(activeTool, childHashForTool(activeTool)); 1195 } 1196 Object.keys(framesById).forEach(function (id) { 1197 var frameTool = tools.find(function (tool) { 1198 return tool.id === id; 1199 }); 1200 if (frameTool) { 1201 framesById[id].title = localizedPhrase(frameTool.label) 1202 + " - " 1203 + localizedPhrase("Angels Online Tools"); 1204 } 1205 }); 1206 setShellThemeLabel(root.dataset.theme || "light"); 1207 }); 1208 } else { 1209 initializeLocalizedShell(); 1210 } 1211 })();
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.