PageSourceSearch

https://angelsonline.wiki/assets/scripts/home.js

js angelsonline.wiki collected 2026-10-06 13:17:15 UTC 59,900 bytes, 1,211 lines download raw bytes

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.